原文链接:Structure your microservice using a hexagonal architecture by Fredrik Lindgren
原文链接:What is DDD - Eric Evans
原文链接:DDD and Microservices: At Last, Some Boundaries!
原文链接:Structure your microservice using a hexagonal architecture by Fredrik Lindgren
Hexagonal Architecture 又名:端口和适配器模式
是一种对测试友好的服务架构模式
allow an application to equally be driven by users, programs, automated test or scripts, and to be developed and tested in isolation from its eventual run-time devices and databases
允许 application 研发过程由,用户驱动、旧有程序驱动、自动化测试驱动或脚本驱动
开发和测试与最终运行时设备和数据库隔离
提出外部与内部的概念,对比传统分层架构
提出对外暴露多个端口,区别与传统分层架构的向上提供接口,向右提供接口
浏览器 ——> 报名参加一项课程 ------> 检查课程报名条件,存储报名信息
浏览器 ——> 报名参加一项课程 ------> 发送报名确认邮件,发送通知给授课老师
POST 请求 ——》 报名服务 ——》通知服务
考察报名服务 参见:API Design:业务一直在变化
业务需求在变化
使用的技术在变化
要求使用协作 sheet 存储数据
协作 sheet 的版本在升级
略
尽管使用的技术在一直变化,但是核心业务中的一些基础概念(basic business concepts)一直沿用下来
有人说:六边形架构,将常规MVC三层架构反转,以业务为主导,自顶向下
这里“业务”指的是能一直沿用下去的,经过提炼的,核心业务中的基础概念。
每一次提交都需要做测试,而且是集成测试
新技术协作 sheet 不便于自动化测试验证
我们可以内网 模拟一套 协作 sheet 的 API 来进行测试,还不能保证准确性
驱动方(Driver Side) | 应用程序核心(Application Core) | 被驱动方(Driven Side) |
---|---|---|
主动请求系统的一方为驱动者 | 应用程序核心不需要知道实现细节 | 被系统调用的一方为被驱动着 |
所有的业务逻辑必须在核心中定义 | 非核心功能(secondary) | |
核心使用端口(java 中叫接口中的方法)(ports)主动定义系统边界。具体的实现通过端口和核心的双向交互 | ||
具体的实现使用适配器(Adapters)和核心做消息转换 | ||
驱动方的适配器(Adapters)通过驱动端口(driver ports)调用核心 | ||
核心通过驱动端口(driver ports)调用被驱动方。被驱动方s实现驱动端口(driver ports) | ||
发出 event | cqrs | xxx |
交互端口(Purpose of Interaction)
一组和(应用程序)外部“用户”交互的端口(接口)
关键目标是交互,具有技术无关性
问题:给学生发消息,与给老师发消息是否需要分开来?
这是一个 trade-off,在本系统中,提供一个高层抽象,发送 Notification
应用逻辑端口
端口不应该和技术相关,而应该和应用程序逻辑相关
接口使用业务命名,而不是 saveXxx 之类的
端口属于某个应该程序
这些端口定义了该应用的边界
ports ================ components =================== adapters
自动化测试解决方案
使用 生产-测试 双重适配器,用以测试业务逻辑
Mock 框架,模拟外部系统
通过外部系统 adapter 测试外部系统,外部系统的集中测试从业务逻辑中独立出来
最后做一些交付测试
隔离外部依赖关系
从简单开始,如果需要,再添加额外的东西,例如单独的模块
简单的 API,宽而浅的 API,不要使用错综复杂的调用层次很深的 API。
尽量将 ports 封装到单个接口中,且接口中的方法全部是需要 exposed 的方法
避免接口里返回接口
ports 中的参数使用不可变对象
最有效的设计软件核心逻辑的方法是什么?Models 或者是获得 Models 的方法
原文链接:What is DDD - Eric Evans
考虑集装箱运输货物的物流问题。集装箱装上船飘洋过海,到达港口,使用转运大卡车,搬到火车站或者其他航线
假设我们要从天津运到上海,我们走老秦开挖的“京杭大运河”到杭州,然后换火车送到上海
天津 ==== 京杭大运河 ====> 杭州 ====== 火车 =====> 上海
名称 | 属性 | 行为 |
---|---|---|
货物 | id,始发地,重点,重量 | x |
运输服务 | 货物ID,航线ID,上货地点,卸货地点,船ID,时间 | x |
问题1:从软件的实现者,开发人员的角度来说
问题2:在非开发人员,需求定义角度来说,整体的领域模式 Model 也有问题
一种方案
名称 | 属性 | 行为 |
---|---|---|
运输服务 | 货物ID,…,出发时间,到达时间 | x |
站点 | 是否始发站,是否终点站 |
第二种方案
我们还需要对比其他方案,找到最佳方案。做软件前多想想替代方案
名称 | 属性 | 行为 |
---|---|---|
运输服务 | 货物ID,…,出发时间,到达时间 | x |
航线 | 上货地点,卸货地点 | x |
站点和航线除了名字,好像没有什么区别嘛!
将脚程 替换 成 航线
斟酌词汇,获得更好的设计
一种更好的方案
运输服务 -------> 行程 --------》站点
名称 | 属性 | 行为 |
---|---|---|
运输服务 | 货物ID,…,出发时间,到达时间 | x |
行程 | x | x |
站点 | 上货地点,卸货地点 | x |
当需求变异,不要犹豫不前,这是完善领域模型的好机会
当需求编译,不要吹毛求疵,不要使用错误的 Model,换下一个
文档?我们只得到了三个词汇:运输服务,行程,站点
不同的 Model 适用不同的目标问题,所以这个问题本身就有问题
名称 | 属性 | 哪个更好? |
---|---|---|
航线 | 起点,终点,运输方式 | ✔ |
站点 | 位置,上/卸货地 | ✖ |
使用一种最适用于目标问题的 Model 来表述
能最清晰的表述目标问题
栗如地图:你是要用地图导航?还是用地图测绘?不同的问题适用不同的地图
xxxx(abstraction)
xxxx
xxxx(assertion)
Domain 是信息和活动的集合(一个界定)
Model 是 Domain 的某个方面(aspect)的抽象,
Model 细化了 Domain
Model 建立在一定的假设的前提下
过多考虑现实使人郁闷(Realism is a distraction),不一定有利于目标问题
建议从不同的方面(aspect)建立多个 model 来,处理一些大的 Domain
不要用一个 Model 代表所有
名称 | 属性 | 对于当前的问题,哪个更好 |
---|---|---|
航线 | 起点,终点,运输方式 | ✔ 我们只需要让上一个航线终点对应到这个航线的起点就可以,所以这个 Model 更方便 |
站点 | 位置,上/卸货地 | ✖ |
对于当前问题:
天津 ==== 京杭大运河====> 杭州港 = ??? => 杭州火车站 ==== 火车 ====> 上海
名称 | 属性 | 行为 |
---|---|---|
货物 | id,始发地,重点,重量 | x |
运输服务 | 货物ID,…,出发时间,到达时间 | x |
行程 | x | x |
航线 | 起点,终点,运输方式 | x |
我们的运输服务,不再直接操作数据库修改状态 ✅
我们只需要让上一个航线终点对应到这个航线的起点就能连接航线
主要任务是定义一些词汇:货物……
因为有的业务非常复杂,有助于理解 Domain
能避免,造出比现实更复杂的设计
修改原有系统,替换航线为站点
使用一个新的子系统,引入站点的概念
使用航线的起点代表站点
原文链接:DDD and Microservices: At Last, Some Boundaries!
自主的团队,隔离的实现
管理大型团队带来的管理问题(acknowledge the rough and tumble of enterprises)
各个部分业务编码水平不一致(cattle not pets)
带来领域驱动的机遇
等等其他……
如何做服务集成?
在交流中的上下文:语义环境,决定其含义
在软件中的限界上下文:
为了简化计算机间不同上下文的整合映射
指定一个模块内,特定 Model 的含义一致
有时候,划分模块很复杂,程序一堆乱麻
在 DDD 中,我们引入了一种新技术 Context-Map,上下文映射,假设一个没有边界的系统如下:
A ---- partner ---- B
假设现在 A 和 B 交互,B 和 A 交互,A 和 C 交互
C 是 A 的下位系统
C – conforming --> A <–> B:C 对 A 具有一致性(conforming)
也就是说 C 中的 Context (使用相同的消息定义,或者说是相同的语言)需要保持和 A 一致,C 为了和 A 集成,放弃一部分自主技术选型的机会
人们一般会这么处理下位系统 C
假设 D 也需要 和 A 交互,D --> A <–>,且 D 不愿意适用和 A 一致的上下文
E --> A ,A 需要从 E 获得数据
现在 D 也需要订阅 E 发出的消息
F 需要订阅 B ,然后 C 确认从 B 过来的消息
旧系统
不同的 team 会定义不同的 model
Model 必须小清晰
Model 必须有清晰的定义
定义清晰的上下文
需要有简单的断言
所以我们需要边界来支持业务断言
让 A、C、D、E 都和新引入的 I 交互。I = 一个内部交换上下文(interchange context)
不改变原有的交互方式,只改变得到消息的方式。
为了更好的执行,我们引入了一个 Domain Language
以服务接口/消息的形式表示
不同于服务内部的对象/功能
防止限界上下文的早期-扭曲/冻结
当我们有许多服务时,提供全局性的理解
和企业级集成不同,我们会有不止一个交换上下文(为边界内频繁交互的服务做交换上下文)
为啥不做系统的逻辑划分?(模块划分)
因为我们做逻辑划分做了几十年了,也没有看到效果。
逻辑边界不是很清晰,不会有明确的物理边界
规范会所有东西不现实
如果是传统的单一项目,这是可行的
实际经验表明,大部分的项目,逻辑划分无法经受风吹雨打
DDD 提出了具体的(concrete boundaries)边界,微服务恰好提供这些特性
服务的激增重现了一些旧问题
有助于构建高内聚的微服务集群