总篇117篇 2021年第8篇
背景
DDD是一套完善的系统设计方法,可以帮助我们在系统设计的过程中缕清思路,规范流程,降低系统建设的复杂度,同时DDD的领域建模过程也是团队成员之间形成系统通用语言、建立良好沟通机制的过程。
在电商中台研发团队内部,很早之前就对DDD有过分享和讨论,近期我们在做购车发票总库的迁移工作,经过分析,借此项目做一次DDD的落地实践,在时间和人员投入上都比较适宜。本篇文章就对我们的整个实践过程做一个总结,与大家一起学习和提升。
DDD基本概念及原理
我们先来看一张DDD的经典知识体系结构图:
DDD的核心就是将问题范围限定在特定的边界内,在这个特定的边界内建立领域模型,进而解决相应的业务问题。领域是用来限定业务边界和范围,自然它的边界就或大或小,当一个领域过大的时候可以将其进一步拆分为子领域。而子领域按照重要性以及关注度,可以分类为:核心子域、支撑子域、通用子域,这三类子域随着时间的推移或者业务的变化,其角色也可能发生转换,我们的重点应该放在核心子域建设上。
明确了领域(子域)其实也就定义好了边界,即:限界上下文,为了确保在限界上下文内,所有人对概念的理解一致,不产生歧义,就需要对限界上下文中的每一个事件、动作、实体等对象都要形成通用语言,统一命名。
实体和值对象是领域模型中非常重要的两个基本概念,一起构成了领域模型中最核心的领域逻辑。实体具有唯一标识以及本身的生命周期,当实体的属性(值)发生变化时,实体还是原来的那个实体,而值对象更多的是属性(值)的描述,当属性(值)发生变化时,值对象就不在是原来的值对象了。实体在代码形态上,通常采用充血模型实现,与实体相关的所有业务逻辑都在该实体类中实现,实体自身保障事务的原子性,当需要多个实体共同实现某个逻辑时,则会在领域服务层实现跨多个实体的组合封装。
聚合根也是实体,它用来管理聚合内所有的实体以及值对象,一个聚合内包含了聚合根、实体、值对象、领域服务等,它们按照聚合的业务规则完成聚合内的领域逻辑,一个聚合只有一个聚合根,聚合和聚合之间只能通过聚合根的唯一ID标识进行引用,对于跨聚合的服务调用,往往是在应用服务层组合编排他们之间的调用关系,实现相互之间的协同。在DDD中强调一次事务最多只修改一个聚合的数据,因此在聚合内部会采用数据强一致性,而聚合之间则采用数据最终一致性的方式实现数据一致性。
通常在大部分领域模型中,有70%的聚合通常只有一个实体,即聚合根,该实体内部没有包含其他实体,只包含一些值对象;另外30%的聚合中,基本上也只包含两到三个实体。这意味着大部分的聚合都只是一个实体,该实体同时也是聚合根。
领域事件是领域模型中非常重要的一环,一个个领域事件可以串联起完整的业务逻辑闭环,领域事件采用事件驱动的方式用来解耦服务与服务之间的依赖关系,在服务内部可以采用事件总线的方式实现逻辑的串联,服务与服务之间可以采用消息队列的方式实现服务间的解耦和数据最终一致性。
工厂和仓储
此工厂非彼设计模式的工厂,领域驱动的工厂强调封装了所有创建对象的复杂操作过程。
仓储就是持久化机制,比如:Dao层、Cache层等。
DDD实践过程
在整个发票总库DDD的实践过程中,我们也是按照战略设计和战术设计两个阶段进行:业务抽象的过程就是业务领域建模的过程,对应DDD的战略设计,而系统架构设计并落地的过程则对应DDD的战术设计。
在战略设计阶段,因为发票总库的业务逻辑相对比较清晰,也有助于我们采用事件风暴的方式,快速的找出了聚合、领域对象、领域类型,为构建领域模型奠定了基础,以下是我们梳理完成的发票域的对象清单:
从上图中可以看到,整个发票域我们只定义了发票这个聚合,同时它也是聚合根,在发票领域服务内部,还包含了排重规则、发票日志两个实体以及发票审核状态、业务类型、入库类型、应用唯一标识四个值对象,结合发票入库这个领域事件以及发票总库的业务逻辑,在发票领域服务内对外提供了发票入库、发票排重等方法,对于数据存储,则需要统一抽象了仓储接口(不受限于具体的数据存储介质)。
在战术设计阶段,我们完成了分层设计、规范设计等事项,在分层设计中我们遵循了DDD的四层分层模型,如下所示:
用户接口层主要实现后端服务与前端调用入口的接口数据适配和转换;应用层主要用来协调领域层多个聚合之间的服务组合和编排,以及负责领域事件的订阅和发布等职责;领域层是领域模型的核心,实现领域模型的核心业务逻辑,其内部逻辑相对稳定,不会受外界变化(如:底层数据存储介质的变换、通讯方式的变化等)的影响,基础层主要用来提供通用的基础工具类或基础服务。
结合上述四层模型,在实际代码工程的构建过程中,我们也是参考了COLA架构进行构建,具体如下:
Invoice-ddd-starter
作为服务的启动模块(SpringBootApplication)以及用户接口层(Controller),定义了所有对外发布的接口以及DTO到领域层实体类的转换。
Invoice-ddd-app
应用层模块,提供了发票领域服务的方法定义及实现,同时作为事件总线(EventBus)的接收层,提供了多个发票领域事件的Handler处理器,对领域事件做下一步处理,比如:发送MQ消息等。
Invoice-ddd-domain
领域层模块,定义了发票领域的实体、值对象、发票事件、发票领域服务以及仓储层接口,同时包含了创建领域实体的工厂类。在发票领域服务类中,通过发票实体以及发票规则实体的协作,完成发票入库逻辑,当发票入库事件发生后,会通过事件总线(EventBus)将该领域事件发布出去。而在发票的聚合根实体内,则通过事务控制了发票实体以及发票日志实体的数据存储。
Invoice-ddd-infrastructure
基础设施层模块,提供了发票仓储接口、发票日志仓储接口的实现类以及从领域实体类到仓储层PO对象类的转换,同时提供了事件总线(EventBus)以及加解密等通用工具类。
Invoice-ddd-client
客户端模块,提供了API定义以及DTO、VO等对象的定义。
规范设计
类名约定
类名应该是自明的,也就是看到类名就知道里面是干了什么事,这也就反向要求我们的类也必须是单一职责的。为了避免团队人员对dto、bo、vo、po等这些pojo对象的二义性理解,我们制定了类名规范。
总结
经过上述过程,我们按照DDD的方式完成了购车发票总库的迁移工作,目前系统已经顺利上线,因为大家都是第一次做DDD的落地实践,在整个过程中会发现从理论到实践难免会出现一些理解的偏差和分歧,比如:
跨多个实体的操作是在聚合根内完成还是在领域服务内完成?
领域服务内事务的范围究竟要控制到什么程度?
DDD的适用范围在哪?
业务逻辑应该放在领域实体里还是领域服务里?
在实践过程中出现这些问题其实都很正常,每一个问题都不一定会有标准答案,比如上述没有提到的问题:一个领域对象究竟是定义为实体还是值对象等,即便是上述我们有答案的问题也仅仅是我们的一些见解,仁者见仁,智者见智,面对这些问题,我们一方面需要摆脱掉之前的思维定式,再次深入的理解理论定义,一方面也需要借鉴参考一些成熟代码案例,找到适合我们自身项目的结论。
按照DDD的模式进行项目的落地实施,相比与传统开发模式也会有一些质的提升,主要表现在以下几方面:
系统边界更加清晰,利于后续功能的延展
DDD首先强调的是确定领域边界,领域边界确定后所有设计均会在同一个领域范围内进行开展,清晰的领域边界对应了实际系统落地过程中合理的系统边界,这对于后续系统内高内聚功能的延展起到了积极的帮助作用。
借助通用语言可以快速共识领域知识体系
在传统的开发模式中,不同环节的人员经常会出现对于同一对象的叫法不一致、定义不一致的现象,这在一定程度上也造成了大家对于系统理解的偏差,而在DDD模式中,提出了通用语言的概念,参与其中的所有成员在系统生命周期内均采用简单清晰明了的通用语言进行交流,通用语言成为团队内外部系统交流的统一语言,既减少了信息的失真, 又确保了大家在同一领域知识体系内交流的便利性、理解的一致性。
开发模式和框架更加健壮
传统开发模式采用贫血模型,将属性与业务逻辑彻底分离,通过get / set方法改变对象属性,对象属性可以被随意的修改,在面对简单的业务场景,贫血模型还可以应对,但是在面对复杂的业务场景时,传统开发模式的贫血模型维护成本高的弊端就会被暴露。与贫血模型相对应的,DDD的开发模式强调采用充血模型,将属性和行为聚合到一个实体内,业务逻辑更加便于维护和管理,也更加符合面向对象的编程逻辑,这也是基于充血模型的 DDD 开发模式越来越被人提倡的原因。
DDD所涵盖的范围其实是比较广泛的,在具体的实践过程中是需要结合实际的项目场景和团队成员的实际情况等多方面因素,不断的摸索,经过长时间的学习和积累,逐步的在团队内部建立DDD的方法论以及对应的开发模式、技术架构。
以上就是我们的DDD实践总结,欢迎对此感兴趣的同学与我们交流,共同进步。
作者简介
前往“发现”-“看一看”浏览“朋友在看”