This commit is contained in:
louzefeng
2024-07-09 18:38:56 +00:00
parent 8bafaef34d
commit bf99793fd0
6071 changed files with 1017944 additions and 0 deletions

View File

@@ -0,0 +1,858 @@
<audio id="audio" title="基于DDD的微服务设计实例代码详解" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/d5/45/d5b286c3cf318d5d5b4daf510204c845.mp3"></audio>
你好,我是欧创新。好久不见,今天我带着你期待的案例来了。
还记得我们在 [[第 18 讲]](https://time.geekbang.org/column/article/169881) 中用事件风暴完成的“在线请假考勤”项目的领域建模和微服务设计吗今天我们就在这个项目的基础上看看用DDD方法设计和开发出来的微服务代码到底是什么样的点击 [Github](https://github.com/ouchuangxin/leave-sample) 获取完整代码,接下来的内容是我对代码的一个详解,期待能帮助你更好地实践我们这个专栏所学到的知识。
## 项目回顾
“在线请假考勤”项目中,请假的核心业务流程是:请假人填写请假单提交审批;根据请假人身份、请假类型和请假天数进行校验并确定审批规则;根据审批规则确定审批人,逐级提交上级审批,逐级核批通过则完成审批,否则审批不通过则退回申请人。
在 [[第 18 讲]](https://time.geekbang.org/column/article/169881) 的微服务设计中我们已经拆分出了两个微服务请假和考勤微服务。今天我们就围绕“请假微服务”来进行代码详解。微服务采用的开发语言和数据库分别是Java、Spring boot 和 PostgreSQL。
## 请假微服务采用的DDD设计思想
请假微服务中用到了很多的DDD设计思想和方法主要包括以下几个
<img src="https://static001.geekbang.org/resource/image/5f/92/5f22ed9bb3d5b6c63f21583469399892.jpg" alt="">
## 聚合中的对象
请假微服务包含请假leave、人员person和审批规则rule三个聚合。leave聚合完成请假申请和审核核心逻辑person聚合管理人员信息和上下级关系rule是一个单实体聚合提供请假审批规则查询。
Leave是请假微服务的核心聚合它有请假单聚合根leave、审批意见实体ApprovalInfo、请假申请人Applicant和审批人Approver值对象它们的数据来源于person聚合还有部分枚举类型如请假类型LeaveType请假单状态Status和审批状态类型ApprovalType等值对象。
下面我们通过代码来了解一下聚合根、实体以及值对象之间的关系。
### 1. 聚合根
聚合根leave中有属性、值对象、关联实体和自身的业务行为。Leave实体采用充血模型有自己的业务行为具体就是聚合根实体类的方法如代码中的getDuration和addHistoryApprovalInfo等方法。
聚合根引用实体和值对象,它可以组合聚合内的多个实体,在聚合根实体类方法中完成复杂的业务行为,这种复杂的业务行为也可以在聚合领域服务里实现。但为了职责和边界清晰,我建议聚合要根据自身的业务行为在实体类方法中实现,而涉及多个实体组合才能实现的业务能力由领域服务完成。
下面是聚合根leave的实体类方法它包含属性、对实体和值对象的引用以及自己的业务行为和方法。
```
public class Leave {
String id;
Applicant applicant;
Approver approver;
LeaveType type;
Status status;
Date startTime;
Date endTime;
long duration;
int leaderMaxLevel; //审批领导的最高级别
ApprovalInfo currentApprovalInfo;
List&lt;ApprovalInfo&gt; historyApprovalInfos;
public long getDuration() {
return endTime.getTime() - startTime.getTime();
}
public Leave addHistoryApprovalInfo(ApprovalInfo approvalInfo) {
if (null == historyApprovalInfos)
historyApprovalInfos = new ArrayList&lt;&gt;();
this.historyApprovalInfos.add(approvalInfo);
return this;
}
public Leave create(){
this.setStatus(Status.APPROVING);
this.setStartTime(new Date());
return this;
}
//其它方法
}
```
### 2. 实体
审批意见实体ApprovalInfo被leave聚合根引用用于记录审批意见它有自己的属性和值对象如approver等业务逻辑相对简单。
```
public class ApprovalInfo {
String approvalInfoId;
Approver approver;
ApprovalType approvalType;
String msg;
long time;
}
```
### 3. 值对象
在Leave聚合有比较多的值对象。
我们先来看一下审批人值对象Approver。这类值对象除了属性集之外还可以有简单的数据查询和转换服务。Approver数据来源于person聚合从person聚合获取审批人返回后从person实体获取personID、personName和level等属性重新组合为approver值对象因此需要数据转换和重新赋值。
Approver值对象同时被聚合根leave和实体approvalInfo引用。这类值对象的数据来源于其它聚合不可修改可重复使用。将这种对象设计为值对象而不是实体可以提高系统性能降低数据库实体关联的复杂度所以我一般建议优先设计为值对象。
```
public class Approver {
String personId;
String personName;
int level; //管理级别
public static Approver fromPerson(Person person){
Approver approver = new Approver();
approver.setPersonId(person.getPersonId());
approver.setPersonName(person.getPersonName());
approver.setLevel(person.getRoleLevel());
return approver;
}
}
```
下面是枚举类型的值对象Status的代码。
```
public enum Status {
APPROVING, APPROVED, REJECTED
}
```
这里你要记住一点,由于值对象只做整体替换、不可修改的特性,在值对象中基本不会有修改或新增的方法。
### 4. 领域服务
如果一个业务行为由多个实体对象参与完成,我们就将这部分业务逻辑放在领域服务中实现。领域服务与实体方法的区别是:实体方法完成单一实体自身的业务逻辑,是相对简单的原子业务逻辑,而领域服务则是多个实体组合出的相对复杂的业务逻辑。两者都在领域层,实现领域模型的核心业务能力。
一个聚合可以设计一个领域服务类,管理聚合内所有的领域服务。
请假聚合的领域服务类是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(&quot;leave does not exist&quot;);
}
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&lt;Leave&gt; queryLeaveInfosByApplicant(String applicantId) {
List&lt;LeavePO&gt; leavePOList = leaveRepositoryInterface.queryByApplicantId(applicantId);
return leavePOList.stream().map(leavePO -&gt; leaveFactory.getLeave(leavePO)).collect(Collectors.toList());
}
public List&lt;Leave&gt; queryLeaveInfosByApprover(String approverId) {
List&lt;LeavePO&gt; leavePOList = leaveRepositoryInterface.queryByApproverId(approverId);
return leavePOList.stream().map(leavePO -&gt; leaveFactory.getLeave(leavePO)).collect(Collectors.toList());
}
}
```
**领域服务开发时的注意事项:**
在领域服务或实体方法中,我们应尽量避免调用其它聚合的领域服务或引用其它聚合的实体或值对象,这种操作会增加聚合的耦合度。在微服务架构演进时,如果出现聚合拆分和重组,这种跨聚合的服务调用和对象引用,会变成跨微服务的操作,导致这种跨聚合的领域服务调用和对象引用失效,在聚合分拆时会增加你代码解耦和重构的工作量。
以下是一段不建议使用的代码。在这段代码里Approver是leave聚合的值对象它作为对象参数被传到person聚合的findNextApprover领域服务。如果在同一个微服务内这种方式是没有问题的。但在架构演进时如果person和leave两个聚合被分拆到不同的微服务中那么leave中的Approver对象以及它的getPersonId()和fromPersonPO方法在person聚合中就会失效这时你就需要进行代码重构了。
```
public class PersonDomainService {
public Approver findNextApprover(Approver currentApprover, int leaderMaxLevel) {
PersonPO leaderPO = personRepository.findLeaderByPersonId(currentApprover.getPersonId());
if (leaderPO.getRoleLevel() &gt; leaderMaxLevel) {
return null;
} else {
return Approver.fromPersonPO(leaderPO);
}
}
}
```
那正确的方式是什么样的呢在应用服务组合不同聚合的领域服务时我们可以通过ID或者参数来传数如单一参数currentApproverId。这样聚合之间就解耦了下面是修改后的代码它可以不依赖其它聚合的实体独立完成业务逻辑。
```
public class PersonDomainService {
public Person findNextApprover(String currentApproverId, int leaderMaxLevel) {
PersonPO leaderPO = personRepository.findLeaderByPersonId(currentApproverId);
if (leaderPO.getRoleLevel() &gt; leaderMaxLevel) {
return null;
} else {
return personFactory.createPerson(leaderPO);
}
}
}
```
## 领域事件
在创建请假单和请假审批过程中会产生领域事件。为了方便管理我们将聚合内的领域事件相关的代码放在聚合的event目录中。领域事件实体在聚合仓储内完成持久化但是事件实体的生命周期不受聚合根管理。
### 1. 领域事件基类DomainEvent
你可以建立统一的领域事件基类DomainEvent。基类包含事件ID、时间戳、事件源以及事件相关的业务数据。
```
public class DomainEvent {
String id;
Date timestamp;
String source;
String data;
}
```
### 2. 领域事件实体
请假领域事件实体LeaveEvent继承基类DomainEvent。可根据需要扩展属性和方法如leaveEventType。data字段中存储领域事件相关的业务数据可以是XML或Json等格式。
```
public class LeaveEvent extends DomainEvent {
LeaveEventType leaveEventType;
public static LeaveEvent create(LeaveEventType eventType, Leave leave){
LeaveEvent event = new LeaveEvent();
event.setId(IdGenerator.nextId());
event.setLeaveEventType(eventType);
event.setTimestamp(new Date());
event.setData(JSON.toJSONString(leave));
return event;
}
}
```
### 3. 领域事件的执行逻辑
一般来说,领域事件的执行逻辑如下:
第一步:执行业务逻辑,产生领域事件。
第二步:完成业务数据持久化。
```
leaveRepositoryInterface.save(leaveFactory.createLeavePO(leave));
```
第三步:完成事件数据持久化。
```
leaveRepositoryInterface.saveEvent(leaveFactory.createLeaveEventPO(event));
```
第四步:完成领域事件发布。
```
eventPublisher.publish(event);
```
以上领域事件处理逻辑代码详见LeaveDomainService中submitApproval领域服务里面有请假提交审批事件的完整处理逻辑。
### 4. 领域事件数据持久化
为了保证事件发布方与事件订阅方数据的最终一致性和数据审计,有些业务场景需要建立数据对账机制。数据对账主要通过对源端和目的端的持久化数据比对,从而发现异常数据并进一步处理,保证数据最终一致性。
对于需要对账的事件数据我们需设计领域事件对象的持久化对象PO完成领域事件数据的持久化如LeaveEvent事件实体的持久化对象LeaveEventPO。再通过聚合的仓储完成数据持久化
```
leaveRepositoryInterface.saveEvent(leaveFactory.createLeaveEventPO(event))。
```
事件数据持久化对象LeaveEventPO格式如下
```
public class LeaveEventPO {
@Id
@GenericGenerator(name = &quot;idGenerator&quot;, strategy = &quot;uuid&quot;)
@GeneratedValue(generator = &quot;idGenerator&quot;)
int id;
@Enumerated(EnumType.STRING)
LeaveEventType leaveEventType;
Date timestamp;
String source;
String data;
}
```
## 仓储模式
领域模型中DO实体的数据持久化是必不可少的DDD采用仓储模式实现数据持久化使得业务逻辑与基础资源逻辑解耦实现依赖倒置。持久化时先完成DO与PO对象的转换然后在仓储服务中完成PO对象的持久化。
### 1. DO与PO对象的转换
Leave聚合根的DO实体除了自身的属性外还会根据领域模型引用多个值对象如Applicant和Approver等它们包含多个属性personId、personName和personType等属性。
在持久化对象PO设计时你可以将这些值对象属性嵌入PO属性中或设计一个组合属性字段以Json串的方式存储在PO中。
以下是leave的DO的属性定义
```
public class Leave {
String id;
Applicant applicant;
Approver approver;
LeaveType type;
Status status;
Date startTime;
Date endTime;
long duration;
int leaderMaxLevel;
ApprovalInfo currentApprovalInfo;
List&lt;ApprovalInfo&gt; historyApprovalInfos;
}
public class Applicant {
String personId;
String personName;
String personType;
}
public class Approver {
String personId;
String personName;
int level;
}
```
为了减少数据库表数量以及表与表的复杂关联关系我们将leave实体和多个值对象放在一个LeavePO中。如果以属性嵌入的方式Applicant值对象在LeavePO中会展开为applicantId、applicantName和applicantType三个属性。
以下为采用属性嵌入方式的持久化对象LeavePO的结构。
```
public class LeavePO {
@Id
@GenericGenerator(name=&quot;idGenerator&quot;, strategy=&quot;uuid&quot;)
@GeneratedValue(generator=&quot;idGenerator&quot;)
String id;
String applicantId;
String applicantName;
@Enumerated(EnumType.STRING)
PersonType applicantType;
String approverId;
String approverName;
@Enumerated(EnumType.STRING)
LeaveType leaveType;
@Enumerated(EnumType.STRING)
Status status;
Date startTime;
Date endTime;
long duration;
@Transient
List&lt;ApprovalInfoPO&gt; historyApprovalInfoPOList;
}
```
### 2. 仓储模式
为了解耦业务逻辑和基础资源,我们可以在基础层和领域层之间增加一层仓储服务,实现依赖倒置。通过这一层可以实现业务逻辑和基础层资源的依赖分离。在变更基础层数据库的时候,你只要替换仓储实现就可以了,上层核心业务逻辑不会受基础资源变更的影响,从而实现依赖倒置。
一个聚合一个仓储,实现聚合数据的持久化。领域服务通过仓储接口来访问基础资源,由仓储实现完成数据持久化和初始化。仓储一般包含:仓储接口和仓储实现。
**2.1仓储接口**
仓储接口面向领域服务提供接口。
```
public interface LeaveRepositoryInterface {
void save(LeavePO leavePO);
void saveEvent(LeaveEventPO leaveEventPO);
LeavePO findById(String id);
List&lt;LeavePO&gt; queryByApplicantId(String applicantId);
List&lt;LeavePO&gt; queryByApproverId(String approverId);
}
```
**2.2仓储实现**
仓储实现完成数据持久化和数据库查询。
```
@Repository
public class LeaveRepositoryImpl implements LeaveRepositoryInterface {
@Autowired
LeaveDao leaveDao;
@Autowired
ApprovalInfoDao approvalInfoDao;
@Autowired
LeaveEventDao leaveEventDao;
public void save(LeavePO leavePO) {
leaveDao.save(leavePO);
approvalInfoDao.saveAll(leavePO.getHistoryApprovalInfoPOList());
}
public void saveEvent(LeaveEventPO leaveEventPO){
leaveEventDao.save(leaveEventPO);
}
@Override
public LeavePO findById(String id) {
return leaveDao.findById(id)
.orElseThrow(() -&gt; new RuntimeException(&quot;leave not found&quot;));
}
@Override
public List&lt;LeavePO&gt; queryByApplicantId(String applicantId) {
List&lt;LeavePO&gt; leavePOList = leaveDao.queryByApplicantId(applicantId);
leavePOList.stream()
.forEach(leavePO -&gt; {
List&lt;ApprovalInfoPO&gt; approvalInfoPOList = approvalInfoDao.queryByLeaveId(leavePO.getId());
leavePO.setHistoryApprovalInfoPOList(approvalInfoPOList);
});
return leavePOList;
}
@Override
public List&lt;LeavePO&gt; queryByApproverId(String approverId) {
List&lt;LeavePO&gt; leavePOList = leaveDao.queryByApproverId(approverId);
leavePOList.stream()
.forEach(leavePO -&gt; {
List&lt;ApprovalInfoPO&gt; approvalInfoPOList = approvalInfoDao.queryByLeaveId(leavePO.getId());
leavePO.setHistoryApprovalInfoPOList(approvalInfoPOList);
});
return leavePOList;
}
}
```
这里持久化组件采用了Jpa。
```
public interface LeaveDao extends JpaRepository&lt;LeavePO, String&gt; {
List&lt;LeavePO&gt; queryByApplicantId(String applicantId);
List&lt;LeavePO&gt; queryByApproverId(String approverId);
}
```
**2.3仓储执行逻辑**
以创建请假单为例,仓储的执行步骤如下。
第一步仓储执行之前将聚合内DO会转换为PO这种转换在工厂服务中完成
```
leaveFactory.createLeavePO(leave)。
```
第二步:完成对象转换后,领域服务调用仓储接口:
```
leaveRepositoryInterface.save。
```
第三步由仓储实现完成PO对象持久化。
代码执行步骤如下:
```
public void createLeave(Leave leave, int leaderMaxLevel, Approver approver) {
leave.setLeaderMaxLevel(leaderMaxLevel);
leave.setApprover(approver);
leave.create();
leaveRepositoryInterface.save(leaveFactory.createLeavePO(leave));
}
```
## 工厂模式
对于大型的复杂领域模型聚合内的聚合根、实体和值对象之间的依赖关系比较复杂这种过于复杂的依赖关系不适合通过根实体构造器来创建。为了协调这种复杂的领域对象的创建和生命周期管理在DDD里引入了工厂模式Factory在工厂里封装复杂的对象创建过程。
当聚合根被创建时,聚合内所有依赖的对象将会被同时创建。
工厂与仓储模式往往结对出现,应用于数据的初始化和持久化两类场景。
- DO对象的初始化获取持久化对象PO通过工厂一次构建出聚合根所有依赖的DO对象完数据初始化。
- DO的对象持久化将所有依赖的DO对象一次转换为PO对象完成数据持久化。
下面代码是leave聚合的工厂类LeaveFactory。其中createLeavePOleave方法组织leave聚合的DO对象和值对象完成leavePO对象的构建。getLeaveleave通过持久化对象PO构建聚合的DO对象和值对象完成leave聚合DO实体的初始化。
```
public class LeaveFactory {
public LeavePO createLeavePO(Leave leave) {
LeavePO leavePO = new LeavePO();
leavePO.setId(UUID.randomUUID().toString());
leavePO.setApplicantId(leave.getApplicant().getPersonId());
leavePO.setApplicantName(leave.getApplicant().getPersonName());
leavePO.setApproverId(leave.getApprover().getPersonId());
leavePO.setApproverName(leave.getApprover().getPersonName());
leavePO.setStartTime(leave.getStartTime());
leavePO.setStatus(leave.getStatus());
List&lt;ApprovalInfoPO&gt; historyApprovalInfoPOList = approvalInfoPOListFromDO(leave);
leavePO.setHistoryApprovalInfoPOList(historyApprovalInfoPOList);
return leavePO;
}
public Leave getLeave(LeavePO leavePO) {
Leave leave = new Leave();
Applicant applicant = Applicant.builder()
.personId(leavePO.getApplicantId())
.personName(leavePO.getApplicantName())
.build();
leave.setApplicant(applicant);
Approver approver = Approver.builder()
.personId(leavePO.getApproverId())
.personName(leavePO.getApproverName())
.build();
leave.setApprover(approver);
leave.setStartTime(leavePO.getStartTime());
leave.setStatus(leavePO.getStatus());
List&lt;ApprovalInfo&gt; approvalInfos = getApprovalInfos(leavePO.getHistoryApprovalInfoPOList());
leave.setHistoryApprovalInfos(approvalInfos);
return leave;
}
//其它方法
}
```
## 服务的组合与编排
应用层的应用服务完成领域服务的组合与编排。一个聚合的应用服务可以建立一个应用服务类管理聚合所有的应用服务。比如leave聚合有LeaveApplicationServiceperson聚合有PersonApplicationService。
在请假微服务中有三个聚合leave、person和rule。我们来看一下应用服务是如何跨聚合来进行服务的组合和编排的。以创建请假单createLeaveInfo应用服务为例分为这样三个步骤。
第一步根据请假单定义的人员类型、请假类型和请假时长从rule聚合中获取请假审批规则。这一步通过approvalRuleDomainService类的getLeaderMaxLevel领域服务来实现。
第二步根据请假审批规则从person聚合中获取请假审批人。这一步通过personDomainService类的findFirstApprover领域服务来实现。
第三步根据请假数据和从rule和person聚合获取的数据创建请假单。这一步通过leaveDomainService类的createLeave领域服务来实现。
由于领域核心逻辑已经很好地沉淀到了领域层中,领域层的这些核心逻辑可以高度复用。应用服务只需要灵活地组合和编排这些不同聚合的领域服务,就可以很容易地适配前端业务的变化。因此应用层不会积累太多的业务逻辑代码,所以会变得很薄,代码维护起来也会容易得多。
以下是leave聚合的应用服务类。代码是不是非常得少
```
public class LeaveApplicationService{
@Autowired
LeaveDomainService leaveDomainService;
@Autowired
PersonDomainService personDomainService;
@Autowired
ApprovalRuleDomainService approvalRuleDomainService;
public void createLeaveInfo(Leave leave){
//get approval leader max level by rule
int leaderMaxLevel = approvalRuleDomainService.getLeaderMaxLevel(leave.getApplicant().getPersonType(), leave.getType().toString(), leave.getDuration());
//find next approver
Person approver = personDomainService.findFirstApprover(leave.getApplicant().getPersonId(), leaderMaxLevel);
leaveDomainService.createLeave(leave, leaderMaxLevel, Approver.fromPerson(approver));
}
public void updateLeaveInfo(Leave leave){
leaveDomainService.updateLeaveInfo(leave);
}
public void submitApproval(Leave leave){
//find next approver
Person approver = personDomainService.findNextApprover(leave.getApprover().getPersonId(), leave.getLeaderMaxLevel());
leaveDomainService.submitApproval(leave, Approver.fromPerson(approver));
}
public Leave getLeaveInfo(String leaveId){
return leaveDomainService.getLeaveInfo(leaveId);
}
public List&lt;Leave&gt; queryLeaveInfosByApplicant(String applicantId){
return leaveDomainService.queryLeaveInfosByApplicant(applicantId);
}
public List&lt;Leave&gt; queryLeaveInfosByApprover(String approverId){
return leaveDomainService.queryLeaveInfosByApprover(approverId);
}
}
```
**应用服务开发注意事项:**
为了聚合解耦和微服务架构演进,应用服务在对不同聚合领域服务进行编排时,应避免不同聚合的实体对象,在不同聚合的领域服务中引用,这是因为一旦聚合拆分和重组,这些跨聚合的对象将会失效。
在LeaveApplicationService中leave实体和Applicant值对象分别作为参数被rule聚合和person聚合的领域服务引用这样会增加聚合的耦合度。下面是不推荐使用的代码。
```
public class LeaveApplicationService{
public void createLeaveInfo(Leave leave){
//get approval leader max level by rule
ApprovalRule rule = approvalRuleDomainService.getLeaveApprovalRule(leave);
int leaderMaxLevel = approvalRuleDomainService.getLeaderMaxLevel(rule);
leave.setLeaderMaxLevel(leaderMaxLevel);
//find next approver
Approver approver = personDomainService.findFirstApprover(leave.getApplicant(), leaderMaxLevel);
leave.setApprover(approver);
leaveDomainService.createLeave(leave);
}
}
```
那如何实现聚合的解耦呢我们可以将跨聚合调用时的对象传值调整为参数传值。一起来看一下调整后的代码getLeaderMaxLevel由leave对象传值调整为personTypeleaveType和duration参数传值。findFirstApprover中Applicant值对象调整为personId参数传值。
```
public class LeaveApplicationService{
public void createLeaveInfo(Leave leave){
//get approval leader max level by rule
int leaderMaxLevel = approvalRuleDomainService.getLeaderMaxLevel(leave.getApplicant().getPersonType(), leave.getType().toString(), leave.getDuration());
//find next approver
Person approver = personDomainService.findFirstApprover(leave.getApplicant().getPersonId(), leaderMaxLevel);
leaveDomainService.createLeave(leave, leaderMaxLevel, Approver.fromPerson(approver));
}
}
```
在微服务演进和聚合重组时,就不需要进行聚合解耦和代码重构了。
## 微服务聚合拆分时的代码演进
如果请假微服务未来需要演进为人员和请假两个微服务我们可以基于请假leave和人员person两个聚合来进行拆分。由于两个聚合已经完全解耦领域逻辑非常稳定在微服务聚合代码拆分时聚合领域层的代码基本不需要调整。调整主要集中在微服务的应用服务中。
我们以应用服务createLeaveInfo为例当一个微服务拆分为两个微服务时看看代码需要做什么样的调整
### 1. 微服务拆分前
createLeaveInfo应用服务的代码如下
```
public void createLeaveInfo(Leave leave){
//get approval leader max level by rule
int leaderMaxLevel = approvalRuleDomainService.getLeaderMaxLevel(leave.getApplicant().getPersonType(), leave.getType().toString(), leave.getDuration());
//find next approver
Person approver = personDomainService.findFirstApprover(leave.getApplicant().getPersonId(), leaderMaxLevel);
leaveDomainService.createLeave(leave, leaderMaxLevel, Approver.fromPerson(approver));
}
```
### 2. 微服务拆分后
leave和person两个聚合随微服务拆分后createLeaveInfo应用服务中下面的代码将会变成跨微服务调用。
```
Person approver = personDomainService.findFirstApprover(leave.getApplicant().getPersonId(), leaderMaxLevel);
```
由于跨微服务的调用是在应用层完成的我们只需要调整createLeaveInfo应用服务代码将原来微服务内的服务调用personDomainService.findFirstApprover修改为跨微服务的服务调用personFeignService. findFirstApprover。
同时新增ApproverAssembler组装器和PersonResponse的DTO对象以便将person微服务返回的person DTO对象转换为approver值对象。
```
// PersonResponse为调用微服务返回结果的封装
//通过personFeignService调用Person微服务用户接口层的findFirstApprover facade接口
PersonResponse approverResponse = personFeignService. findFirstApprover(leave.getApplicant().getPersonId(), leaderMaxLevel);
Approver approver = ApproverAssembler.toDO(approverResponse);
```
在原来的person聚合中由于findFirstApprover领域服务已经逐层封装为用户接口层的Facade接口所以person微服务不需要做任何代码调整只需将PersonApi的findFirstApprover Facade服务发布到API网关即可。
如果拆分前person聚合的findFirstApprover领域服务没有被封装为Facade接口我们只需要在person微服务中按照以下步骤调整即可。
第一步将person聚合PersonDomainService类中的领域服务findFirstApprover封装为应用服务findFirstApprover。
```
@Service
public class PersonApplicationService {
@Autowired
PersonDomainService personDomainService;
public Person findFirstApprover(String applicantId, int leaderMaxLevel) {
return personDomainService.findFirstApprover(applicantId, leaderMaxLevel);
}
}
```
第二步将应用服务封装为Facade服务并发布到API网关。
```
@RestController
@RequestMapping(&quot;/person&quot;)
@Slf4j
public class PersonApi {
@Autowired
@GetMapping(&quot;/findFirstApprover&quot;)
public Response findFirstApprover(@RequestParam String applicantId, @RequestParam int leaderMaxLevel) {
Person person = personApplicationService.findFirstApprover(applicantId, leaderMaxLevel);
return Response.ok(PersonAssembler.toDTO(person));
}
}
```
## 服务接口的提供
用户接口层是前端应用与微服务应用层的桥梁通过Facade接口封装应用服务适配前端并提供灵活的服务完成DO和DTO相互转换。
当应用服务接收到前端请求数据时组装器会将DTO转换为DO。当应用服务向前端返回数据时组装器会将DO转换为DTO。
### 1. facade接口
facade接口可以是一个门面接口实现类也可以是门面接口加一个门面接口实现类。项目可以根据前端的复杂度进行选择由于请假微服务前端功能相对简单我们就直接用一个门面接口实现类来实现就可以了。
```
public class LeaveApi {
@PostMapping
public Response createLeaveInfo(LeaveDTO leaveDTO){
Leave leave = LeaveAssembler.toDO(leaveDTO);
leaveApplicationService.createLeaveInfo(leave);
return Response.ok();
}
@PostMapping(&quot;/query/applicant/{applicantId}&quot;)
public Response queryByApplicant(@PathVariable String applicantId){
List&lt;Leave&gt; leaveList = leaveApplicationService.queryLeaveInfosByApplicant(applicantId);
List&lt;LeaveDTO&gt; leaveDTOList = leaveList.stream().map(leave -&gt; LeaveAssembler.toDTO(leave)).collect(Collectors.toList());
return Response.ok(leaveDTOList);
}
//其它方法
}
```
### 2. DTO数据组装
组装类Assembler负责将应用服务返回的多个DO对象组装为前端DTO对象或将前端请求的DTO对象转换为多个DO对象供应用服务作为参数使用。组装类中不应有业务逻辑主要负责格式转换、字段映射等。Assembler往往与DTO同时存在。LeaveAssembler完成请假DO和DTO数据相互转换。
```
public class LeaveAssembler {
public static LeaveDTO toDTO(Leave leave){
LeaveDTO dto = new LeaveDTO();
dto.setLeaveId(leave.getId());
dto.setLeaveType(leave.getType().toString());
dto.setStatus(leave.getStatus().toString());
dto.setStartTime(DateUtil.formatDateTime(leave.getStartTime()));
dto.setEndTime(DateUtil.formatDateTime(leave.getEndTime()));
dto.setCurrentApprovalInfoDTO(ApprovalInfoAssembler.toDTO(leave.getCurrentApprovalInfo()));
List&lt;ApprovalInfoDTO&gt; historyApprovalInfoDTOList = leave.getHistoryApprovalInfos()
.stream()
.map(historyApprovalInfo -&gt; ApprovalInfoAssembler.toDTO(leave.getCurrentApprovalInfo()))
.collect(Collectors.toList());
dto.setHistoryApprovalInfoDTOList(historyApprovalInfoDTOList);
dto.setDuration(leave.getDuration());
return dto;
}
public static Leave toDO(LeaveDTO dto){
Leave leave = new Leave();
leave.setId(dto.getLeaveId());
leave.setApplicant(ApplicantAssembler.toDO(dto.getApplicantDTO()));
leave.setApprover(ApproverAssembler.toDO(dto.getApproverDTO()));
leave.setCurrentApprovalInfo(ApprovalInfoAssembler.toDO(dto.getCurrentApprovalInfoDTO()));
List&lt;ApprovalInfo&gt; historyApprovalInfoDTOList = dto.getHistoryApprovalInfoDTOList()
.stream()
.map(historyApprovalInfoDTO -&gt; ApprovalInfoAssembler.toDO(historyApprovalInfoDTO))
.collect(Collectors.toList());
leave.setHistoryApprovalInfos(historyApprovalInfoDTOList);
return leave;
}
}
```
DTO类包括requestDTO和responseDTO两部分。
DTO应尽量根据前端展示数据的需求来定义避免过多地暴露后端业务逻辑。尤其对于多渠道场景可以根据渠道属性和要求为每个渠道前端应用定义个性化的DTO。由于请假微服务相对简单我们可以用leaveDTO代码做个示例。
```
@Data
public class LeaveDTO {
String leaveId;
ApplicantDTO applicantDTO;
ApproverDTO approverDTO;
String leaveType;
ApprovalInfoDTO currentApprovalInfoDTO;
List&lt;ApprovalInfoDTO&gt; historyApprovalInfoDTOList;
String startTime;
String endTime;
long duration;
String status;
}
```
## 总结
今天我们了解了用DDD开发出来的微服务代码到底是什么样的。你可以将这些核心设计思想逐步引入到项目中去慢慢充实自己的DDD知识体系。我还想再重点强调的是由于架构的演进微服务与生俱来就需要考虑聚合的未来重组。因此微服务的设计和开发要做到未雨绸缪而这最关键的就是解耦了。
**聚合与聚合的解耦:**当多个聚合在同一个微服务时,很多传统架构开发人员会下意识地引用其他聚合的实体和值对象,或者调用其它聚合的领域服务。因为这些聚合的代码在同一个微服务内,运行时不会有问题,开发效率似乎也更高,但这样会不自觉地增加聚合之间的耦合。在微服务架构演进时,如果聚合被分别拆分到不同的微服务中,原来微服务内的关系就会变成跨微服务的关系,原来微服务内的对象引用或服务调用将会失效。最终你还是免不了要花大量的精力去做聚合解耦。虽然前期领域建模和边界划分得很好,但可能会因为开发稍不注意,而导致解耦工作前功尽弃。
**微服务内各层的解耦:**微服务内有四层在应用层和领域层组成核心业务领域的两端有两个缓冲区或数据转换区。前端与应用层通过组装器实现DTO和DO的转换这种适配方式可以更容易地响应前端需求的变化隐藏核心业务逻辑的实现保证核心业务逻辑的稳定实现核心业务逻辑与前端应用的解耦。而领域层与基础层通过仓储和工厂模式实现DO和PO的转换实现应用逻辑与基础资源逻辑的解耦。
最后我想说DDD知识体系虽大但你可以根据企业的项目场景和成本要求逐步引入适合自己的DDD方法和技术建立适合自己的DDD开发模式和方法体系。
这一期的加餐到这就结束了,希望你能对照完整代码认真阅读今天的内容,有什么疑问,欢迎在留言区与我交流!

View File

@@ -0,0 +1,102 @@
<audio id="audio" title="抽奖《DDD实战课》沉淀成书了感谢有你" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/d9/b9/d9b8054e172892149aec7418e818f6b9.mp3"></audio>
你好,我是欧创新。好久不见!
专栏结课已经快一年了,累计有 **10000** 余人加入学习,非常感谢大家的支持。每次登录后台查看留言,依然能看到大家的分享,真的非常开心能和你一直保持联系、保持交流!
《DDD实战课》完结后我并没有停止在DDD方面的深度探索他一直延伸至我工作的方方面面。做了很多次的实践与思考和诸多技术大牛进行切磋加之专栏的铺垫和积累历时一年我完成了这本书**《中台架构与实现基于DDD和微服务》**,特来与你分享好消息。
<img src="https://static001.geekbang.org/resource/image/6f/16/6f6a29f043eb83yyd59f9463a2f25316.jpeg" alt="">
完整学习过专栏的同学对DDD的优势应该了如指掌了有所遗忘的同学也可以再去看看[开篇词](https://time.geekbang.org/column/article/149941)这里不再赘述。但话说回来DDD与微服务乃至中台设计的结合目前仍是一个非常新的领域。对于如何利用DDD完成中台和微服务的协同设计其实还有很多难题等待攻克。
想必你在购买这个专栏的时候就已经了解到DDD相关的资料在市面上的表现是怎样的一个状态。微服务开发和技术的学习资料非常多但在中台数字化转型过程中关于如何进行业务领域边界划分如何完成中台领域建模实现能力复用如何完成单体应用拆分和微服务设计如何实现前中后台的协同设计等等可参考和借鉴的资料并不是很多。即便有一些真正理解和实施起来也是困难重重。这也是我写书的源动力。
目前这本书已经正式出版,极客商城、京东、当当同步开售。由于新上架,**现在极客商城刚好是有限时优惠活动的,感兴趣的话可以通过该链接查看:**[http://gk.link/a/10mEQ](http://gk.link/a/10mEQ)
全书共计22万余字系统地阐述了基于DDD的中台和微服务建设的方法体系主要包括中台业务边界划分和领域模型构建微服务、微前端设计理念与实践以及如何进行前中后台的协同设计和单元化设计等内容。另外大家在《DDD实战课》中提出的宝贵问题我也在本书中做出了解答。再次感谢你与我共创内容
**今天我带了3本我的签名图书与大家分享我会在文末的留言区中抽取3位幸运同学赠书期待看到你的身影。**
下面我来简单介绍下这本书,喜欢纸质书的同学,也可以多做一点了解。先睹为快了!
## 本书大纲
共计24章分为6部分。
**第一部分认识中台第14章**
这部分包括4章主要介绍中台相关背景知识认识并理解中台的真正含义从业务中台、数据中台、技术中台以及与之匹配的组织架构等多个方面分析传统企业中台转型应该具备的能力带你初步了解DDD是如何指导中台和微服务设计并厘清它们的协作关系。
**第二部分DDD基本原理第511章**
为了让你能够更加深刻地理解DDD这部分通过一些浅显易懂的案例帮助你学习并深刻理解DDD的核心基础知识、设计思想、原则和方法等内容了解它们之间的协作和依赖关系解决DDD概念理解困难的问题做好中台实践前的准备工作。
这部分包括7章主要讲解DDD的关键核心知识体系包括领域、子域、核心子域、通用子域、支撑子域、限界上下文、实体、值对象、聚合、聚合根、领域事件和DDD分层架构等知识。
**第三部分中台领域建模与微服务设计第1219章**
这部分包括8章主要介绍DDD是如何通过战略设计构建中台业务模型以及如何通过战术设计指导微服务拆分和设计的。在这一部分我会用多个实际案例带你用DDD方法完成中台和微服务的全流程设计深刻理解DDD在中台领域建模与微服务设计中的步骤、方法、设计思想和价值。
- 了解如何用事件风暴方法构建领域模型;
- 了解如何用DDD设计思想构建企业级可复用的中台业务模型
- 了解如何用DDD设计微服务代码模型如何将领域模型映射到微服务以建立领域模型与微服务代码模型的映射关系如何完成微服务架构演进等。
最后用一个案例将DDD所有知识点串联在一起带你深入了解如何用DDD的设计方法完成领域建模与微服务设计的全流程并对代码示例进行了详细分析和讲解。
**第四部分前端设计第20章和第21章**
这部分包括2章主要介绍微前端的设计思想通过前端微服务化和单元化的设计思想解决业务中台建设完成后前端应用解耦和前后端服务集成复杂的难点。书中阐述了如何借鉴微服务的设计思想来解构前端应用实现前端应用的拆分解耦并结合实践介绍前端架构的转型策略与技术落地。
另外,这部分还探讨了基于领域模型的单元化设计方法。通过微服务与微前端组合后的单元化设计,既可以降低企业级前台应用集成的复杂度,又可以让企业具有更强的产品快速发布和业务响应能力。这种能力能给我们的团队组建、研发模式、业务能力发布等带来非常大的价值。
**第五部分中台设计案例第22章**
这部分包括1章通过保险订单化设计案例采用自顶向下的领域建模策略带你走一遍中台设计的完整流程。案例中涵盖业务领域分解、中台领域建模、微服务和微前端设计、单元化设计以及如何实现业务和数据融合等内容希望能够帮助你加深对DDD、中台、微服务和微前端等知识体系、设计思想和技术体系的全面理解更好地投入DDD、中台和微服务建设实践中。
**第六部分总结第23章和第24章**
这部分是全书的总结包括2章。书中结合我多年的设计经验和思考带你了解单体应用向微服务架构的演进策略如何避免陷入DDD设计的常见误区微服务设计原则以及分布式架构下的关键设计等内容。
## 本书阅读对象
本书是一本关于中台、微服务和微前端设计与建设的书采用了DDD设计思想和方法适合的阅读对象主要分为下面几类
1. 从事企业数字化转型的企业管理者;
1. 从事企业技术架构和微服务设计的架构师;
1. 从事企业业务架构设计和业务建模的业务人员;
1. 从事微服务设计和开发的高级技术人员;
1. 希望从事中台和微服务架构设计的人员;
1. 对DDD、微服务和中台设计感兴趣的学习者。
最后我想说DDD不是新事物它2003年诞生于集中式架构盛行的年代。**技术常变,思想则永恒!**在充满不确定的时代,凡事预则立,不预则废。心中有边界,脚下有乾坤,分而治之,方能轻松应对业务和技术的快速发展和变化。
祝你在工作中一切顺利。更多关于专栏和图书的问题都可以在留言区中与我交流。如有任何疑问、技术交流需求或建议以及纰漏之处也可直接发送至我的邮箱chuangxinou@163.com期待你的来信
>
<p>**推荐语1知行合一**<br>
&nbsp;<br>
右军<br>
支付宝专家 《深入分布式缓存》《程序员的三门课》联合作者<br>
&nbsp;<br>
个人对DDD一直比较有兴趣也包括企业架构设计、在DDD之前的领域分析如分析模式、彩色建模等。如果把软件按照相对的“稳定性”来排序领域层应用层界面层。以营销为例撬动用户的还是老三样卡、券、积分本质就是营销资产+资金流而从产品包装上可以策划满减、满返、2件折扣、限时优惠、限定电商全场消费、限定活动线下商超、限定品类等活动不一而足。领域层是相对稳定的应用层业务逻辑层和具体规则可以有多种变化而广义界面层的实质包括产品包装、交互等可以有更多的互动玩法。窃以为领域分析的价值所在就是寻求“千变万化”中相对的“稳定性、第一性”然后通过合理的架构分层及抽象隔离的业务复杂度和技术复杂度隔离业务领域的稳定性和易变性从架构上精巧、快速地支持业务的变化。技术为业务服务但绝不是业务到IT的简单翻译。<br>
&nbsp;<br>
欧老师精于保险业务对于DDD也有自己的理解和看法。从经典的DDD战略设计到基于微服务的战术设计/实现的案例本书给出了全面的参考案例。知行合一则“限界上下文”“实体”“值对象”“聚合”“事件”“事务一致性”等都不再神秘。本书也有一些可喜的创见如对于“微前端”和“业务单元化”的提炼。本书以保险订单化销售业务领域为例采用自顶向下策略完成保险部分业务领域的中台设计带领读者了解中台设计全流程理解DDD、业务中台、微服务、微前端与单元化设计的关系以及它们的核心设计思想。<br>
&nbsp;<br>
本书价值不菲强烈推荐。无论对于DDD的初学者还是DDD的资深人士都有相应的启发。写作者的最大安慰莫过于读者觉得有价值有收获。祝大家阅读愉快<br>
&nbsp;<br>
**推荐语2为不确定而架构**<br>
&nbsp;<br>
王威<br>
ThoughtWorks中国区技术战略咨询服务负责人<br>
&nbsp;<br>
在过去的几年中因为工作的关系我同很多科技类企业和组织合作过。这些企业和组织分布在不同的行业和地区从电信、金融到物流供应链从国内到全球各地。几乎所有技术行业的同仁在谈到未来的时候都流露出了很强的改变意愿和紧迫感。例如今年出现的新冠肺炎疫情以及围绕疫情在全球范围出现的一系列连锁反应都导致大家逐渐形成了一个共识世界已经从根本上改变未来20年将要发生的事情可能是我们今天根本无法想象的。在这样的背景下每一个组织都希望能够通过加大科技的投入赋能自己的客户和业务从而做好应对未知挑战的准备。<br>
&nbsp;<br>
另一方面软件“侵蚀”世界已经是不争的事实在国内的很多城市中恐怕已经很难想象完全脱离软件的生活会是什么样子。即使我们不谈“不可见”的嵌入式软件和网络控制类软件仅仅脱离了智能手机以及建于其上的各种App我们熟悉的生活似乎将无法运转下去。新兴的科技公司在利用软件技术打造新的场景培养用户的使用习惯创造新的业务价值的同时也在倒逼前辈们对传统的业务进行数字化改造以适应新时代下技术的变化速度。同时科技公司又将自己的最佳实践标准化、产品化希望通过与传统企业的合作加速整个行业变革的进程。20年前的SOA架构、6年前的微服务架构和3年前阿里的“中台”都是这种模式的很好代表。<br>
&nbsp;<br>
客户习惯的改变技术的发展和快速演进以及在一些行业出现的外力作用都带来了价值、场景、技术、政策的不确定性。所有不确定性的综合使得软件的构建过程一定会面对这样的窘境软件永远跟不上业务变化。为了解决这样的问题业界的前辈们一直在通过管理、技术、工具平台等多种维度来解决同一个问题如何使软件的构建具备更高的响应力。敏捷、精益、DevOps、效能平台都是为解决这个问题而出现的解决方案。在这个过程中“如何在复杂业务场景下设计软件”逐渐成为架构师们关注的焦点。领域驱动设计以下简称DDD的提出恰恰解决了这一问题。但是在2010年之前因为单体应用仍然占据主流地位DDD“曲高和寡”。直到“微服务”的出现才消除了原来单体应用的桎梏使得DDD成为架构师们都在讨论的软件架构设计标准实践。<br>
&nbsp;<br>
近年来DDD在国内的影响力逐年增大。我仍然记得在2015年前后和企业交流的时候当时大家对于什么是DDD完全摸不着头脑很多组织直接把源自“产品线工程”的“领域工程”和DDD作为相同的概念加以实践。2017年我们举办了第一届DDD中国峰会那时有很多参与的同行对于DDD如何在自己的组织、场景中落地还存有这样或那样的疑虑。而到2019年的第三届峰会时大家更多是带着问题和经验来和业界的同行们一起交流心得探索在新场景下如何利用DDD带来更多的价值。<br>
&nbsp;<br>
我和欧创新老师正是在这样的背景下认识的。欧老师在过去几年中将DDD的思想、微服务以及中台的理念同自己企业的实际相结合积累了丰富的实践经验。每一次和欧老师交流我都能学到很多东西。当欧老师找到我为这本书作序的时候我既受宠若惊又诚惶诚恐。在拜读完本书后我惊讶于在这么短的时间内欧老师不仅将自己获得的经验提炼总结还用通俗易懂的语言和丰富的案例将DDD、微服务、中台的概念和围绕在它们周围的实践讲述得如此详细。本书确实是业界难得的一个针对架构设计和中台转型的技术层面的总结我个人从中获益匪浅相信本书的读者朋友会和我有同样的体会。</p>