极客时间已完结课程限时免费阅读

基于DDD的微服务设计实例代码详解

基于DDD的微服务设计实例代码详解-极客时间

基于DDD的微服务设计实例代码详解

讲述:欧创新

时长26:01大小23.76M

你好,我是欧创新。好久不见,今天我带着你期待的案例来了。
还记得我们在 [第 18 讲] 中用事件风暴完成的“在线请假考勤”项目的领域建模和微服务设计吗?今天我们就在这个项目的基础上看看,用 DDD 方法设计和开发出来的微服务代码到底是什么样的?点击 Github 获取完整代码,接下来的内容是我对代码的一个详解,期待能帮助你更好地实践我们这个专栏所学到的知识。

项目回顾

“在线请假考勤”项目中,请假的核心业务流程是:请假人填写请假单提交审批;根据请假人身份、请假类型和请假天数进行校验并确定审批规则;根据审批规则确定审批人,逐级提交上级审批,逐级核批通过则完成审批,否则审批不通过则退回申请人。
[第 18 讲] 的微服务设计中,我们已经拆分出了两个微服务:请假和考勤微服务。今天我们就围绕“请假微服务”来进行代码详解。微服务采用的开发语言和数据库分别是:Java、Spring boot 和 PostgreSQL。

请假微服务采用的 DDD 设计思想

请假微服务中用到了很多的 DDD 设计思想和方法,主要包括以下几个:

聚合中的对象

请假微服务包含请假(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<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;
}
//其它方法
}

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("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());
}
}
领域服务开发时的注意事项:
在领域服务或实体方法中,我们应尽量避免调用其它聚合的领域服务或引用其它聚合的实体或值对象,这种操作会增加聚合的耦合度。在微服务架构演进时,如果出现聚合拆分和重组,这种跨聚合的服务调用和对象引用,会变成跨微服务的操作,导致这种跨聚合的领域服务调用和对象引用失效,在聚合分拆时会增加你代码解耦和重构的工作量。
以下是一段不建议使用的代码。在这段代码里 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() > 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() > 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 = "idGenerator", strategy = "uuid")
@GeneratedValue(generator = "idGenerator")
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<ApprovalInfo> 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="idGenerator", strategy="uuid")
@GeneratedValue(generator="idGenerator")
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<ApprovalInfoPO> historyApprovalInfoPOList;
}

2. 仓储模式

为了解耦业务逻辑和基础资源,我们可以在基础层和领域层之间增加一层仓储服务,实现依赖倒置。通过这一层可以实现业务逻辑和基础层资源的依赖分离。在变更基础层数据库的时候,你只要替换仓储实现就可以了,上层核心业务逻辑不会受基础资源变更的影响,从而实现依赖倒置。
一个聚合一个仓储,实现聚合数据的持久化。领域服务通过仓储接口来访问基础资源,由仓储实现完成数据持久化和初始化。仓储一般包含:仓储接口和仓储实现。
2.1 仓储接口
仓储接口面向领域服务提供接口。
public interface LeaveRepositoryInterface {
void save(LeavePO leavePO);
void saveEvent(LeaveEventPO leaveEventPO);
LeavePO findById(String id);
List<LeavePO> queryByApplicantId(String applicantId);
List<LeavePO> 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(() -> new RuntimeException("leave not found"));
}
@Override
public List<LeavePO> queryByApplicantId(String applicantId) {
List<LeavePO> leavePOList = leaveDao.queryByApplicantId(applicantId);
leavePOList.stream()
.forEach(leavePO -> {
List<ApprovalInfoPO> approvalInfoPOList = approvalInfoDao.queryByLeaveId(leavePO.getId());
leavePO.setHistoryApprovalInfoPOList(approvalInfoPOList);
});
return leavePOList;
}
@Override
public List<LeavePO> queryByApproverId(String approverId) {
List<LeavePO> leavePOList = leaveDao.queryByApproverId(approverId);
leavePOList.stream()
.forEach(leavePO -> {
List<ApprovalInfoPO> approvalInfoPOList = approvalInfoDao.queryByLeaveId(leavePO.getId());
leavePO.setHistoryApprovalInfoPOList(approvalInfoPOList);
});
return leavePOList;
}
}
这里持久化组件采用了 Jpa。
public interface LeaveDao extends JpaRepository<LeavePO, String> {
List<LeavePO> queryByApplicantId(String applicantId);
List<LeavePO> 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。其中 createLeavePO(leave)方法组织 leave 聚合的 DO 对象和值对象完成 leavePO 对象的构建。getLeave(leave)通过持久化对象 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<ApprovalInfoPO> 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<ApprovalInfo> approvalInfos = getApprovalInfos(leavePO.getHistoryApprovalInfoPOList());
leave.setHistoryApprovalInfos(approvalInfos);
return leave;
}
//其它方法
}

