端口和适配器架构有什么好处
DDD邮轮,有咨询公司的报告显示,在接下来的几年内,邮轮游作为国人出游形式的比例会大幅上升,在这样一个大背景下,DDD Cruise,一家中国的邮轮公司,正在研发新一代的预订系统,尝试在线邮轮预订。
目前计划中有两个触点应用:
1.微信小程序
提供邮轮搜索、邮轮预订的核心体验。
2.中国区官网
这原是一个包含几个HTML页面的遗留应用,本次希望可以提供邮轮搜索的功能,值得注意的是,有部分邮轮是承包给旅行社销售,在网站上也需要展示以便做市场宣传。
C4 Model——System Context Diagram
在这两个触点背后,是这次的主角,预订引擎1.0,计划以一个单体应用起步,为触点应用提供API,实现邮轮搜索、邮轮预订。邮轮有多个数据来源,一部分来自一个遗留的预订系统,一部分来自业务部门的Excel表格,存放在AWS S3对象存储中。最后还有一个小型的Headless CMS为市场人员提供邮轮描述,吸引眼球。
现在让我们代入端口和适配器:
上“套路”,Driving Adapter一个,端口两个,Driven Adapter两个,连线少许。
1.API Controller是一个典型的Driving Adapter,它实现Rest API的Endpoint,调用入口端口CruiseSearch。
2.CruiseSearch作为应用的入口,向Driving Adapter屏蔽了邮轮搜索的实现。
3.在另一边,出口端口CruiseSource要求返回全量的Cruise数据,为应用隐藏了外部数据源的集成方案:从遗留预订系统或AWS S3上的文件中抽取Cruise。
促进单一职责原则
那么我们接下来在这个架构的基础上,进行概要设计,组件很自然地分为了三个部分:
概要设计类图
1.绿色是Driving Adapter,如果你对Java-Spring技术栈,可以从命名发现他是一个RestController。
2.黄色是Cruise Search的实现,这里的概念只和邮轮相关,你在这里不应该看到技术术语。
3.粉色部分则是Driven Adapter,除了与处理从数据源获取Cruise的Adapter,我们还需要。
a. CompositeCruiseSource,它不直接与数据源打交道,但它负责合并多个数据源并根据规则去除重复的Cruise。
b. CachingCruiseSource,它也不直接与数据源打交道,负责缓存Cruise。
从架构角度来看,这些组件很简单。请注意,简单(Simple)并不代表着容易(Easy),简单说的是只做一件事(或一种事),而容易是指做一件事的难度,例如如果使用Spring MVC实现Driving Adapter,利用注解寥寥几行代码就可以实现。由于这些组件要么实现业务逻辑,要么实现对某种技术的适配,符合单一职责原则,你可以更有效地将变更控制在某一个范围内,更有信心地应对变化。
澄清测试策略
应对变化的另一个有效手段是自动化测试,测试金字塔是最常被提及的测试策略,它建议自动化测试集应该由大量单元测试作为基础,它们编写容易、运行速度快,应该只包含少量的用UI驱动的测试,由于需要处理测试数据冲突、外部依赖准备,它们编写困难、运行速度也较慢。但中层的service/集成测试的测试目标是什么,它们和单元测试有什么区别呢?
测试金字塔——端口和适配器版
如果你也有此困惑,不妨按照端口和适配器架构来重新解读,金字塔应该包含大量的Driving Adatper测试、业务逻辑测试、Driven Adapter测试。
1.Driving Adapter测试,目标是验证API能正确地解析输入、按预期的参数调用了入口端口并生成输出。由于Driving Adapter不关心入口端口的实现,在测试中,可以通过Mock方便地构造测试场景,并提升测试速度。
近两年开始流行的契约测试也可以认为是Driving Adapter测试的扩展
2.业务逻辑测试,通过Mock出口端口,同样可以方便地构造测试数据,而且这里应该都是Plain Object,测试可以完全在内存中运行,速度是最快的。
传统的单元测试
3.Driven Adapter测试,目标是验证按预期的方式操作了外部工具、下游服务、数据库。传统上,涉及这些外部依赖的测试编写难度大,运行速度慢,但如果出口端口和Driven Adapter设计得当,它们就不涉及业务逻辑,从而需要测试用例会大大减少,通过引入内存数据库、Stub Server等技术,其测试场景的构建难度会改善不少,整体执行时间也会相应减少。
单一职责的Driven Adapter也降低了测试难度,不过测试速度仍然相对较慢
需要注意的是以上测试都是在技术上检测组件是否符合预期,可以考虑适当加入E2E Test来验证这些组件集成起来可用,业务上符合预期,一般覆盖关键功能的Happy Path场景即可。
促进增量开发
端口和适配器架构可能还能给与我们一些灵感,实施增量开发,不妨看一下这个用户故事分解的例子:
由于旅行社代售的邮轮都来自于Excel表格,只要确定了表格字段含义,我们就可以开始集成,我们选择这张卡来搭建脚手架:
如果业务优先级允许,选择技术实现最简单的卡搭建脚手架
接下来在InMemoryCruiseSearch中实现筛选:
实现筛选功能
引入LegacyBookingCruiseSource和CompositeCruiseSource
扩展数据源,另外还需要扩展Cruise销售渠道的筛选条件实现
最后,可以引入一张技术卡:
加入CachingCruiseSource,提升Cruise读取速度
到这里,我们不妨小结一下:
端口和适配器架构的组成元素及它的好处