引言
很多人都听过领域模型,可是什么是领域模型,什么是领域,如何使用,是很多人的盲点。
DDD,用一句话来概括,即把业务逻辑映射到代码层面,形成连贯可传承的结构化代码,而不是一堆片段式的为了实现业务的代码。
这里的几个关键字,传承,结构化,DDD是有成本的,一般一个项目发展到一定程度才建议使用。
一个好的ddd的实现,是最终形成的代码与当前流行的技术栈持久层分开的,一段只属于公司核心业务的代码,与技术栈的迭代解耦,同时代码与公司的业务发展基本保持相同节奏迭代传承。
通常开启的一个ddd的流程,大概是这样的。(下面可能有很多名词,大家先不用刻意理解,后面我会细说)
战略设计-》战术设计-》具体代码落地
DDD 包括战略设计和战术设计:
战略设计:主要面向业务完成领域建模,划分边界和职责。
战术设计:是根据领域模型完成微服务设计的过程,落地架构和代码。
战略设计
一个正式的战略设计落地文档,如下图所示
事件风暴
那么我们该采用什么样的方法,才能从错综复杂的业务领域中分析并构建领域模型呢,即划分出边界
就是事件风暴。事件风暴是一项团队活动,领域专家与项目团队通过头脑风暴的形式,罗列出领域中所有的领域事件,整合之后形成最终的领域事件集合,然后对每一个事件,标注出导致该事件的命令,再为每一个事件标注出命令发起方的角色。命令可以是用户发起,也可以是第三方系统调用或者定时器触发等,最后对事件进行分类,整理出实体、聚合、聚合根以及限界上下文。而事件风暴正是 DDD 战略设计中经常使用的一种方法,它可以快速分析和分解复杂的业务领域,完成领域建模。
事件风暴,类似于盲人摸象,找一堆盲人过来,慢慢摸,最终汇总出整体的系统,同时在盲人之间达成共识。因为涉及到职责与任务的划分。
一个好的事件风暴,会将,命令,实体,领域事件,补充信息,提取出来。形成具体的核心域,子域、核心域、通用域、支撑域。并以文档的形式落地。(循环迭代)
在领域建模的过程中,我们需要重点关注这类业务的语言和行为。比如某些业务动作或行为(事件)是否会触发下一个业务动作,这个动作(事件)的输入和输出是什么?是谁(实体)发出的什么动作(命令),触发了这个动作(事件),按照时间线,将事件排序,汇总,梳理出因果依赖关系,我们可以从这些暗藏的词汇中,分析出领域模型中的事件、命令和实体等领域对象。
这个过程,需要,各色人参与,有领域专家打头阵,通过可视化、高互动的方式一步一步将领域模型设计出来。领域专家是事件风暴中必不可少的核心参与者。
领域专家就是对业务或问题域有深刻见解的主题专家,他们非常了解业务和系统是怎么做的,同时也深刻理解为什么要这样设计。如果你的公司里并没有这个角色,那也没关系,你可以从业务人员、需求分析人员、产品经理或者在这个领域有多年经验的开发人员里,按照这个标准去选择合适的人选。
除了领域专家,事件风暴的其他参与者可以是 DDD专家、架构师、产品经理、项目经理、开发人员和测试人员等项目团队成员。
领域建模是统一团队语言的过程,因此项目团队应尽早地参与到领域建模中,这样才能高效建立起团队的通用语言。到了微服务建设时,领域模型也更容易和系统架构保持一致。
这个过程,并不是一次就能形成结论,需要反复研讨。
形成落地文档之后,此时领域划分已完成。进入第二阶段,战术设计。
聚合设计
在事件风暴中,我们会根据一些业务操作和行为找出实体(Entity)或值对象(ValueObject),进而将业务关联紧密的实体和值对象进行组合,构成聚合,再根据业务语义将多个聚合划定到同一个限界上下文(Bounded Context)中,并在限界上下文内完成领域建模。
聚合是DDD中的一个重要概念,它对外代表的是一个整体,类似于一个大的对象,内部是由有主从之分的很多对象组成的。聚合是一个行为在逻辑上高度一致的对象群,注意,它是一个对象群体的总称。聚合的内部结构如同一棵树,每个聚合都有一个根,其他对象和聚合根之间都是枝叶与树根的关系。
这样有序化的好处是:只有根 能引用或指向其他对象,根 自身不能被其他任何对象引用,根类似团队的小组长,队员都要向其汇报工作。这就是聚合根的设计来源,聚合根拥有自己边界内的数据所有权,以及行为职责的管理权限。数据和行为两者兼顾的所有权只有聚合才能具有,为什么需要数据和行为两者兼顾呢?通常情况下,数据和行为是分离的,行为在服务中实现,而数据隔离在数据表中,行为通过服务转为SQL语句去操作数据表,这种方式的问题是隔离了行为和数据的紧密逻辑关系。
通常聚合的产生和领域模式同时产生的
以投保业务为案例,聚合的产生
一个聚合的诞生通常由有以下几个步骤
-
采用事件风暴,根据业务行为,梳理出在投保过程中发生这些行为的所有的实体和值对象,比如投保单、标的、客户、被保人等等。
-
从众多实体中选出适合作为对象管理者的根实体,也就是聚合根。判断一个实体是否是聚合根,你可以结合以下场景分析:是否有独立的生命周期?是否有全局唯一 ID?是否可以创建或修改其它对象?是否有专门的模块来管这个实体。图中的聚合根分别是投保单和客户实体。
-
根据业务单一职责和高内聚原则,找出与聚合根关联的所有紧密依赖的实体和值对象。构建出 1 个包含聚合根(唯一)、多个实体和值对象的对象集合,这个集合就是聚合。在图中我们构建了客户和投保这两个聚合。
-
在聚合内根据聚合根、实体和值对象的依赖关系,画出对象的引用和依赖模型。这里我需要说明一下:投保人和被保人的数据,是通过关联客户 ID 从客户聚合中获取的,在投保聚合里它们是投保单的值对象,这些值对象的数据是客户的冗余数据,即使未来客户聚合的数据发生了变更,也不会影响投保单的值对象数据。从图中我们还可以看出实体之间的引用关系,比如在投保聚合里投保单聚合根引用了报价单实体,报价单实体则引用了报价规则子实体。
-
多个聚合根据业务语义和上下文一起划分到同一个限界上下文内。
这就是一个聚合诞生的完整过程。
战术设计
微服务,落地拆分,不一定是按照ddd具体的领域拆分,通常微服务的拆分有以下几个策略
微服务拆分
基于领域模型进行拆分
围绕业务领域按职责单一性、功能完整性拆分。
基于业务需求变化频率
识别领域模型中的业务需求变动频繁的功能,考虑业务变更频率与相关度,将业务需求变动较高和功能相对稳定的业务进行分离。这是因为需求的经常性变动必然会导致代码的频繁修改和版本发布,这种分离可以有效降低频繁变动的敏态业务对稳态业务的影响。
基于应用性能
识别领域模型中性能压力较大的功能。因为性能要求高的功能可能会拖累其它功能,在资源要求上也会有区别,为了避免对整体性能和资源的影响,我们可以把在性能方面有较高要求的功能拆分出去。
不同领域之间,通过六边形架构,来做解耦。
实现代码落地
这里初步只具体展示俩个最核心的概念的落地,聚合,领域事件,领域服务,其他的下一篇详聊。
按照聚合跟的设计,根据不同编程不同编程语言有不同的实现,在java等面向对象的语言中,聚合根与聚合对象的联系就是,类和属性的关系,由聚合根统一向外提供服务。
public class Leave {
String id;
Applicant applicant;
Approver approver;
LeaveType type;
Status status;
Date startTime;
Date endTime;
long duration;
int leaderMaxLevel; //审批领导的最高级别
ApprovalInfo currentApprovalInfo;
List<ApprovalInfo> historyApprovalInfos;
public long getDuration() {
return endTime.getTime() - startTime.getTime();
}
public Leave addHistoryApprovalInfo(ApprovalInfo approvalInfo) {
if (null == historyApprovalInfos)
historyApprovalInfos = new ArrayList<>();
this.historyApprovalInfos.add(approvalInfo);
return this;
}
public Leave create(){
this.setStatus(Status.APPROVING);
this.setStartTime(new Date());
return this;
}
//其它方法
}
领域事件
即之前战略设计事件风暴中汇总出的业务的事件
比如,请假审批系统中的,创建请假单和请假审批过程中会产生的事件。领域事件实体在聚合仓储内完成持久化,但是事件实体的生命周期不受聚合根管理。
public class DomainEvent {
String id;
Date timestamp;
String source;
String data;
}
领域服务
如果一个业务行为由多个实体对象参与完成,我们就将这部分业务逻辑放在领域服务中实现。 领域服务与实体方法的区别是:实体方法完成单一实体自身的业务逻辑,是相对简单的原子业务逻辑,而领域服务则是多个实体组合出的相对复杂的业务逻辑。两者都在领域层,实现领域模型的核心业务能力。
一个聚合可以设计一个领域服务类,管理聚合内所有的领域服务。
请假聚合的领域服务类是 LeaveDomainService。领域服务中会用到很多的 DDD 设计模式,比如:用工厂模式实现复杂聚合的实体数据初始化,用仓储模式实现领域层与基础层的依赖倒置和用领域事件实现数据的最终一致性等。
聚合和领域服务的关系,相当于程序员和计算机的关系,主人和工具。
public class LeaveDomainService {
@Autowired
EventPublisher eventPublisher;
@Autowired
LeaveRepositoryInterface leaveRepositoryInterface;
@Autowired
LeaveFactory leaveFactory;
@Transactional
public void createLeave(Leave leave, int leaderMaxLevel, Approver approver) {
leave.setLeaderMaxLevel(leaderMaxLevel);
leave.setApprover(approver);
leave.create();
leaveRepositoryInterface.save(leaveFactory.createLeavePO(leave));
LeaveEvent event = LeaveEvent.create(LeaveEventType.CREATE_EVENT, leave);
leaveRepositoryInterface.saveEvent(leaveFactory.createLeaveEventPO(event));
eventPublisher.publish(event);
}
@Transactional
public void updateLeaveInfo(Leave leave) {
LeavePO po = leaveRepositoryInterface.findById(leave.getId());
if (null == po) {
throw new RuntimeException("leave does not exist");
}
leaveRepositoryInterface.save(leaveFactory.createLeavePO(leave));
}
@Transactional
public void submitApproval(Leave leave, Approver approver) {
LeaveEvent event;
if (ApprovalType.REJECT == leave.getCurrentApprovalInfo().getApprovalType()) {
leave.reject(approver);
event = LeaveEvent.create(LeaveEventType.REJECT_EVENT, leave);
} else {
if (approver != null) {
leave.agree(approver);
event = LeaveEvent.create(LeaveEventType.AGREE_EVENT, leave); } else {
leave.finish();
event = LeaveEvent.create(LeaveEventType.APPROVED_EVENT, leave);
}
}
leave.addHistoryApprovalInfo(leave.getCurrentApprovalInfo());
leaveRepositoryInterface.save(leaveFactory.createLeavePO(leave));
leaveRepositoryInterface.saveEvent(leaveFactory.createLeaveEventPO(event));
eventPublisher.publish(event);
}
public Leave getLeaveInfo(String leaveId) {
LeavePO leavePO = leaveRepositoryInterface.findById(leaveId);
return leaveFactory.getLeave(leavePO);
}
public List<Leave> queryLeaveInfosByApplicant(String applicantId) {
List<LeavePO> leavePOList = leaveRepositoryInterface.queryByApplicantId(applicantId);
return leavePOList.stream().map(leavePO -> leaveFactory.getLeave(leavePO)).collect(Collectors.toList());
}
public List<Leave> queryLeaveInfosByApprover(String approverId) {
List<LeavePO> leavePOList = leaveRepositoryInterface.queryByApproverId(approverId);
return leavePOList.stream().map(leavePO -> leaveFactory.getLeave(leavePO)).collect(Collectors.toList());
}
}
未完待续