ddd领域驱动设计过程
导读
JavaWeb开发目前已经离不开spring框架,spring对象容器似乎总是暗示大家以单例的模式组织服务对象,大家都清楚单例要保证线程有两种情况,对象无状态,Java对象的状态就是实例成员变量,或者成员变量也是线程安全的,就像我们的service套service。在这种暗示下大部分情况下丧失了Java面向对象编程的优势,大家也逐渐习惯了面向过程编程,这样的编程方式的确在简单应用场景下很高效也容易被理解,但复杂的saas平台架构体系下,业务模型不断迭代升级,为了降低系统的复杂度,系统被逐步拆分,再过度到微服务体系并不断演进,各种相似需求堆积让系统的维护和扩展性变差。
架构师都面临一个挑战,是否有一种方法论可以指导工程师识别系统边界,对系统合理拆分,是否有一种编程范式可以让系统更具扩展性和维护性,让系统的生命周期拉长(系统一定会面临小范围的迭代重构,只能延长软件重构的周期,或者让重构变得更加容易)?ddd领域驱动设计所倡导的软件架构和"编程世界观",我觉得是非常值得借鉴的,
这是一个ddd论文学习笔记的精简版,帮做大家用最短的时间来探索著名建模专家Eric Evans所倡导的ddd编程理念。
ddd论文简述
ddd论文原文分5个章节,划一个重点,重点学习前2章节: 1.让模型开始工作 2.构建领域模型的模块。一个阐述软件早期如何落地ddd,同领域专家反复咀嚼业务知识形成统一语言/模型,一个阐述了如何利用早期形成的统一语言/模型落地软件实现
论文的第3、4章节,揭示了软件长生命周期的本质,需要持续跟随业务演进,重构来获得更强的生命力,甚至透过模型发掘未来趋势,对于架构师和技术管理者,深入阅读也大有裨益
第5章节,是一个结语。对论文细节感兴趣的可移步:领域驱动设计原文,强烈建议深入研读ddd原文,作者在文中有很多实践举例对整个设计思想进行佐证和说明,网上关于ddd的文章非常多,内容也大同小异,深入阅读原文之后再去看体会一定不一样
1.让模型开始工作【启航】
1.1. 提取关键领域知识
关键词:知识消化->知识丰富(提取隐藏概念)->深化模型(保留关键知识)
知识消化,原文Crunching Knowledge,直译为咀嚼知识,文中用大量的篇幅介绍,作者如何同集成电路的领域专家反复以下过程:提取关键知识-》建模-》和领域专家对齐确认-》提取隐藏概念持续改进模型,最终达成一致的过程。旨在阐明一点:研发人员跟领域专家一起反复咀嚼领域知识,尝试构建以获得接近真实业务的领域模型。
- 像领域/业务专家一样思考,挖掘和提取关键领域知识,形成统一语言,并使用统一语言/模型跟专家沟通对齐、持续改进模型。
- 技术人员避免在没有跟领域专家/业务方没有达成共识的情况下开展系统设计和开发,过早的引入技术复杂因素反而不利于挖掘业务关键知识
1.2. 统一沟通语言/模型
关键词:一个团队一种语言、大声/大胆的讨论模型
- 统一语言/模型,作者建议采用专家能迅速理解的形式建立早期模型,以模型作为日常沟通桥梁,反复提取领域关键知识并持续改进。
- 模型的选择,采用草图、图表法、uml模型,c4Model等等(如果领域专家不了解uml,则不推荐,构建统一模型后期虽然会被技术设计实现所使用,但最初不要用偏技术的表达方式设计模型,除非领域专家能迅速理解)
- 大声/大胆讨论建模【原文:Modeling Out Loud 大声的模仿】大胆用模型概念表述,在各种场合,跟团队的各个角色,大胆地使用同一模型/语言沟通讨论,各角色都能通过统一语言迅速获得自己的关注点
- 持续改进、深化模型,在这个过程中会不断提高跟领域专家的沟通效率,建立一个研发和业务都能迅速理解、高效沟通的统一语言/模型,并持续改进。 【注意不要一开始以研发落为目的建模,一方面建模和重构成本高,效率低,还会让领域专家不好理解】
1.3. 绑定模型与实现(为下一章节展开论述做铺垫)
关键词:软件建模工具、让骨头显露、动手模特
着重强调了两个方面,为下一章节的内容铺垫
- 建模范例和工具支持
为了使模型和设计的这种密切对应成为可能,在一个具有软件工具的建模范式中工作是必不可少的,该软件工具通过允许创建直接类似于范式中的概念来直接支持它面向对象编程功能强大,因为它基于建模范式并提供模型构造的实现。就程序员而言,对象确实存在于内存中,与其他对象有关联,被组织成类,并提供通过消息传递可用的行为。尽管许多开发人员从仅仅应用对象的技术能力来组织程序代码中受益,但当代码表达模型的概念时,对象设计的真正突破就来了,Java和许多其他工具允许创建对象和关系直接类似于概念对象模型(上面一段是原文直翻过来的,水平有限 见谅)。上面的大致意思是告诉大家,ddd的落地的两个要素必不可少:领域建模工具、面相对象开发语言【推荐java】- 从程序驱动到模型驱动
文中继续引用了印刷电路板的例子来说明问题,此处省略原文中的问题示例描述,总而言之这是一个复杂的领域问题,作者举例了工程师了两种做法:通过编程脚本/伪码的方式来表达 称之为机械的设计,通过模型驱动(文章使用了类图)来表达,显而易见通过模型来表达驱动更高效,不同角色更容易从模型中得到自己的关注点,同时基于模型也更容易调整重构- 让骨头显露【原文内容:let bons show】,这是非常有意思的一个概念,大致的意思是说,在产品设计和表现的时候尽量紧扣模型,平行的呈现核心模型的一些概念,否则核心领域模型就失去了意义。原文用了一个浏览器收藏夹例子进行例整,偏离模型的产品表达和设计会导致误解,有时候甚至导致错误。个人在项目上就时常遇到类似的事件,互联网公司的团队人员更迭频繁,沉淀的统一模型不一定会被每个人所熟知和理解,接手的产品很可能就会做出更核心模型相悖的产品设计,这时候需要跟产品就核心模型就行沟通对齐,如果核心模型更符合业务领域场景,那么需要纠正产品设计,如果领域模型有偏差,先纠正模型之后再按模型设计表现产品(大部分情况是前者)
- 动手模特【hander-model】,工程架构和代码实现跟模型不对应,很多情况下是编码人员没有参与前期业务建模过程,或者没有深入理解架构师的建模产物,或者建模的架构人员完全没有关心系统编码实现是否遵循架构模型。作者鼓励工程人员参与建模 或 架构建模者早期参与代码主体架构实现,并持续关注模型和系统的变化并持续小范围重构保持一致
2.如何落地构建领域模型层【重点】
这部分给出一些ddd落地开发,技术实现的具体指导方法论,我们先来看下常规的软件体系架构演变
-
常见的分层架构
image -
六变形架构
image.png
不管是哪一种架构,在ddd理念都倡导构构建接近真实业务的领域模型,构建防腐层去向上、下适配,个人认为六边形架构更接近ddd落地实现的终态
-
传统架构模式 和 ddd优劣分析【借鉴】
image.png
2.1. 隔离域
关键词:分层、核心域、通用域、支撑域
引用原文图
image.png
2.1.1 垂直分层,逐层依赖
传统的分层仍然适用,域层采用面向对象的方式,围绕关键域对象聚合业务算法,通过业务层或粘合层组织串联,摊薄业务层,增加可维护性
2.2.2 水平划分,尽量单向依赖
领域的核心思想就是问题拆分,来降低业务理解和系统实现的复杂度,只到人脑可以解决和维护的粒度。
通过识别分层、核心域、通用域、支撑域,将领域逐步细分,逐步缩小找到聚合根对象进行构建,紧密组织域场景数据结构 和 逻辑。
核心域、支撑域和通用域的主要目标是:通过领域划分,区分不同子域在公司内的不同功能属性和重要性,从而公司可对不同子域采取不同的资源投入和建设策略,其关注度也会不一样【通过模型迅速定位务重点】。
- 场景剖析
一个常用的分层模型,数据操作dao层来向下适配物理模型,解耦领域层对数据层依赖,合理的解耦应该使orm框架和db都可随时替换。business-logic层向上适配用户接口协议层,组装驱动domain领域层,构建一个防腐层来确保域模型层的稳定。web层构造用户接口协议,快速适应外层业务变化,灵活组装和暴露底层模型能力【图一右边的分层架构】。
领域层的构建 【举例场景说明】
* 资质id域:主体基本信息、主体资质证照合规信息、准入类目、准入评估信息。事件有:招商、准入、资质管理等
* 供货规则code域:供应商跟平台合作供货的管理单元,包含 供货范围、订货信息、供货目录(sku),该域为核心域,事件有:合同关联新建供货规则以及合同联动、订货信息维护、sku上新联动供货目录、采购订货、采购单履约、财务结算等
* 供货目录(sku):sku维度供货属性、城市价格/活动价、资质图片等。事件:sku上新、sku询价、合同签约类目范围联动等
有了具体也业务领域事件和统一模型,就可以定义出粒度合适的聚合根对象,将依赖的实体、值对象、紧密的领域服务处理 和行为领导组织起来,使用面向对象/充血的方式进行高内聚封装【注意控制粒度,一个聚合场景过大导致依赖复杂反而难以维护,可以按聚合根+二级事件进行二次拆分解耦】领域层聚合根的通信原则
根据java6大开发原则的"组合复用原则",我们优先通过适配层来进行组合聚合根来降低耦合,同领域内的也允许将一个聚合根注入到另一个聚合根的方式来组织逻辑(不推荐),对于跨领域通信,一般会使用事件总线或消息【例如:阿里开源ddd框架cola的实现,向evenBus发送DomainEventI】支撑域/通用域:业务线内的外部域的saas能力、平台通用能力,都以支撑域管理,向其他供应商业务域提供,如:短信、大象消息、审批流、poi门店域基础服务等
2.2 基于域模型的软件实现【充血模型】
关键词:对象划分与识别(实体对象、值对象、域对象)、聚合根、领域事件
2.2.1 对象识别/对象生命周期
2.2.2 聚合和聚合根、域对象
通过聚合根对象聚合一个有业务上下的场景或事件,来达到高内聚和可复用、可扩展的目的。聚合根也叫域对象,一般是名词命名,负载场景可以加动词事件后缀进行进一步拆分负责度。它作为紧密相关的实体、值对象、域服务处理的聚合管理者和领导者
- 设计原则
- 聚合根的设计方法:事件风暴,通过真实业务场景和事件代入,推演业务,验证聚合根合理性
- 高内聚原则,在一致性边界内建模真正的不变条件,聚合用来封装真正的不变性,而不是简单地将对象组合在一起。聚合内有一套不变的业务规则,各实体和值对象按照统一的业务规则运行,实现对象数据的一致性,边界之外的任何东西都与该聚合无关,这就是聚合能实现业务高内聚的原因。
- 低耦合原则,聚合力度适中,如果聚合根设计得过大,聚合会因为包含过多的实体,导致实体之间的管理过于复杂,需要考虑聚合根是否可再拆分。如果单个业务聚合根的场景确实比较复杂,个人的经验是可以聚合根结合具体的事件进行再拆分
- 紧密联系的单个领域内多聚合根可以组合,来达到一些场景复用的目的【这个存在争议,会让依赖扩散,不利于构造低耦合的系统】
- 夸领域聚合根避免直接组合依赖,应通过业务应用层或者夸微服务rpc调用来实现通信,避免跨聚合的数据库表关联
- 事务处理原则1,在边界之外使用最终一致性,聚合内数据强一致性,而聚合之间数据最终一致性。在一次事务中,最多只能更改一个聚合的状态
- 事务处理原则2,聚合根处理逻辑不包含事务,遵循最小依赖原则,通过business-logic层组织粘合并关联事务处理(业务层由于领域层的存在而摊薄,变得更容易向上适配业务变化和维护)
- 具体编程示例剖析
无规矩不成方圆,这里尝试定义一个基于ddd的简单工程结构和包结构规范【大型平台系统架构会根据业务复杂度拆分更细】
moudel模块 包名 说明 **-dao po
mapper(custome、generate)
dto
dao数据操作层(将数据持久操作和物理模型做高内聚,向上暴露面向业务场景的数据操作接口,存储介质和orm可被替代) **-service domain // 聚合根/域对象,领域模型层的体现
dto // 值对象
service // 业务服务对象(接口和实现分离)服务层,按领域边界拆分多个
(这个一般将api 和 impl也分开两个module,解决跨服务AB循环依赖问题)**-web dto(req、res) // 协议参数对象
web //rest协议接口
用户应用协议层 补充说明
- 实体对象Po结尾、值对象不做约束(dto、req、res)、域对象Domain结尾
- 尝试局部充血,逐步扩大ddd应用范围,在实践中不断加深对ddd编程范式结合业务的理解。基于ddd领域驱动的充血模型编程范式之所以很难被落地,因为它是一个业务问题,取决于你对业务的认知程度,而业务又是不断演变的,不推荐在整个业务去落地充血模型,因为对工程师将带来极大的挑战,甚至会影响效率。但是完全贫血将永远失去ddd高内聚低耦合带来的好处
- 示例程序【聚合根域对象如何组织逻辑】
市面上有很多基于ddd的开源框架,不借助较重的框架,我们是否可以体验下ddd编程范式带来的好处?这里尝试以原生代码的方式做一个简单的示例说明。
也不建议在系统中全局落地ddd,上面也分析过,全局落地ddd对人的要求和成本非常高,很难完全掌控,但是可以在局部去进行聚合充血,不断提升对ddd的理解和运用,最后逐步在项目中落地【最终可以借鉴一些开源ddd框架去落地】
域对象:
按照ddd鼓励的充血模型(也是面向对象所鼓励的)一般由四个部分组成:域模型事件所依赖的值/实体对象、紧密管理的同个领域的域对象、紧密相关的域服务对象(合理的拆分不应该超过5个)、域处理逻辑,如图:
截屏2022-02-13 下午5.34.55.png
business-logic层的service对象:
域对象聚合了关键业务算法,service的主体逻辑更加清晰,只负责组织串联,如图:
image.png
延伸考虑
- 围绕聚合根封装核心业务处理,business-logic层无疑会被摊薄,主体逻辑更清晰易于维护,同时聚合根采用充血模式组织的逻辑形成一个独立可复用、单独测试的模块,更便于维护。
- 尤其针对复杂业务场景,可采用合理的聚合根拆分降低问题复杂度,提高局部稳定性从而提高系统整体稳定性。单个聚合特别复杂时,我个人的经验是可以借助事件的维度将域对象再拆,来简化问题
- 对于数据持久化封装在域模型层 还是 business-logic层的问题,个人持开放态度,都可以。遵循高内聚低耦合原则,一定要在mapper层之上封装dao层来隔离域层对orm框架的依赖
2.2.3 领域事件
- 微服务/域内的领域事件
业务领域内的事件,一旦按照第一章节所阐述的方式构建统一模型/语言,一般比较容易识别,不做过多阐述
- 微服务/跨域的领域事件
跨领域边界的事件处理,一般遵循最终一致性,引入消息或事件总线来达到解耦的目的,遵循最小依赖原则
- 供应商场景示例剖析
域内事件,准入、汰换、履约 等等
域外事件,商品侧上新指定默认供货code、sku订货获取供货code订货信息等
延伸考虑
- 域外事件,设计对外交互方式,强依赖场景优先采用rpc调用方式,避免异步引入复杂性,考虑异常情况的补偿,遵循最终一致原则。弱依赖场景跨域多方调用,可采用消息或事件总线模式解耦
第3、4部分待补充。。。。
3. 重构以获得更深入的洞察力
关键词:突破、从模型中挖掘隐形概念、持续改进及扩展
3.1. 突破
关键词:
3.2. 挖掘隐形概念
关键词:
3.3 柔顺设计/让设计具备延续扩展性
关键词:
4. 战略设计
一旦有了核心域模型,业务和架构能够结合市场趋势,透过模型更容易洞悉一些未来趋势,进行适当的前瞻性的模型演变和设计,这部分不做深入阐述,待完善。。。
引用
- 领域驱动设计原文:https://msstest.sankuai.com/v1/mss_156341e12eb248878e532dd820706c40/mall-data-init/ddd-en.compressed.pdf
- ddd开源框架:阿里开源cola脚手架 https://start.aliyun.com/bootstrap.html?spm=a2ck6.17690074.0.0.503c2e7dELPJiH