DDD
原文链接: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 |
六边形架构——端口(ports)
端口分类
-
交互端口(Purpose of Interaction)
-
一组和(应用程序)外部“用户”交互的端口(接口)
-
关键目标是交互,具有技术无关性
问题:给学生发消息,与给老师发消息是否需要分开来?
这是一个 trade-off,在本系统中,提供一个高层抽象,发送 Notification
-
-
应用逻辑端口
端口不应该和技术相关,而应该和应用程序逻辑相关
接口使用业务命名,而不是 saveXxx 之类的
-
端口属于某个应该程序
-
这些端口定义了该应用的边界
-
整体架构
ports ================ components =================== adapters
自动化测试解决方案
-
使用 生产-测试 双重适配器,用以测试业务逻辑
-
Mock 框架,模拟外部系统
-
通过外部系统 adapter 测试外部系统,外部系统的集中测试从业务逻辑中独立出来
-
最后做一些交付测试
设计原则
-
隔离外部依赖关系
-
从简单开始,如果需要,再添加额外的东西,例如单独的模块
-
简单的 API,宽而浅的 API,不要使用错综复杂的调用层次很深的 API。
-
尽量将 ports 封装到单个接口中,且接口中的方法全部是需要 exposed 的方法
-
避免接口里返回接口
-
-
ports 中的参数使用不可变对象
第二部分:DDD 基础——是什么?为什么?怎么做?
最有效的设计软件核心逻辑的方法是什么?Models 或者是获得 Models 的方法
直接举个栗子
考虑集装箱运输货物的物流问题。集装箱装上船飘洋过海,到达港口,使用转运大卡车,搬到火车站或者其他航线
假设我们要从天津运到上海,我们走老秦开挖的“京杭大运河”到杭州,然后换火车送到上海
天津 ==== 京杭大运河 ====> 杭州 ====== 火车 =====> 上海
我们还不能开始设计 Model 和 API
- 首先考虑问题域的其他旧系统,考虑系统集成 context-Mapping
- 其次……
先 look look 高层架构
名称 | 属性 | 行为 |
---|---|---|
货物 | id,始发地,重点,重量 | x |
运输服务 | 货物ID,航线ID,上货地点,卸货地点,船ID,时间 | x |
-
问题1:从软件的实现者,开发人员的角度来说
- 可读性,可扩展性…
- 运输服务太大了,里头逻辑太多,太复杂
- 🤦♂️ 因为有一条老的软件原则,不要使用 update 数据库 state 来实现业务逻辑 🤷♂️
- 你这运输表得多少状态字段啊……
-
问题2:在非开发人员,需求定义角度来说,整体的领域模式 Model 也有问题
- 同物流专家的角度探讨一下,基本流程,栗如把这个存储到数据库
- 天津 ==== 京杭大运河 ====> 杭州港 = ??? => 杭州火车站 ==== 火车 ====> 上海
- 杭州港到杭州火车站好像挺远的………………
- ━┳━ ━┳━ 怎么和专家解释呢?这时候我们需要一种共通语言(DDD 的重要组成部分)
所以,我们的 application core ,缺少哪些基本概念(concept)?
-
一种方案
名称 属性 行为 运输服务 货物ID,…,出发时间,到达时间 x 站点 是否始发站,是否终点站 -
第二种方案
我们还需要对比其他方案,找到最佳方案。做软件前多想想替代方案
名称 属性 行为 运输服务 货物ID,…,出发时间,到达时间 x 脚程上货地点,卸货地点x航线 上货地点,卸货地点 x -
站点和航线除了名字,好像没有什么区别嘛!
-
将脚程 替换 成 航线
-
斟酌词汇,获得更好的设计
-
-
一种更好的方案
运输服务 -------> 行程 --------》站点
名称 属性 行为 运输服务 货物ID,…,出发时间,到达时间 x 行程 x x 站点 上货地点,卸货地点 x -
当需求变异,不要犹豫不前,这是完善领域模型的好机会
-
当需求编译,不要吹毛求疵,不要使用错误的 Model,换下一个
-
文档?我们只得到了三个词汇:运输服务,行程,站点
选择一个 Model 方案
使用 UML 背后的 Model,背后的基础概念(concept) 而不是 UML 本身
不同的 Model 适用不同的目标问题,所以这个问题本身就有问题
名称 | 属性 | 哪个更好? |
---|---|---|
航线 | 起点,终点,运输方式 | ✔ |
站点 | 位置,上/卸货地 | ✖ |
站在不同的角度,对一个问题有不同的看法
-
使用一种最适用于目标问题的 Model 来表述
-
能最清晰的表述目标问题
-
栗如地图:你是要用地图导航?还是用地图测绘?不同的问题适用不同的地图
一个 Model 由一系列 Statements 组成
-
xxxx(abstraction)
-
xxxx
-
xxxx(assertion)
Modle 的定义
-
Domain 是信息和活动的集合(一个界定)
-
Model 是 Domain 的某个方面(aspect)的抽象,
-
Model 细化了 Domain
-
Model 建立在一定的假设的前提下
-
过多考虑现实使人郁闷(Realism is a distraction),不一定有利于目标问题
Model 应该缩小关注点
-
建议从不同的方面(aspect)建立多个 model 来,处理一些大的 Domain
-
不要用一个 Model 代表所有
哪个 Model 更有用?用这个 Model 来解决 xxx 问题?
名称 | 属性 | 对于当前的问题,哪个更好 |
---|---|---|
航线 | 起点,终点,运输方式 | ✔ 我们只需要让上一个航线终点对应到这个航线的起点就可以,所以这个 Model 更方便 |
站点 | 位置,上/卸货地 | ✖ |
对于当前问题:
天津 ==== 京杭大运河====> 杭州港 = ??? => 杭州火车站 ==== 火车 ====> 上海
所以,最终方案
名称 | 属性 | 行为 |
---|---|---|
货物 | id,始发地,重点,重量 | x |
运输服务 | 货物ID,…,出发时间,到达时间 | x |
行程 | x | x |
航线 | 起点,终点,运输方式 | x |
-
我们的运输服务,不再直接操作数据库修改状态 ✅
-
我们只需要让上一个航线终点对应到这个航线的起点就能连接航线
-
主要任务是定义一些词汇:货物……
为什么要定义这些 Model
-
因为有的业务非常复杂,有助于理解 Domain
-
能避免,造出比现实更复杂的设计
现在业务扩展了,我们重新需要站点,用于到站计费啥的
-
修改原有系统,替换航线为站点
-
使用一个新的子系统,引入站点的概念
-
使用航线的起点代表站点
怎么做?参考上文
- 如果 model 不再适用与目标问题,不管 Model 设计的多好,弃之食草
第三部分:DDD与微服务
原文链接:DDD and Microservices: At Last, Some Boundaries!
MicroService
-
自主的团队,隔离的实现
-
管理大型团队带来的管理问题(acknowledge the rough and tumble of enterprises)
-
各个部分业务编码水平不一致(cattle not pets)
-
带来领域驱动的机遇
-
等等其他……
两个服务之间的交互如何定义?
如何做服务集成?
-
在交流中的上下文:语义环境,决定其含义
-
在软件中的限界上下文:
-
为了简化计算机间不同上下文的整合映射
-
指定一个模块内,特定 Model 的含义一致
-
有时候,划分模块很复杂,程序一堆乱麻
-
交互遇到的问题:上下文映射(Context-Map)越来越复杂
在 DDD 中,我们引入了一种新技术 Context-Map,上下文映射,假设一个没有边界的系统如下:
-
A ---- partner ---- B
- 意味着 A 和 B 之间存在一个 translator ,使得 A 和 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 一致的上下文
- D 与 A 之间需要定义一个更复杂的 translator
-
E --> A ,A 需要从 E 获得数据
- 这意味着,尽管是从 E 指向 A,但是是数据流,A 依旧是驱动方,E 是被驱动方
- 这时候 A 不能很好的完成工作,因为现在 A 具有很高的耦合(C,D,E都和 A 交互),而 A 本身的 Model 是为了处理复杂逻辑而设计的,而不是处理数据转换而设计的
-
现在 D 也需要订阅 E 发出的消息
-
F 需要订阅 B ,然后 C 确认从 B 过来的消息
- 这时,C 需要确认 F 和 B,这是不可行的。
交互遇到的问题:我们总是有不同的 Model
-
旧系统
-
不同的 team 会定义不同的 model
交互遇到的问题:应用集成是,不同的系统业务不一致
-
Model 必须小清晰
-
Model 必须有清晰的定义
-
定义清晰的上下文
-
需要有简单的断言
- 栗如,不存在没有订单的客户,但不是所有的系统都有这样的假设
-
所以我们需要边界来支持业务断言
-
一种解决方案
让 A、C、D、E 都和新引入的 I 交互。I = 一个内部交换上下文(interchange context)
不改变原有的交互方式,只改变得到消息的方式。
内部交换上下文
-
为了更好的执行,我们引入了一个 Domain Language
-
以服务接口/消息的形式表示
-
不同于服务内部的对象/功能
-
防止限界上下文的早期-扭曲/冻结
-
当我们有许多服务时,提供全局性的理解
-
和企业级集成不同,我们会有不止一个交换上下文(为边界内频繁交互的服务做交换上下文)
为什么不使用逻辑边界
-
为啥不做系统的逻辑划分?(模块划分)
-
因为我们做逻辑划分做了几十年了,也没有看到效果。
-
逻辑边界不是很清晰,不会有明确的物理边界
-
规范会所有东西不现实
-
如果是传统的单一项目,这是可行的
-
-
实际经验表明,大部分的项目,逻辑划分无法经受风吹雨打
用别的方式来划分边界
-
DDD 提出了具体的(concrete boundaries)边界,微服务恰好提供这些特性
-
服务的激增重现了一些旧问题
- 当太多的服务进行交互时,很容易出现 “意大利面代码”(一团乱麻)
-
有助于构建高内聚的微服务集群