DDD领域驱动设计
# 简介
DDD 在代码,微服务长久实施的理论指导。
# 数据驱动设计
传统开发: ER图,向下可以设计表结构,向上设计实体类。 POJO/info 实体类 (贫血模型) 只有属性。 事务脚本: 梳理业务还是按照业务流程(面向过程)的方式去实现。 以上戏称: 数据驱动设计。
# 优点
- 如果软件团队可以很好的把握住里面的业务数据变化(所有业务流程)的过程,那么采用这个模式去设计也是很好的。
- 可以短频快的开发。
- 有了ER关系就可以放心的开发了。 核心: ER关系 和 所有的业务数据流程的变化需要清楚。 听说日本的公司,就是用这种瀑布模型严格要求和实践的。
# 缺点
- 业务越来越复杂,容易出现超出业务掌控的业务流程,从而打乱数据流程变化,至此导致有些数据莫名奇妙的被修改,找bug找好久的情况。 还有可能有子系统数据隔离的风险,别的系统代码修改你的数据,你找bug??? 我听说过这种情况。
- 事务脚本越来越多,从而出现if 的不可控的代码。 不可避免的造成代码层面的系统老化: (从而这里出现了静态代码检查工具,规范代码过程、如if嵌套层级/方法代码行数,一方面提高可读性,另一方面把软件设计的过程分散到每个迭代的过程中避免代码失控,从而强制开发人员用抽离方法/设计模式来优化整体代码,每次都要调整部分项目结构) 然而上面的这种在具体实施的过程中并不明显,再做上面的代码调整过程中,可能也会预估将来的业务需求,做扩展点接口的预留(灵活设计),当时这些扩展点都是针对某些的业务,但是以后的实际业务需求可能不是预留的,那么最终的结果就是预留点成了摆设,还增加了代码复杂度/成为了业界中常称的过度设计。
- 在架构上面,主体业务类会成为一个大泥球(某张表的字段非常多,比如商品表中的采购/物流/厂家....),你可能会说拆表/表关系什么的,这些是具体的实现,DDD也是这样做物理上也是这样实现的,但是逻辑上可不会这样体现。我知道这一点说服不了你,但是下面的一条是他的大缺陷。
- 从横线看层次是分明的,但是它的业务纠缠严重,也就是service模块之间的互相调用是强耦合的,最终导致关系越来越复杂。 要拆开成服务,改动会很多,service之间的还有实体之间形成了密切耦合的关系。甚至有可能出现一个需求要改一个小字段,它会导致上面的service之间都要变动,形成牵一发动全身的情况。 这个情况遇见过吗pojo到sql、到DTO、BO、VO还有service,美滋滋???
- 单体 -> 微服务 ,利用数据驱动模式;拆分主要依靠架构是的经验,也就出现了极大的变数, 各个系统不断的膨胀,还是会有问题;且必须提前规划好,否则后期系统内部就会出现各种数据交错了,那时候拆分=重构。
最终形成系统老化 ,导致: 开发难,if 堆积/数据流程不清晰 -> 测试难,单元测试更加难提/无法针对的编写单元测试,大部分只能做回归测试 -> 沟通难,开发和产品扯皮(核心还是数据变动不清晰,产品觉得简单,开发觉得难) -> 创新难,改动太大不敢动。
数据驱动设计,上面的核心问题是软件实现和业务需求脱离了联系(或者说偏差,传统的MVC的技术层次在业务中是没有的,这中间的业务转化,最终会造成信息的不一致;同时业务代码中包含了各种专业技术相关的混合操作,梳理业务也困难,如果里面的类图层级再深一些,再复杂交错的话,基本没法梳理业务,这个再第一家公司深有体会呀,文档和代码注释对不上、注释和实现对不上,还是和小师傅,当时还做了思维导图最后也有遗漏的地方,也是一个需求做好几天,大部分时间)。
# 微软的ERP 系统
领域加实施的方式,去解决上诉问题。 他把ERP软件销售的同时,还提供了解决方案。 也就是说强制要求现实中的业务模型和他的软件保持一致。 可以看到这种方式,太霸道了,不仅要求现实向软件妥协,还要求软件产品和解决方案2者的成熟度都非常高,不能说是卖出去了软件和提供的解决方案还有冲突或者bug啥的。 他是现实向软件妥协。
# 领域驱动设计
而DDD是软件向现实妥协。
定义: 核心是将真实世界(事物、行为、关系等等)与软件世界(属性、方法、关联)通过领域模型联系到一起,并且一一对应。 其实也就是贫血和充血模型。 定义之间的区别: 通过领域模型向上对于业务需求,向下对应具体的代码逻辑实现。 可以看到和ER关系核心不再是关注数据了,而是整体的业务了。 如果业务需求变更了的话,直接操作领域模型。 而ER图则需要先梳理各个数据的关系和他们之间的扭转。 方案之间的区别: 不再使用模块、服务、组件来划分项目了,而是采用BC(限界上下文) 。 通过BC再不同的领域中,领域类封装了领域模型对象以及处理该领域所需的知识语境(也就是说同一对象再不同的领域中会表现出不同的含义/属性和不同的行为,比如商品在销售中关注的是性价比/价格/特点、在物流中关注的是重量/大小/地点)。
# 代码改造和四层架构
# MVC在代码层面有啥缺点吗???
- 测试麻烦问题: 需要部署所有相关服务和中间件等等外部的依赖。 如果用DDD做了防腐层的处理,对这些外部依赖做了解耦适配是不是就ok了??
- 单元测试困难: 所有的测试用例可能随着外部依赖的扩展而增加,这些对于业务本身本来没什么用的,但是要覆盖全的测试用例有必须做,这样就导致一个微服务加了新功能,引用了该服务的业务都要做其他测试用例指数级编写,这是不是核心的问题???;
- 单月测试无法保证最小粒度 : 比如我想测试某个外部依赖是否正确,在MVC中是把它单独抽成一个方法来进行测试来实现的,这样也会导致一个service有很多无用的方法对外暴露,如果你不写那就不讨论了。
- 事务脚本最严重的问题: 违法了单一职责、依赖反转、开放封闭原则。 应该只有业务变化是他的单一职责问题,不应该依赖外部的实现而是基于外部的抽象层,对可被修改业务代码应该封装成可被扩展的类。
- 前面提到的业务纠缠严重的问题。
# 代码改造
基于上面的问题,如何做改造???
- 把实体改成充血模型的领域。
- 充血模型: 有属性和行为(方法)、里面的属性不再和数据表相关而是和行为相关。 这样的好处就是、看到这个模型就能知道他能做什么事/有什么属性,不用再去service和实体类中看了。
- 数据库相关操作的改造 : 在定义仓库层接口,对外提供存储服务,在这里面依赖传统dao和info做相关的转化成领域实体(但是这些info和dao对外是不可见的)。 这样还能把仓库建成多实现用于解耦,可以替换数据库/还可以用mock去做操作等等。 防腐层ACL(适配模式),隔离外部服务依赖,隔离第三方组件(消息队列/规则引擎等等)。
- 定义服务接口,改接口不再以第三方的输入输出为主,而是以自身系统的业务来定义输入输出。
- 在实现类中,依赖真实的第三方,并做一些适配模式的逻辑处理。使得不再关注第三方的变动而影响业务。
- 还可以在这块做一些公共的处理,如缓存、降级熔断,还可以做一些开关处理等等操作。 用领域服务封装多领域实体的逻辑操作。
- 避免入参是基础类型,导致语义不清晰。 把入参改成有业务意义的值对象。
- 同时在这些值对象中,可以封装一些检查/校验/set改变值等等的操作。这样可以避免这些对值对象的检测和封装四处扩散。
- 将多个值对象的业务操作,做聚合同时单独封装成一个类,这样业务扩展/变更也就下放到了这个类中去做处理。
- 同时他聚合了值对象的关系,使得他们互相影响的操作,也能独立出来。 而不用在值对象中互相引用的复杂类图的形式。
总结:
- 将业务逻辑和数据流转之间相互独立/分离。
- 通过封装保证应用层(业务层)是独立的,不依赖任何中间件和服务的具体实现,而可以独自演进。
- 独立且包含所有的核心业务逻辑,测试可以通过不同的组件做一些模拟操作。
# DDD 4层架构
本质: 将软件系统变化的原因进行分析,并把它们抽象出来做单独的演进过程,而体现出下面的层次。 意义: 远不止在代码层面,还可以对微服务体系、中台战略等等做一些理论指导。
用户接口层: 和业务关系、前端依赖紧密,变化频繁。
应用层/业务层: 随着业务变动可能会变动。
领域层: 不依赖与具体实现,而是抽象的一些操作,而且核心逻辑是不会变的。
基础层: 下面的具体实现可能也会频繁变动,比如数据库的字段、聚合操作、第三方接口、缓存、甚至还有中间件也可能会替换。
# 指导应用架构
核心思想: 面向变化,做解耦、内聚、抽象、分层的实现。
领域模型的基础就是领域服务、实体和值对象。
通过防腐层保持底层领域模型的稳定。
# 概念
| 贫血模型 | 充血模型 |
|---|---|
| 只有数据属性和get/set方法,把业务逻辑放到事务脚本中去实现,如Dao/service/.... | 有数据属性和内敛的业务操作(方法),把部分不依赖于外部的业务操作放到自身来实现,减少了service/Dao中的一些方法。 |
| 贫血失忆症 : 贫血模型下的业务模型变更必然带来大量代码变更 | 业务内敛,变化平缓 : 模型变更就变得非常简单,模型变更的影响范围也得到有效收敛 |
| 简单易行 | 设计复杂,组件膨胀,代码层级更多,调用关系更深。 |
| 架构稳定 | 需要团队协同作战,同时整体的架构退化的风险更高(再各层从互相引用导致类似service纠缠严重,这样层级还多,最终会变成比充血模型还大的泥球) |
| 业务定义: 将所有的操作(CRUD)都被认为是业务 | 业务定义: 将只会引起实体状态变化的流程,查询就不再是业务了。 这些也就可以使用事务脚本去使用 |
| 需要注意: 后期迭代中的贫血失忆症。 适合业务变化不是很频繁的场景 | 需要注意: 架构退化的风险更高,这是一条红线。 适合业务变化很频繁的场景,一般都是核心业务 |
| 简单直接的草根 | 高雅昂贵的贵族 |
贫血模型和充血模型各有利弊,项目中需要灵活取舍
| 实体 | 值对象 |
|---|---|
| 代表那些具有唯一ID的领域对象 | 代表那些一成不变的、本质性的事务 |
| 实体与实体之间,通过ID相关联 | 实体与值对象之间,通过信息冗余进行关联 |
值对象 : 领域服务 :
# 领域服务
- 领域服务表示的就是那些在领域对象之外的操作和行为。
- 将各个领域的行为组成为业务操作。相当于是整个业务行为的粘合剂,他去组织领域、仓库等等的操作。
- 跨实体的业务操作,交由服务来协调。
- 服务用来隔离业务逻辑与技术实现。(重点,也是DDD实施的重点注意事项)
注意: 这些服务面向领域对象的行为,而不能直接操作实体的属性(这些操作应该放到对应的领域中)。这一点是非常重要的,也是区别于事务脚本的。否则就退化成了事务脚本和领域脚本混合的四不像,及其容易使得DDD失败。
# 防腐层(Anti-Conrruption Layer)
和服务类似,用来隔离业务逻辑与技术实现。 这里不仅仅是DDD的,还有适配器模式也是这个概念。同时DDD也是主要使用这个模式。
防腐层是保护领域安全的闸门,他的侧重点是隔离,服务侧重点是组织业务逻辑。
它不仅可以做输入输出转换、中间件的使用的选择做隔离,还可以把隔离对外部通用的逻辑,比如缓存、熔断等等
哪些功能适合放入防腐层??? 微服务、Netty、HTTP、新老系统隔离
同时这里吐槽一下,有些团队要求再service中做面向接口编程,而实际上,service下对应的业务只有一个实现类,完全起不到隔离的作用。当然,有些有做灰度发布的,那么这种情况service有多个版本就当我没说。
# 总结
1、贫血模型和充血模型各有利弊,项目中需要灵活取舍
2、领域模型的基础就是领域服务、实体和值对象。
3、通过防腐层保持底层领域模型的稳定。
# 如何保护领域模型
在迭代过程中,领域模型需要保护,防止退还,最终导致架构失控的问题。
聚合、仓库、工厂这些概念的实际意义,最终用来保护领域模型。
通过仓库和工厂来实现聚合的设计。
# 聚合(Aggregator)
实体和值对象体现的是个体的能力,聚合体现的是这些个体的系统协调工作能力。
聚合是用来确保这些领域对象在实现共同的业务逻辑时,能保证数据的一致性。
判断聚合关系最有效的方式,就是去看是否符合整体与部分的关系。
# 聚合根(Aggregator Root)
每个聚合内部有一个外部访问聚合的唯一入口,称为聚合根。
每个聚合中应确定唯一的聚合根实体。
通过聚合与聚合根的设计,极大的简化整个系统内的对象关系图。
# 仓库(Repository)
获得所有领域模型的入口。 可以看做是抽象和内部业务聚合的门面。
# 工厂(Factory)
用于初始化领域模型,和管理领域模型的全生命周期。 可以看做是具体的实现。
# DDD的核心
# 领域
关于领域,最重要的一个关键词就是范围,范围也就是边界
# DDD中维护领域的方式
DDD中强调通用语言的定义: 1、通用语言是一种团队共享的语言 2、通用语言只表达一个单一的领域模型 3、DDD强调通用语言的统一表达
DDD采用“分而治之”的思想,将领域不断切分成不同的子域
# 限界上下文(Bounded Context)
有了领域划分后,就需要保证领域之间的边界,这个边界就是限界上下文。
限界上下文是领域模型的知识语境;也是业务能力的纵向切分。
核心思想就是 “单一职责原则”,每个限界上下文内实现的都是软件变化的同一个原因(内部只有一个原因导致变化)。
# 使用DDD设计系统
系统的设计都需要从需求分析开始,而需求分析往往是信息丢失最为严重的阶段。
DDD改变了需求分析的方式。他指导软件团队以统一语言建模作为指导思想,鼓励技术更加主动的理解业务。
落地到具体的实践,就是通过事件风暴会议。
# 问题和要求
微服务系统,面临的第一个难题是如何进行微服务拆分
“高内聚,低耦合”的标准
# 统一语言建模
- 需求分析最大的风险是 技术研发人员与业务人员似乎永远是站在对立面的,谁也不能说服对方。
- DDD强调的是技术主动去理解业务
- DDD对需求分析的基本思路是建立统一语言建模
- DDD建议的实践方式- 事件风暴会议
# 领域模型
- 领域模型是快速有效的沟通工具,对重要的对象和业务过程进行统一描述,从而帮助整个团队成员,而不仅仅是参会人员。
- 通过事件风暴,建立领域模型,并对子域进行划分,确定子域的基本定位,并建立子域内核心概念的结构化模型。这将用来指导后续的微服务拆分。
- 在子域内,细化实体和行为,识别子域中的重要角色和重要规则。
# 事件风暴会议
时机 : 该会议再需求之后,设计之初进行
主持人: 产品经理
参与方:业务需求方、领域专家、产品经理、项目经理、技术经理或者架构师
会议内容: 梳理业务的重要对象;初步领域划分;领域行为分析;会议输出整理;
# 问题
1、通过时间风暴会议形成的领域模型,要如何落地到微服务的设计?
2、在落地过程中还会遇到哪些设计和技术难题?如何将面向过程设计的业务流程转换成为面向对象的软件设计?
# 为微服务拆分提供指导思想
DDD提供了一种反常规的项目推进方案,即边设计,边实现。优先设计业务清晰的模块,不清晰的模块可以延后设计。这种推进方案比较激进,但是在DDD的视角下,也是一种可选的方案,这也满足微服务的短频快的要求。
DDD之所以能够和微服务很好的结合,除了限界上下文非常适合指导微服务拆分外,他也允许软件团队在开发过程中及时给出模型上的反馈。帮助软件团队在项目实施过程中,进一步的激励业务,使团队内的业务领域知识,能够由模糊逐渐变为清晰。
# 子系统拆分的问题
粗暴的微服务拆分(子把业务拆开,而用的表结构还是一套)往往会适得其反, 这种不像是微服务,更像是子系统拆分。
核心问题是,数据没有隔离。出现过其他子系统修改了主系统一张表的数据,连累主系统找半天,发现不是我改的,之后其他系统也开始找这个bug,把问题排查扩散到了全系统。
# 限界上下文
而限界上下文的指导,隔离/职责单一,内部数据对外是封闭的。具体手段就是只能有我提供的接口/API等等业务操作做数据的流转。
同时领域划分的界线是自顶而下的纵向业务拆分更能体现微服务的优势,所有都隔离了/解耦了。
# 具体设计
# 高内聚
“高内聚”要求微服务严格遵循“单一职责”原则。
从DDD的角度来看,就要求每个服务内封装一个完整的业务领域,从而达到各自变化唯一。
# 低耦合
“低耦合”要求每个微服务拥有集群架构内的唯一性。
从DDD的角度来看,就要求每个领域严格遵循限界上下文,封装好自己的领域知识与能力,从而达到隔离、独自演变的过程。
# 实施
基于顶层领域模型的领域划分,核心域优先设计、通用域放低一点、支撑域可以再放低一点。 先做骨架再做具体实现。
# 单体架构如何迁移成微服务
通过使用限界上下文,我们在对系统进行微服务改造时,能够相对平滑的过渡成为微服务拆分的指导方式。
先梳理业务,设计并建立业务领域模型、上下文边界, 做代码层面改造把领域之间关联的拆成调用其他领域仓库的聚合根如表关联查询、再service中的强耦合事务脚本,先上线一版确保业务稳定/修改拆分的bug。
再剥离代码拆成各自的子系统,再上线测试一版。
再把数据库、redis等中间件选择性的拆开,逐个拆分,回归测试。
题外话: 应为这些过程是慢慢的演化的,所以避免不了多次的回归测试。所以这里需要再第一次开始做好测试代码,以便回归测试。 同时这个没有大量的人力物力是做不来的,所以需要评估是否直接重构更好??
# 支持快速交付的中台
# 背景
SuperCell游戏公司 的快速组合+快速试错
阿里的 ”小前台+大中台”的战略规划
中台,就是将各个业务线中可以复用的一些功能抽取出来,剥离个性,提取共性,形成一些可复用的组件。
同时分为技术、业务、数据中台。
# 缺点
- 中台的价值很模糊
- 中台并不是总能提炼出共性需求
- 中台存在更多的变化
# DDD指导中台建设
中台是一个很大很宽泛的概念,对于中台的理解,要思考的问题实在太多太多。而要将DDD与中台进行一部分融合,但是不是完全的融合使用。
- 领域 -> 业务
- 核心域 -> 核心业务中台
- 支撑域 -> 技术中台
- 通用域 -> 数据中台
# 如何建设
从单体 -> 微服务 -> 平台化 -> 中台战略
要求:
- 技术栈统一
- 解决方案统一
- 运维与框架统一
- 部署与测试统一
- 上线方案统一
# 统一数据存储方案(中台)
难点 :
难点1:需要与多种数据源对接
难点2:需要与多种技术环境对接
方案细节扩展 :
支持更丰富的业务方技术场景
提供对多环境的统一兼容 (适配)
逐渐沉淀业务方的其他需求
如何实现:
一套以实体加注解的方式来描述业务需求的统一存储服务方案。这个中台方案只是作为一个载体,承载对于中台战略的思考。
注意:
基于DDD的思想,技术只要”刚刚好”的支撑业务,那就是好的方案,不要为了炫酷而滥用。
DDD的本质是面向变化进行设计,在整个DDD的战术篇和战略篇,这个思想是统一并且一致的。
# 架构变化之道总结
软件进化困难的根本原因在于,业务代码大量的依赖于底层的技术框架,形成了耦合。
所以决绝问题的根本方法,是要通过分层设计,进行解耦。之前讨论的四层架构,就是一种体现。
DDD是一种有形而无质的架构方法论。通过分层架构、整洁架构、清晰架构,来实现的。
DDD的本质是解耦,方法很多,架构思路是一致的。
本质的思路是对领域模型进行有效的保护,以减少业务变化对领域模型的影响。
下面几种种架构考虑的核心问题都是前端需求的变与领域模型的不变。
# 洋葱分层架构(推荐)
- 领域模型封装领域内的核心逻辑。
- 领域服务调用领域对象,形成业务规则。
- 应用服务通过对领域服务的编排,形成系统的业务流程。
- 用户接口层主要提高适配能力。
# 六边形架构
6种适配器,Web/App/DB/文件系统/缓存等等的适配器。
# 清晰架构
将所有中间件都带有适配接口层去做强制的解耦。
分层架构下,每个领域处理用户请求的顺序是相对固定的。
远程服务层、本地消息层、消息契约层、领域层、端口层、适配层。
# DDD视角下的单体架构与微服务架构
系统老化问题的根源不在于使用单体架构与微服务架构,而是在于没有守住限界上下文
当DDD与微服务结合后,可以随时将限界上下文之间的逻辑边界变成微服务之间的物理边界,可以随时将急速膨胀的领域抽取出来,形成另一个独立的微服务,这样就能不断将业务变更的系统复杂度进行分散,平摊,这样就能保证系统整体不再老化。
从DDD角度来看,微服务与单体架构是统一的,都是领域的不同组合方式。
菱形结构下的领域合作,类型流分析法,微服务架构下的领域能力调用,事件驱动下的领域能力调用。
后微服务时代,Martin Fowler大师提出的Monilith First(单体架构优先)
DDD定义的一整套规范,最为重要的就是将限界上下文的重要性凸显了出来。
对于DDD的理解并不局限于那些眼花缭乱的概念,只要能够守住限界上下文的边界,就是符合DDD风格的架构
# 自己的DDD
# 不足
DDD并不是万能的银弹,映射到具体的业务场景时,DDD的理论体系也需要由模糊到清晰。
DDD缺乏一个规范的过程指导
DDD没有万能的需求管理体系
DDD没有明确的领域建模方法
DDD需要团队有足够的知识储备
DDD学习成本挺大的
DDD的周边技术和理论的生态与MVC相比还有很大的差距
DDD是动态变化的
# 注意事项
- 理解业务的重要性
- 理解团队的重要性
- DDD的战术工具与战略工具同样重要
- 理解DDD的精髓,尽信书不如无书
DDD自2004年提出,直到微服务时代才开始火爆。不管未来DDD怎么样发展,他必然成为不可磨灭的经典。DDD拥有一整套完整的理论架构体系。对于企业,能够降低技术门槛,保证软件项目质量。对于程序员,DDD是一个适合所有技术人员,并且适合贯穿整个技术生涯的艺术品。
DDD各种理论工具的背后,是一条贯穿整个战略篇和战术篇的思想主线:面向变化进行架构设计。把握这个核心主线,助你走出自己的DDD之路。
# 思考
从软件生命周期来说DDD?
DDD的定位是那些变化的业务场景,或者说将会一直迭代的功能。 再需求分析阶段就得明确,功能的大致生命周期,而不是吹嘘的说要做10年,这样再设计阶段就能针对的选择ER图的设计和领域驱动的设计。 并且是否是领域模型,再设计之初都要定义好大致的边界,且一致遵守定义的边界。 对于ER图一定要保留好数据流转的设计且和代码实现要一一对应,且必须做设计再做代码实现。
对于DDD可以先做核心的领域,而且还可以只做主体的业务逻辑,再这些业务/应用层中的代码不做下层具体的实现。 做完了这些接口之后再和需求产品复审一遍,之后可以一边开发一边设计/谈论这些业务和领域问题,同时开发要主动理解需求,发现再主体层设计了不合理的边界时要向上反馈。 这也是根据主体业务流程复审的好处。 这样最终达到产品和开发都以达到良性循环。 但是这里说的下层的变化设计,而不是主体的逻辑变化设计
# 参考资料
https://blog.csdn.net/DBC_121/article/details/85930593
单一职责原则 : 应该仅有一个引起它变化的原因 依赖倒置原则 : 程序要依赖于抽象接口,不要依赖于具体实现。 https://www.cnblogs.com/tjxy/p/7495390.html
领域驱动设计在互联网业务开发中的实践 (opens new window)
里面有提到贫血模型失忆症 : **简单的业务系统采用这种贫血模型和过程化设计是没有问题的,**但在业务逻辑复杂了,业务逻辑、状态会散落到在大量方法中,原本的代码意图会渐渐不明确,我们将这种情况称为由贫血症引起的失忆症。