梳理之后的领域设计类图非常规范。除了合成关系,存在OO聚合关系的实体都分到不同的聚合中,更不用说完全独立的Backlist实体。如果多个聚合边界的实体依赖了相同的值对象,可以定义多个相同的值对象,然后将它们放到各自的聚合边界内。分解关系薄弱处确定的聚合边界如图20-55所示。
考虑聚合设计原则,由于Learning聚合中的Course实体具有独立性,因此需要对图20-55稍做调整,将Course实体分离出来,定义为单独的聚合。除此之外,其余聚合边界都是合理的,不需再做调整。最终,确定了聚合边界的领域设计类图如图20-56所示。
由此得到的聚合包括:
qTraining聚合;
qCourse聚合;
qLearning聚合;
qTicket聚合;
qTicketHistory聚合;
qFilter聚合;
qValidDate聚合;
qValidDateAction聚合;
qCancellingAction聚合;
qCandidate聚合;
qAttendance聚合;
qBlacklist聚合。
即使在领域设计模型中,我们也无须为领域模型对象定义字段。每个聚合内的实体或值对象到底需要定义哪些字段,可以结合业务服务,通过测试驱动开发逐步驱动出来。领域设计类图最重要的要素是聚合。一旦确定了聚合,实际上也就确定了管理聚合生命周期的资源库。至于需要哪些领域服务和其他角色构造型,可以交由服务驱动设计来识别。
(2)服务驱动设计
服务驱动设计的起点是业务服务。以提名候选人业务服务为例,将业务服务规约的基本流程转换为由动词短语组成的任务,然后通过向上归纳和向下分解获得由组合任务与原子任务组成的任务树:
q提名候选人(业务服务)
♦ 确定候选人是否已经参加过该课程
○ 获取该培训对应的课程
○ 确定课程学习记录是否有该候选人
♦ 如果未参加,则提名候选人
○ 获得培训票
○ 提名
○ 保存票的状态
♦ 发送提名通知
○ 获取通知邮件模板
○ 组装提名通知内容
○ 发送通知
结合任务分解与角色构造型,它的序列图脚本如下:
NominationAppService.nominate(nominationRequest) {
LearningService.beLearned(candidateId, trainingId) {
TrainingRepository.trainingOf(trainingId);
LearningRepository.isExist(candidateId, courseId);
}
TicketService.nominate(ticketId, candidate) {
TicketRepository.ticketOf(ticketId);
Ticket.nominate(candidate);
TicketRepository.update(ticket);
}
NotificationService.notifyNominee(ticket, nominee) {
MailTemplateRepository.templateOf(templateType);
MailTemplate.compose(ticket, nominee);
NotificationClient.notify(notificationRequest);
}
}
该序列图脚本对应的序列图如图20-57所示。
图20-57中的NominationAppService应用服务承担了多个领域服务之间的协作职责,且需要根据beAttend()方法的返回结果决定提名的执行流程。这实际上属于领域逻辑的一部分,故而应该在NominationAppService应用服务内部引入一个领域服务来封装这些业务逻辑。新增的领域服务为NominationService,修改后的序列图如图20-58所示。
图20-58中的MailTemplate是一个聚合,存储了不同类型操作需要通知的邮件模板。在前面的领域分析建模与领域设计建模时,未能发现该聚合。这也印证了领域建模很难一蹴而就,需要不断地迭代更新和演进。