在上一篇文章中,以英雄联盟自动预警系统为例子,展开了事件风暴获得了上下文,这里就继续延伸,看看事件风暴的产物还可以怎么进一步落到开发代码上面。
这里我们聚焦案件记录上下文,假设它独立为一个服务存在。然后我们可以把它对应的决策/命令搬过来一起看:
Step1. 设计API
结合命令风暴的结果我们发现有一部分的决策是外部触发的(框起来的部分),那这部分我们就应该为它们设计对应的API
在api/rest/CaseController.java:
@RestController @RequestMapping("/case") public class CaseController { @PostMapping public void createCase(@RequestBody String caseBrief) {} @PostMapping("/{caseId}/close") public void closeCase(@PathVariable int caseId) {} @PostMapping("/{caseId}/esculate") public void esculateCase(@PathVariable int caseId) {} @PostMapping("/{caseId}/thief") public void updateThiefInfo(@PathVariable int caseId, @RequestBody String thiefInfo) {} @PostMapping("/{caseId}/fight-plan") public void addFlightPlan(@PathVariable int caseId, @RequestBody FightPlanDTO fightPlanDTO) {} @PostMapping("/{caseId}/fight-plan/{planId}/success") public void markSuccessFightPlan(@PathVariable int caseId, @PathVariable int planId) {} @PostMapping("/{caseId}/fight-plan/{planId}/fail") public void markFailFightPlan(@PathVariable int caseId, @PathVariable int planId) {} }
这里大体上遵循了REST的规则,充分使用URL指定所需的资源。当然,比如像“关闭”案件记录“应该用POST还是DELETE这些细节地方也是可以进一步斟酌的。
Step2. 构建领域对象
把聚合根对应的实体和值对象,以及后面补充的一些关键属性补充起来,暂时它还是一个贫血的POJO:
public class CaseReport { int id; Timestamp startTime; Timestamp closeTime; String caseBrief; String thief; List<FightPlan> fightPlanList; } public class FightPlan { int id; String status; String planDetail; int meetingId; }
Step3. 构建应用服务和领域服务
这里以系统收到升级预警通知的步骤为例子:
场景与步骤:收到升级预警通知 -> 触发升级预警
- 校验所升级预警的案件存在
- 创建线上会议
- 把会议登陆URL发送通知给其他英雄
如果按我自己原来的写法,我会把这个逻辑写在应用服务层ApplService里面。但是刚巧在DDD跟张逸老师协作了他的一个场景驱动工作坊,所以这里尝试用他的方式去规划一下。
场景放在App Service:
@Service public class CaseReportService { @Autowired CaseReportService caseReportService; @Transactional public void esculate(int caseId) { caseReportService.esculate(caseId); } }
步骤放在DomainService:
@Service public class CaseReportService { @Autowired CaseReportRepository caseReportRepository; @Autowired FightPlanRepository fightPlanRepository; @Autowired MeetingGateway meetingGateway; @Autowired NotificationGateway notificationGateway; public void esculate(int caseId) { final CaseReport caseReport = caseReportRepository.getCaseReportById(caseId); if (caseReport != null) { final MeetingInfo meeting = meetingGateway.createMeeting(caseReport); notificationGateway.sendEsculationMsg(meeting); } else { throw new BizException(INVALID_CASEREPORT_ID); } } }
碰巧这里三个步骤都是调用底层服务,所以都是调用了gateway/repository去进行,如果当中有一些是不依赖与底层的动作,则应该充血地放到DomainObject(CaseReport)里面去。
至此,一个分层架构大概出来了,并且已经完成了业务domain的开发以及API的入口:
分层架构
最后,聚合细化的信息其实也是可以对应给DB设计指引的:
- 1-1的场景下,可以合在一个表,可以两个表
- 1-n的场景下,通常两个表,也可以三个表
- n-n的场景下,通常三个表
对应我们的例子,可以这样来设计表结构:
CREATE TABLE case_report_tbl ( id int NOT NULL AUTO_INCREMENT, case_brief varchar(255) NOT NULL, start_time TIMESTAMP NOT NULL, close_time TIMESTAMP NOT NULL, thief varchar(255), PRIMARY KEY (ID) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE fight_plan_tbl ( id int NOT NULL AUTO_INCREMENT, status varchar(10) NOT NULL, plan_detail varchar(255) DEFAULT NULL, meeting_id int NOT NULL, case_id int NOT NULL, PRIMARY KEY (ID), FOREIGN KEY (case_id) REFERENCES case_report_tbl(id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
当然,现在也有很多是推荐不用FOREIGN KEY的设计手法,开放式无约束地建表,并把校验的逻辑都回归到代码里面并使用自动化测试去保证这个必须的逻辑校验,所以这里也只是一个指引,大家可以按需谋划。
到此,事件风暴对应下来,唯一还未补的缺应该就剩下底层所需的技术支撑了。比如Rest调用三方服务,或者是DB相关的SQL。这个应该是easy job就不继续写了哈 ^_^