服务的组合与编排

应用层的应用服务完成领域服务的组合与编排。一个聚合的应用服务可以建立一个应用服务类,管理聚合所有的应用服务。比如 leave 聚合有 LeaveApplicationService,person 聚合有 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<Leave> queryLeaveInfosByApplicant(String applicantId){
return leaveDomainService.queryLeaveInfosByApplicant(applicantId);
}
public List<Leave> 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 对象传值调整为 personType,leaveType 和 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("/person")
@Slf4j
public class PersonApi {
@Autowired
@GetMapping("/findFirstApprover")
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("/query/applicant/{applicantId}")
public Response queryByApplicant(@PathVariable String applicantId){
List<Leave> leaveList = leaveApplicationService.queryLeaveInfosByApplicant(applicantId);
List<LeaveDTO> leaveDTOList = leaveList.stream().map(leave -> 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<ApprovalInfoDTO> historyApprovalInfoDTOList = leave.getHistoryApprovalInfos()
.stream()
.map(historyApprovalInfo -> 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<ApprovalInfo> historyApprovalInfoDTOList = dto.getHistoryApprovalInfoDTOList()
.stream()
.map(historyApprovalInfoDTO -> 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<ApprovalInfoDTO> historyApprovalInfoDTOList;
String startTime;
String endTime;
long duration;
String status;
}

总结

今天我们了解了用 DDD 开发出来的微服务代码到底是什么样的。你可以将这些核心设计思想逐步引入到项目中去,慢慢充实自己的 DDD 知识体系。我还想再重点强调的是:由于架构的演进,微服务与生俱来就需要考虑聚合的未来重组。因此微服务的设计和开发要做到未雨绸缪,而这最关键的就是解耦了。
聚合与聚合的解耦:当多个聚合在同一个微服务时,很多传统架构开发人员会下意识地引用其他聚合的实体和值对象,或者调用其它聚合的领域服务。因为这些聚合的代码在同一个微服务内,运行时不会有问题,开发效率似乎也更高,但这样会不自觉地增加聚合之间的耦合。在微服务架构演进时,如果聚合被分别拆分到不同的微服务中,原来微服务内的关系就会变成跨微服务的关系,原来微服务内的对象引用或服务调用将会失效。最终你还是免不了要花大量的精力去做聚合解耦。虽然前期领域建模和边界划分得很好,但可能会因为开发稍不注意,而导致解耦工作前功尽弃。
微服务内各层的解耦:微服务内有四层,在应用层和领域层组成核心业务领域的两端,有两个缓冲区或数据转换区。前端与应用层通过组装器实现 DTO 和 DO 的转换,这种适配方式可以更容易地响应前端需求的变化,隐藏核心业务逻辑的实现,保证核心业务逻辑的稳定,实现核心业务逻辑与前端应用的解耦。而领域层与基础层通过仓储和工厂模式实现 DO 和 PO 的转换,实现应用逻辑与基础资源逻辑的解耦。
最后我想说,DDD 知识体系虽大,但你可以根据企业的项目场景和成本要求,逐步引入适合自己的 DDD 方法和技术,建立适合自己的 DDD 开发模式和方法体系。
这一期的加餐到这就结束了,希望你能对照完整代码认真阅读今天的内容,有什么疑问,欢迎在留言区与我交流!
分享给需要的人,Ta购买本课程,你将得18
生成海报并分享

赞 43

提建议

上一篇
结束语 | 所谓高手,就是跨过坑和大海!
下一篇
抽奖|《DDD实战课》沉淀成书了,感谢有你!
 写留言

精选留言(138)

  • 冬青
    置顶
    2020-01-02
    偶是编辑,这篇加餐比较长~作者会抽周末的时间把音频讲解为大家补上。感谢等待!
    共 1 条评论
    28
  • 常文龙
    2021-07-11
    看了很多同道中人提了类似的疑问,分享一下自己的灵魂问题的回答: Q1.一个逻辑,貌似放在应用层、领域服务层、甚至聚合根本身都可以,到底什么是准绳? A1. (1)首先,分层的本质是为了复用和内聚,切不可成为作茧自缚的存在。 (2)其次,其实每层的定位,老师也说了,应用层是做跨聚合操作的,领域服务层是做聚合内的跨实体操作,而聚合根是自身能触达的对象的。 其实后两者有点含糊,因为聚合根理论上是聚合内四通八达的对象,但领域服务也是。从大家的共识看来,大家都更喜欢纯getter/setter的“容器模型”,而不是一个有“行为”的对象。这点其实老师在案例和回复也默认了,觉得很简单的才放进聚合根(案例里聚合根也就组合了几个setter,搞搞当前时间而已)。 (3)那么,假如觉得放在哪都行,说明他很可能根本就是一个聚合根的“DB基操”,譬如查询、更新请假单这两个操作,压根就是查库、写库。这时候自然是越底层越好(因为越底层将来就越有复用的可能),因此就放在领域服务层了。 Q2:文中一直提到,一旦拆分,引用会失效。 A2: (1)可能是因为我一开始就接触了dubbo之类的rpc框架,我没觉得有什么失效的,毕竟对于dubbo而言,不用考虑是否走网络,将来拆分,大不了为领域对象做个jar包就完事了。 (2)但按照老师说的,的确有一个好处,就是面向抽象,对于调用方来说,不用new大对象了,也不用担心自己set少了东西会导致问题。说白了,就是把关键的信息传过来得了。 Q3:仓储到底解决了什么问题?比起DAO有什么区别? A3: (1)首先,从老师的案例看来,仓储是依赖了DAO的。而且可以看出,仓储最大的特点,是一个add方法背后偷偷处理了N个表,而不是一个纯纯的小表。 (2)那么更深的灵魂问题来了,从仓储的定位是抽象存储,为何DO转PO这件事会发生在领域服务层(案例里,领域服务调用的factory方法)?按我的理解,至少仓储应该接受、返回DO才对。这点我目前不能理解,还是希望得到老师的回答。 (3)其实大部分互联网公司,尤其java栈都不存在换数据库的担忧,至少SQL大部分时候是标准的、JDBC也是抽象了数据库驱动的。因此,DAO这种读写库的模式已经很够用了。 Q4:DDD到底给我什么意义,解决了目前什么困境? A4:目前来看,DDD有两个深刻的意义,是我过去没领会到的。 (1)提醒了我,世界上有 领域驱动 vs. 数据驱动 两种套路(不说我都没意识到,这自上而下,自下而上的结果可能大不相同) (2)给了我一个划分“领域、聚合”两种范围,“实体(聚合根)、值对象、事件、命令”这个世界观,以及对应的需求分析方法论。 Q5:值对象的价值在哪? A5: (1)值对象本是Martin发现基于引用的编程语言在相等比较、写操作的时候互相干扰的问题而特地发明的概念,要求大家不能在值对象做局部写,而只能整个替换。然而实际上大家往往会通过克隆对象避免误写,通过简单的重写equal解决比较,犯不着新增概念。 (2)但我觉的值对象目前有一个更实在好处,就是当两个割裂的模型发生关联的时候,可以作为“快照”或者“实例”的表达。譬如案例中的人员在请假聚合里,表现为“请假人”、“审批者”,这时候只留下人员的名称和id,其他不留。这也意味着,这个人员改名了,在请假单里也是旧的名字(希望这是真实需求吧)。
    展开
    共 1 条评论
    25
  • Din
    2020-11-20
    老师,您好!请教一个关于repository使用的问题 在DDD的原则里,repository操作的都是聚合根,repository的作用就是把内存中的聚合根持久化,或者把持久化的数据还原为内存中的聚合根。repository中一般也只有getById,save,remove几个方法。 例如取消订单的场景,我其实只需要更新order的状态等少数几个字段,但是如果调用repository的save方法,就会把订单其他字段以及订单明细数据都更新一次,这样就会造成性能影响,以及数据冲突的问题。 针对这个问题,我想到两种解决方案: 1. 在repository增加只更新部分字段的方法,例如只更新订单状态和取消时间 saveOrderCancelInfo(),但这样会对repository有一定的污染,并且感觉saveOrderCancelInfo掺杂了业务逻辑 2. 在repository的save方法中,通过技术手段,找出聚合根对象被修改的数据,然后只对这些数据字段做更改。 老师,您有什么建议呢?
    展开

    作者回复: 你这个问题很好!记得去年thoughtworks的梅雪松老师提过一个方案,你可以通过以下链接看一下https://zhuanlan.zhihu.com/p/87074950。 方案的核心是Aggregate<T>容器,T是聚合根的类型。Repository以Aggregate<T>为核心,当Repository查询或保存聚合时,返回的不是聚合本身,而是聚合容器Aggregate<T>。Aggregate<T>保留了聚合的历史快照,因此在Repository保存聚合时,就可以与快照进行对比,找到需要修改的实体和字段,然后完成持久化工作。

    共 5 条评论
    17
  • 杨杰
    2020-03-27
    欧老师,有个关于充血模型的问题跟您探讨一下。 我研究DDD也有一段时间了,在某几个项目里面也推动团队采用DDD的设计思想,实体采用了充血模型(entity和po分开),在项目真正运行的过程中发现了几个问题: 1、由于我们的项目规模比较大,数据结构比较复杂,变动也比较频繁。每次有数据结构调整的时候改动的工作量比较大,导致团队成员比较抵触。 2、实体是充血模型的话,可以看成实体本身是有状态的。但是在一些逻辑比较复杂的场景下感觉操作起来会有点儿复杂。 最终实际的结果就是,整个团队这个充血模型用的有点儿不伦不类了。我的想法是这样的:按照DDD的设计思想,我个人觉得关键点是领域的边界,至于要不要用充血模型感觉不是那么重要(尤其是在团队整体的思想和能力达不到这么高的要求下),不知道您在实际的工作中是怎么平衡这个的。
    展开

    作者回复: 如果实在团队不好处理,那你先以聚合为单位划好边界,在聚合内可以抛弃聚合根之类的这些概念,聚合内都是领域服务,毕竟微服务的演进是以聚合为单位演进的,聚合内高内聚,聚合之间松耦合。其它的DDD的一些设计要求尽量要遵循。

    9
  • CN....
    2020-01-06
    老师好,浏览代码有两点疑惑 1,我们通常会认为加了事务注解就尽量避免除数据库外的其他调用,但是代码中在领域服务中的方法中发送mq,而且是在有事务注解的方法中,这里是基于什么考虑 2,消费mq的逻辑应该属于那一层 谢谢

    作者回复: 1、这个主要是考虑业务数据,事件数据持久化和发送消息队列同时能够成功,避免出现数据不一致的情况。当然也可以只在业务数据和事件数据持久化增加事务,如果消息队列发送不成功,还可以从事件表中获取数据再次发送。 2、消息订阅方一般在应用层监听和接受事件数据。

    共 3 条评论
    7
  • 盲僧
    2020-01-03
    太棒了,这个案例太精彩

    作者回复: 😄

    6
  • 淮小麦
    2021-03-20
    老师你好,实战中发现一个问题,实际业务一个采购订单,里面采购产品明细可能有几百条,目前设计是采购订单为聚合根,产品明细为聚合内的一个实体,产品明细列表被订单实体引用,现在需要删除一个订单中的一条采购产品明细,需要把订单和几百条明细查询出来,重建订单实体,然后遍历产品明细列表删除对应明细,再保存数据库?这样就会多了很多不必要的查询,也重建了很多不必要的实体对象,这种情况要如何优雅解决呢?
    共 1 条评论
    5
  • xxx
    2021-07-19
    最近看了很多DDD的资料,老师这个代码还是靠谱的👍
    4
  • 川川
    2020-08-28
    老师你好 我看你在文章有个疑惑的点,我看你在文章里面提到“应避免不同聚合的实体对象,在不同聚合的领域服务中引用,这是因为一旦聚合拆分和重组,这些跨聚合的对象将会失效” 但是我看Approver实体的fromPerson方法就是用person聚合的尸体作为参数传递,这个是不是有违背原则呢。

    作者回复: 你说的这种情况我当时考虑到了,也确实跟原则有冲突。 从代码完整性来讲,如果person聚合和leave聚合被拆分在不同的微服务中,那么从person聚合返回的数据应该是DTO类型的。在leave微服务中我们需要将这个person微服务返回的DTO的数据转换成DO对象,其实在转换时,我们是可以直接将person DTO组装成Approver这个DO对象的,这个转换的过程稍微有点复杂。由于当前person聚合和leave聚合是在同一个微服务中,为了避免带来误解,所以在代码里面并没有体现这种解耦处理方式。

    4
  • 涛涛
    2020-06-29
    老师您好,有两个疑问? 1.applicationService,domianService并没有实现接口,是故意这样设计的吗? 2.订单父单和子单设计成一个聚合好,还是2个聚合好?

    作者回复: 1、在应用层和领域层之间业务逻辑和依赖相对固定,为了避免开发的复杂度,因此没有采用面向接口的编程方式。但是面向前端和基础资源时,由于外部变化相对较大,为了适配和解耦,因此采用了面向接口的方式。 2、你说的父单和子单,是不是指订单和订单明细?一般来说订单和订单明细是在一个聚合里面的。

    共 2 条评论
    4
  • 阿玛铭
    2020-01-02
    欧老师的回马枪猝不及防

    作者回复: 😄,舍不得跟大家告别。

    4
  • Jupiter
    2020-09-12
    受益匪浅啊,感谢欧老师的课,理论和实践并存,而且值得多刷几遍去深刻理解DDD的思想。我现在的项目中能感觉有一点DDD的影子,但是我打算在我Master的作业上用一下DDD去构建一个推荐系统应用,可能会因为用DDD而用DDD,但是因为是课程设计,所以想多实践一下。有一个小问题是关于DDD里面的对象的,在前面的课程中,您提到有VO, 我现在在开发的时候 前端传给后端的对象 我使用DTO, 但是后端返回给前端的对象,我直接VO,没有中间DTO转化成VO的操作,请问这样也是可以的吧?谢谢老师。期待老师还有新的专栏分享。
    展开

    作者回复: VO实际上是前端应用的对象,跟后端微服务关系不大,如果DTO与VO之间是一对一关系的话,DTO实际上就是VO的一种数据形式,就不需要再进行转换了。 其实引入PO、DO、DTO、VO这几个对象主要是为了各层解耦,保证领域模型和逻辑的稳定。如果各层对象是一一对应的话,我们没必要增加转换的过程,毕竟对象之间的转换会影响应用性能。

    3
  • 史月Shi Yue
    2020-06-29
    老师,LeaveApplicationService在依赖LeaveDomainService的时候,依赖的应该是接口吧,看代码里注入的是类,这样就不是面向接口编程了啊

    作者回复: 为了避免开发复杂度,在应用层和领域层之间没有采用面向接口编程的方式。 面向接口编程主要用在了用户接口层和基础层,主要是为了前端应用和基础资源的解耦,也是为了核心业务逻辑与外部的适配。

    3
  • Geek_778d19
    2020-05-06
    聚合根与领域服务在职责上有些重叠了,在实现的时候如何选择?

    作者回复: 理论上,聚合根方法和领域服务都可以组合多个实体对象完成复杂的领域逻辑。但为了避免聚合根的业务逻辑过于复杂,避免聚合根类代码量过于庞大,我个人建议聚合根除了承担它的聚合管理职能外,只作为实体实现与聚合根自身行为相关的业务逻辑。而将跨多个实体的复杂领域逻辑放在领域服务中实现。简单聚合的跨实体领域逻辑,可以考虑在聚合根方法中实现。

    3
  • 卓坤鉴
    2021-04-08
    老师您好,我一直对于您文中对于基础层的依赖倒置不太理解,您的demo中的基础层的util、client、common作为基础设施,一直都需要被其它层(应用层、领域层、接口层)所直接调用的。请问这里是怎么样将基础层与其他层解耦的,怎么做到依赖倒置的
    2
  • Md3zed
    2021-02-08
    这样搞的太复杂了,感觉就是把简单的事情复杂化了。 DDD核心就是那几个概念的理解,微服务不一定要DDD才行,DDD只是帮助我们做领域划分,避免业务的变化导致服务的不稳定;DDD是想解决ORM的CRUD的问题,避免干尸式的“贫血”模型,它本质是一种面向对象分析和设计方法,它把业务模型转换为对象模型,通过业务模型绑定系统模型来控制业务变化带来的复杂度从而保持系统的稳定性、可扩展性、可维护性。而示例代码在这方面感觉完全为分层而分层,为DDD而DDD,可维护性,可理解性都比较差。
    展开

    作者回复: 是的,你说的没错,DDD是一种设计方法,不要为了DDD而DDD,我前面章节里面也多次强调。这个案例相对简单,直接用CRUD可能更方便。为什么显得复杂?我主要是想在这个案例里面尽量把DDD的一些设计思想体现出来。具体到实际项目落地的时候,你可以根据具体场景选择合适的方法来完成设计,最终以相对较小的代价解决实际问题为宜。

    共 2 条评论
    2
  • 波锅
    2020-11-27
    怎么跟前面写的不一样,实体不应该是充血模型么?在实体中完成存储操作。还有事件不应该在应用层么?

    作者回复: 有一部分实体采用了充血模型的,但是有些实体本身业务逻辑简单,没有太多复杂的逻辑,所有就不会有复杂的方法,所以看着以为是贫血模型。 事件发布可以放在应用层也可以在领域层实现,比较复杂的领域事件发布逻辑建议构建事件领域服务,然后在应用服务中与业务相关的领域服务组合。 简单的事件发布逻辑直接在领域服务中直接完成发布也是可以的。

    2
  • 睡吧,娃
    2020-07-09
    老师好,我有几个问题麻烦帮忙解答一下 1、实体里实现具体业务,假如需要数据持久化或从基础层获取数据,我是否可以在实体里面调用仓储。或者说这类情况写到领域服务里面去减轻实体或者聚合根的业务的负担。 2、假如获取商品列表领域服务 从仓储会获取po的集合列表, 是否需要将集合里每个po转换成聚合跟对应的Do,然后DO的集合再转换成DTO 如果是的话,这样子会造成一定的开销,但是如果不这样 分层就会被破坏 谢谢
    展开

    作者回复: 1、一般数据的持久化或者初始化都会在聚合的工厂和仓储组合来完成,它可以通过领域服务或者和聚合根的方法来实现,一般建议在领域服务中完成。 2、是这样的,所以在设计的时候要权衡,尽量降低转换带来的开销。如果复杂查询可以不经过领域层,直接在应用服务中完成复杂查询。

    共 3 条评论
    2
  • geek_time
    2020-07-08
    充血模型,是不是在实体里做数据的持久化。比如Leave导入了leaveRepositoryInterface,是不是为了持久数据?

    作者回复: 聚合的持久化都是通过聚合根方法或领域服务来实现,在持久化时,它们会调用仓储接口,然后后仓储实现中的逻辑来完成持久化。 充血模型与贫血模型的关键差异: 在充血模型中,业务逻辑都在领域实体对象中实现,实体本身不仅包含了属性,还包含了它的业务行为。DDD领域模型中实体是一个具有业务行为和逻辑的对象。 而在贫血模型中领域对象大多只有setter和getter方法,业务逻辑统一放在业务逻辑层实现,而不是在领域对象中实现。

    共 2 条评论
    2
  • Geek_821c96
    2020-04-29
    老师您好,我最近看了您的课程收益良多,您的每一讲我都看完了。看完之后了我也有一些疑惑,希望能得到您的指点:1.我看到仅仓储层出现了接口定义,在其它层是不是最好不要使用接口定义了? 2.战术上感觉跟之前的事务基本的架构分层模式的区别主要是:最外层分包结构叫法不太一样但跟之前的controller,service,dao没有太大区别、实体类中多了一些业务逻辑方法、多了事件处理; 3.感觉DDD最主要还是对边界的严格控制上,具体怎么分包并无太大区别。
    展开

    作者回复: 1、如果需要依赖倒置,实现各层的解耦,是可以面向接口编程的,但是同一微服务内没必要搞得这么复杂。所有只有在需要适配前端应用或后端基础资源的时候才用到面向接口的编程,比如用户接口层的Facade和基础层的仓储。 2、3,DDD的核心思想就是划分边界和解耦,这个边界有利于未来微服务架构的演进。

    2