欢迎咱们重视大众号「JAVA前线」检查更多精彩共享文章,首要包含源码剖析、实践使用、架构思想、职场共享、产品考虑等等,一起欢迎咱们加我个人微信「java_front」一同交流学习
1 为什么要写技能文档
回忆软件开发历史,咱们可以将其分为程序规划年代、程序体系年代和软件工程年代三大历史阶段。
在程序规划年代(1946-1956),软件开发首要依赖于个人编程技巧,技能文档只要存在个人开发者的大脑即可,由于没有交流和协作需求,编写技能文档也不具有紧迫性。
在程序体系年代(1956-1968),计算机性能明显提升,使用范围和规划逐渐扩展,以至于依靠个人无法完结软件的开发,所以呈现了团队协作。在早期团队协作过程中,开发者依然保持了早期各自为战的开发习惯,即便呈现了一些办法论雏形,也无法从根本上操控交流和协作的巨大本钱,软件危机就此呈现。1968年国际学术会议提出了软件危机和软件工程的概念:
软件危机界说是落后的软件生产办法无法满意迅速增长的计算机软件需求,然后导致开发与保护过程中呈现一系列严重问题的现象。软件工程界说是树立并运用完善的工程化准则,以较经济的手法取得能在实践机器上有效运转的可靠软件的一系列办法
从此软件开发进入工程化阶段,也应运而生了很多开发办法论和开发模型。其间规范和完善的文档是软件工程重要组成部分,可以很大程度上削减交流和协作本钱,而开发文档又是技能文档重要组成部分。
2 开发文档要体现什么
软件体系生命周期包含界说、开发、运维、消亡这四大阶段。界说阶段包含界说问题、可行性研究和需求剖析。开发阶段包含概要规划、详细规划、代码编写和测试阶段。运维阶段包含更正性保护、适应性保护、预防性保护和完善性保护。消亡阶段包含体系作废、高雅下线和留传体系。
生命周期每个阶段固然有各自的重要性,可是开发者更应该重视界说阶段与开发阶段。界说阶段需求处理为什么开发(why)、需求是什么(what)两个问题,开发阶段需求处理怎样规划,怎样编码,怎样测试(how)三个问题。
开发文档是否需求体现界说和开发的一切子阶段?我以为也无必要。问题界说和可行性研究首要由产品司理负责,测试阶段首要由测试人员负责,开发者可以重视但不是必须体现在开发文档。我以为开发文档必须要体现需求剖析、概要规划、详细规划、代码编写四个子阶段。
3 七大维度
我以为一份完好开发文档应该至少具有七大维度,每个维度描绘体系的一个旁边面,组合在一同终究描绘出整个体系,这些维度分别是:
四色分范畴
用例看功用
流程三剑客
范畴与数据
纵横做规划
分层看架构
接口看对接
本文咱们剖析一个足球运动员信息办理体系,这个体系咱们可能也没有做过,咱们一同剖析这个体系。需求阐明本文侧重介绍办法论的落地,事务细节难以八面玲珑。
3.1 四色分范畴
3.1.1 流程整理
首要整理事务流程,这里有两个问题需求考虑,榜首个问题是从什么视角去整理?由于不同的人看到的流程是不相同的。答案是取决于体系需求处理什么问题,由于咱们要办理运动员从转会到上场竞赛整条链路信息,所以从运动员视角出发是一个合适的挑选。
第二个问题是对事务不熟悉怎样办?由于咱们不是体育和运动专家,并不清楚整条链路的事务细节。答案是整理流程时必定要有事务专家在场,由于没有真实事务细节,无法范畴驱动规划。同理在互联网整理杂乱事务流程时,必定要有对相关事务熟悉的产品司理或许运营一同参加。
假定足球事务专家整理出了事务流程,运动员提出转会,协商一致后到新沙龙体检,体检经过就进行签约。进入新沙龙后进行练习,练习目标合格后上场竞赛,赛后参加新闻发布会。当然实践流程会杂乱很多,本文还是侧重解说办法论。
3.1.2 四色建模
(1) 时标目标
四色建模榜首种颜色是红色,表明时标目标。时标目标是四色建模最重要的目标,可以理解为中心事务单据。在事务进行过程中必定要对关键事务留下单据,经过这些单据可以追溯出整个事务流程。
时标目标具有两个特点:榜首是事实不行变性,记录了曩昔某个时刻点或时刻段内产生的事实。第二是职责可追溯性,记录了办理者重视的信息。现在咱们剖析本体系时标目标有哪些,需求留下哪些中心事务单据。
转会对应转会单据,体检对应体检单据,签合同对应合同单据,练习对应练习目标单据,竞赛对应竞赛目标单据,新闻发布会对应采访单据。依据剖析制作如下时标目标:
(2) 参加方、地、物
这三类目标在四色建模顶用绿色表明,咱们以电商场景为例进行阐明。用户付出购买商家的产品时,用户和商家是参加方。物流体系发货时配送单据需求有配送地址目标,地址目标就是地。订单需求产品目标,物流配送需求有货品,产品和货品就是物。
咱们剖析本例可以知道参加方包含总司理、队医、教练、球迷、记者,地包含练习地址、竞赛地址、采访地址,物包含签名球衣和签名足球:
(3) 人物目标
在四色建模顶用黄色表明,这类目标表明参加方、地、物以什么人物参加到事务流程:
(4) 描绘目标
咱们可以为目标添加相关描绘信息,在四色建模顶用蓝色表明:
3.1.3 区分范畴
在四色建模过程中咱们体会到时标目标是最重要的目标,由于其承载了事务体系中心单据。在区分范畴时咱们同样离不开时标目标,经过收敛相关时标目标区分范畴。
3.1.4 范畴事情
当事务体系产生一件事情时,假如本范畴或其它范畴有后续动作跟进,那么咱们把这件事情称为范畴事情,这个事情需求被感知。
例如球员竞赛受伤了,这是竞赛子域事情,可是医疗和练习子域是需求感知的,那么竞赛子域就发出一个事情,医疗和练习子域会订阅。球员竞赛取得进球,这也是竞赛子域事情,可是练习和合同子域也会重视这个事情,所以竞赛子域也会发出一个竞赛进球事情,练习和合同子域会订阅。
经过事情交互有一个问题需求留意,经过事情订阅完成事务只能选用终究一致性,需求放弃强一致性,可能会引进新的杂乱度需求权衡。
3.2 用例看功用
目前为止范畴现已确认了,大范畴现已拆分成了小范畴,咱们现已不再束手无策,而是可以对小范畴进行用例剖析了。用例图由参加者和用例组成,意图是答复这样一个问题:什么人运用体系干什么事。
下图表明在竞赛范畴,运动员视角(什么人)运用体系进行进球计算,助攻计算,犯规计算,跑动间隔计算,竞赛评分计算,传球成功率计算,受伤计算(干什么事),同理咱们也可以挑选四色建模中其它参加者视角制作用例图。
include关键字表明包含联系。例如竞赛是基用例,包含了进球计算,助攻计算,犯规计算,跑动间隔计算,竞赛评分计算,传球成功率计算,受伤计算七个子用例。包含联系表明法有两个长处:榜首是可以清晰地安排子用例,第二是有利于子用例复用,例如主教练视角用例图也包含竞赛评分,那么就可以直接指向竞赛评分子用例。
extend关键字表明扩展联系。例如点球计算是进球计算的扩展,由于不必定可以取得点球,所以点球计算即便不存在,也不会影响进球计算功用。黄牌计算、红牌计算是犯规计算的扩展,由于一般犯规不会取得红黄牌,所以红黄牌计算不存在,也不会影响犯规计算功用。
用例图不关心完成细节,而是从一种外部视角描绘体系功用,即便不了解完成细节的人,经过看用例图也可以快速了解体系功用,这个特性规定了用例图不宜过于杂乱,可以阐明中心功用即可。
3.3 流程三剑客
用例图是从外部视角描绘体系,可是剖析体系总是要深入体系内部的,其间流程视图就是描绘体系内怎么流通的视图。活动图、序列图、状况机图是流程视图中最重要的三种视图,咱们称为流程三剑客。三者侧重点有所不同:活动图侧重于逻辑分支,次序图侧重于交互,状况机图侧重于状况流通。
3.3.1 活动图
活动图合适描绘杂乱逻辑分支,想象这样一种事务场景,球队需求选出一名球员成为球队的足球先生,选拔规范如下:前场、中场、后场、门将各选出一名候选球员。前场队员顺次比较进球数、助攻数,中场队员顺次比较助攻数、抢断数,后场队员顺次比较解围数、抢断数,门将顺次比较补救数、扑点数,假如一切目标均相同则抽签。每个位置有人选之后,全体教练组投票,假如投票数相同则抽签。
咱们常常说一图胜千言,其间一个重要原因是文字是线性的,所以表达逻辑分支才能不如流程视图,而在流程视图中表达逻辑分支才能最强的是活动图。
3.3.2 次序图
次序图侧重于交互,合适依照时刻次序体现一个事务流程中交互细节,可是次序图并不擅长体现杂乱逻辑分支。
假如某个逻辑分支特别重要,可以挑选再画一个次序图。例如付出流程中有付出成功正常流程,也有付出失利反常流程,这两个流程都非常重要,所以可以用两张次序图体现。回到本文实例,咱们可以经过次序图体现球员从提出转会到竞赛全流程。
3.3.3 状况机图
假定一条数据有ABC三种状况,从正常事务视点来看,状况只能从A流通到B,再从B流通到C,不能乱序也不行逆。可是可能呈现这种反常情况:数据当时状况为A,接收异步音讯更改状况,B音讯由于延时晚于C音讯,终究导致状况先改为C再改为B,那么此时状况就是错误的。
状况机图侧重于状况流通,阐明晰哪些状况之间可以彼此流通,再结合状况机代码形式,可以处理上述状况反常情况。回到本文实例,咱们可以经过状况机图表明球员从提出转会到签约整个状况流程。
3.4 范畴与数据
上述章节从功用层面和流程层面进行了体系剖析,现在需求从数据层剖析体系,咱们首要对比两组概念:值目标与实体,范畴目标与数据目标。
实体是具有仅有标识的目标,仅有标识会伴随实体目标整个生命周期而且不行改动。值目标本质上是特点的集合,没有仅有标识。
范畴目标与数据目标一个重要的区别是值目标存储办法。范畴目标在包含值目标的一起也保留了值目标的事务意义,而数据目标可以运用更加松散的结构保存值目标,简化数据库规划。
现在咱们需求办理足球运动员基本信息和竞赛数据,对应范畴模型和数据模型应该怎么规划?名字、身高、体重是一名运动员本质特点,加上仅有编号可以对应实体目标。跑动间隔,传球成功率,进球数是运动员竞赛体现,这些特点的集合可以对应值目标。
咱们依据图示编写范畴目标与数据目标代码:
// 数据目标
public class FootballPlayerDO {
private Long id;
private String name;
private Integer height;
private Integer weight;
private String gamePerformance;
}
// 范畴目标
public class FootballPlayerDMO {
private Long id;
private String name;
private Integer height;
private Integer weight;
private GamePerformanceVO gamePerformanceVO;
}
public class GamePerformanceVO {
private Double runDistance;
private Double passSuccess;
private Integer scoreNum;
}
为什么要选用JSON存储值目标?由于脚本化是一种拓宽灵活性的办法,脚本化不只指运用groovy、QLExpress脚本增强体系灵活性,还包含松散可扩展的数据结构。数据模型笼统出了名字、身高、体重这些基本特点,关于频繁改动的竞赛体现特点,这些特点值可能常常改动,乃至特点自身也是常常改动,可能会加上射门次数,打破次数等,所以选用松散结构进行存储。
假如需求依据JSON结构中KEY进行检索,例如查询进球数大于5的球员,这也不是没有办法。咱们可以将MySQL表中数据平铺到ES中,一条数据依据JSON KEY平铺变成多条数据,这样就可以进行检索了。
3.5 纵横做规划
杂乱事务之所以杂乱,一个重要原因是触及人物或许类型较多,很难平淡无奇地进行规划,所以咱们需求添加剖析维度。其间最常见的是添加横向和纵向两个维度,本文也侧重讨论两个维度。整体而言横向扩展的是考虑广度,纵向扩展的是考虑深度,对应到体系规划而言可以总结为:纵向做阻隔,横向做编列。
咱们首要剖析一个下单场景做衬托。当时有ABC三种订单类型,A订单价格9折,物流最大分量不能超越8公斤,不支撑退款。B订单价格8折,物流最大分量不能超越5公斤,支撑退款。C订单价格7折,物流最大分量不能超越1公斤,支撑退款。依照需求字面意义平淡无奇地写代码也并不难:
public class OrderServiceImpl implements OrderService {
@Resource
private OrderMapper orderMapper;
@Override
public void createOrder(OrderBO orderBO) {
if (null == orderBO) {
throw new RuntimeException("参数反常");
}
if (OrderTypeEnum.isNotValid(orderBO.getType())) {
throw new RuntimeException("参数反常");
}
// A类型订单
if (OrderTypeEnum.A_TYPE.getCode().equals(orderBO.getType())) {
orderBO.setPrice(orderBO.getPrice() * 0.9);
if (orderBO.getWeight() > 9) {
throw new RuntimeException("超越物流最大分量");
}
orderBO.setRefundSupport(Boolean.FALSE);
}
// B类型订单
else if (OrderTypeEnum.B_TYPE.getCode().equals(orderBO.getType())) {
orderBO.setPrice(orderBO.getPrice() * 0.8);
if (orderBO.getWeight() > 8) {
throw new RuntimeException("超越物流最大分量");
}
orderBO.setRefundSupport(Boolean.TRUE);
}
// C类型订单
else if (OrderTypeEnum.C_TYPE.getCode().equals(orderBO.getType())) {
orderBO.setPrice(orderBO.getPrice() * 0.7);
if (orderBO.getWeight() > 7) {
throw new RuntimeException("超越物流最大分量");
}
orderBO.setRefundSupport(Boolean.TRUE);
}
// 保存数据
OrderDO orderDO = new OrderDO();
BeanUtils.copyProperties(orderBO, orderDO);
orderMapper.insert(orderDO);
}
}
上述代码从功用上完全可以完成事务需求,可是程序员不只要满意功用,还需求考虑代码的可保护性。假如新增一种订单类型,或许新增一个订单特点处理逻辑,那么咱们就要在上述逻辑中新增代码,假如处理不慎就会影响原有逻辑。
为了防止牵一发而动全身这种情况,规划形式中的开闭准则要求咱们面向新增开放,面向修改封闭,我以为这是规划形式中最重要的一条准则。
需求改动经过扩展,而不是经过修改已有代码完成,这样就保证代码稳定性。扩展也不是随意扩展,由于事前界说了算法,扩展也是依据算法扩展,用笼统构建框架,用完成扩展细节。规范意义的二十三种规划形式说到底终究都是在遵从开闭准则。
怎么改动平淡无奇的考虑办法?这就要为问题剖析加上纵向和横向两个维度,我挑选运用剖析矩阵办法,其间纵向表明战略,横向表明场景。
3.5.1 纵向做阻隔
纵向维度表明战略,不同战略在逻辑上和事务上应该是阻隔的,本实例包含优惠战略、物流战略和退款战略,战略作为笼统,不同订单类型去扩展这个笼统,战略形式非常合适这种场景。本文详细详细剖析优惠战略,物流战略和退款战略同理。
// 优惠战略
public interface DiscountStrategy {
public void discount(OrderBO orderBO);
}
// A类型优惠战略
@Component
public class TypeADiscountStrategy implements DiscountStrategy {
@Override
public void discount(OrderBO orderBO) {
orderBO.setPrice(orderBO.getPrice() * 0.9);
}
}
// B类型优惠战略
@Component
public class TypeBDiscountStrategy implements DiscountStrategy {
@Override
public void discount(OrderBO orderBO) {
orderBO.setPrice(orderBO.getPrice() * 0.8);
}
}
// C类型优惠战略
@Component
public class TypeCDiscountStrategy implements DiscountStrategy {
@Override
public void discount(OrderBO orderBO) {
orderBO.setPrice(orderBO.getPrice() * 0.7);
}
}
// 优惠战略工厂
@Component
public class DiscountStrategyFactory implements InitializingBean {
private Map<String, DiscountStrategy> strategyMap = new HashMap<>();
@Resource
private TypeADiscountStrategy typeADiscountStrategy;
@Resource
private TypeBDiscountStrategy typeBDiscountStrategy;
@Resource
private TypeCDiscountStrategy typeCDiscountStrategy;
public DiscountStrategy getStrategy(String type) {
return strategyMap.get(type);
}
@Override
public void afterPropertiesSet() throws Exception {
strategyMap.put(OrderTypeEnum.A_TYPE.getCode(), typeADiscountStrategy);
strategyMap.put(OrderTypeEnum.B_TYPE.getCode(), typeBDiscountStrategy);
strategyMap.put(OrderTypeEnum.C_TYPE.getCode(), typeCDiscountStrategy);
}
}
// 优惠战略履行
@Component
public class DiscountStrategyExecutor {
private DiscountStrategyFactory discountStrategyFactory;
public void discount(OrderBO orderBO) {
DiscountStrategy discountStrategy = discountStrategyFactory.getStrategy(orderBO.getType());
if (null == discountStrategy) {
throw new RuntimeException("无优惠战略");
}
discountStrategy.discount(orderBO);
}
}
3.5.2 横向做编列
横向维度表明场景,一种订单类型在广义上可以以为是一种事务场景,在场景中将独立的战略进行串联,模板办法规划形式适用于这种场景。
模板办法形式是一般运用笼统类界说一个算法骨架,一起界说一些笼统办法,这些笼统办法延迟到子类完成,这样子类不只遵守了算法骨架约定,也完成了自己的算法。既保证了规约也统筹灵活性,这就是用笼统构建框架,用完成扩展细节。
// 创立订单服务
public interface CreateOrderService {
public void createOrder(OrderBO orderBO);
}
// 笼统创立订单流程
public abstract class AbstractCreateOrderFlow {
@Resource
private OrderMapper orderMapper;
public void createOrder(OrderBO orderBO) {
// 参数校验
if (null == orderBO) {
throw new RuntimeException("参数反常");
}
if (OrderTypeEnum.isNotValid(orderBO.getType())) {
throw new RuntimeException("参数反常");
}
// 计算优惠
discount(orderBO);
// 计算分量
weighing(orderBO);
// 退款支撑
supportRefund(orderBO);
// 保存数据
OrderDO orderDO = new OrderDO();
BeanUtils.copyProperties(orderBO, orderDO);
orderMapper.insert(orderDO);
}
public abstract void discount(OrderBO orderBO);
public abstract void weighing(OrderBO orderBO);
public abstract void supportRefund(OrderBO orderBO);
}
// 完成创立订单流程
@Service
public class CreateOrderFlow extends AbstractCreateOrderFlow {
@Resource
private DiscountStrategyExecutor discountStrategyExecutor;
@Resource
private ExpressStrategyExecutor expressStrategyExecutor;
@Resource
private RefundStrategyExecutor refundStrategyExecutor;
@Override
public void discount(OrderBO orderBO) {
discountStrategyExecutor.discount(orderBO);
}
@Override
public void weighing(OrderBO orderBO) {
expressStrategyExecutor.weighing(orderBO);
}
@Override
public void supportRefund(OrderBO orderBO) {
refundStrategyExecutor.supportRefund(orderBO);
}
}
3.5.3 归纳使用
上述实例事务和代码并不杂乱,其实杂乱事务场景也不过是简单场景的叠加、组合和交织,无外乎也是经过纵向做阻隔、横向做编列寻求答案。
纵向维度笼统出才能池这个概念,才能池中包含许多才能,不同的才能依照不同事务维度聚合,例如优惠才能池,物流才能池,退款才能池。咱们可以看到两种程度的阻隔性,才能池之间彼此阻隔,才能之间也彼此阻隔。
横向维度将才能从才能池选出来,依照事务需求串联在一同,构成不同事务流程。由于才能可以恣意组合,所以体现了很强的灵活性。除此之外,不同才能既可以串行履行,假如不同才能之间没有依赖联系,也可以如同流程Y相同并行履行,提升履行功率。
此时可以回到本文足球运动员办理体系,假如咱们选用纵横思想,剖析3.3.1足球先生选拔事务场景可以得到下图:
纵向阻隔出进攻才能池,防守才能池,门将才能池,横向编列出前场、中场、后场、门将四个流程,在不同流程中可以恣意从才能池中挑选才能进行组合,而不是编写冗长的判别逻辑,明显提升了代码可扩展性。
3.6 分层看架构
架构整体而言分为两个层次,榜首种层次是指本项目在整个公司位于哪一层。持久层、缓存层、中间件、事务中台、服务层、网关层、客户端和署理层是常见的分层架构,大多数情况下事务需求终究会体现在服务层,不同的范畴对应不同的微服务。
第二种层次是指本项目内部代码的安排办法,一般分为接口层,拜访层,事务层,范畴层,外部拜访层,根底层。
(1) api
接口层:供给面向外部接口声明和DTO
(2) controller
拜访层:供给HTTP拜访进口
(3) service
事务层:供给BO目标,范畴层和事务层都包含事务,可是用途不同。事务层可以组合不同范畴事务,而且可以添加流控、监控、日志、权限操控切面,相较于范畴层更为丰富
(4) domain
范畴层:供给DMO、VO、事情、DO和数据拜访,中心是依据范畴进行分包,范畴内高内聚,范畴间低耦合
(5) dependency
外部拜访层:在这个模块中调用外部RPC服务,解析返回码和返回数据
(6) infrastructure
根底层:包含通用根底功用,例如根底东西,缓存东西,打印日志,音讯发送
本文展开范畴层进行剖析。范畴层中心是依照范畴进行分包,而且供给DMO、VO、事情、DO和数据拜访,范畴内高内聚,范畴间低耦合。
3.7 接口看对接
当一个接口代码编写完结后,那么这个接口怎么调用,输入和输出参数是什么,这些问题需求在接口文档中得到答复。接口文档生成有两种办法:榜首种是主动生成,例如运用Swagger,第二种办法是手工生成。
主动生成长处是代码即文档,还具有调试功用,在公司内部进行联调时非常便利。可是假如接口是供给给外部第三方运用,那么还是需求手工编写接口文档。关于一个接口的描绘无外乎接口称号、接口阐明、接口协议,输入参数、输出参数信息。
4 文章总结
本文经过一个事务实例介绍了开发文档七大维度:四色分范畴、用例看功用、流程三剑客、范畴与数据、纵横做规划、分层看架构、接口看对接。每个维度描绘体系的一个旁边面,组合在一同终究描绘出整个体系。
在实践开发中假如需求不大,那么也不是这七个维度都要体现,而是依据实践情况作取舍,可以把方案说清楚即可,希望本文对咱们有所帮助。
欢迎咱们重视大众号「JAVA前线」检查更多精彩共享文章,首要包含源码剖析、实践使用、架构思想、职场共享、产品考虑等等,一起欢迎咱们加我个人微信「java_front」一同交流学习