我在公司用了两年的DDD,对支付业务、结算业务、资金业务进行建模。我得到过很多赞美,也面临过很多质疑。总的来说,我还是能收获不少的,对团队成员理解业务有很大的帮助。近半年一直在研究DDD的实战,现在已经取得了阶段性的成果,迫不及待想和大家分享一下我的落地心得。DDD分为战略设计和战术设计。一般来说,领域建模属于战略层,而DDD项目的实施属于战术层。两者是否结合使用,视实际情况而定。例如,传统的MVC架构也可以使用DDD进行领域建模。DDDArchitecture最好先做DDD领域建模。新上线的一个微服务——内部交易中心,我们采用DDD架构来实现。希望大家看完之后,能够有所启发。1.工程架构分层理论在项目实施之前,我们有必要了解主流的工程架构或架构思想。如果你对这些理论有所了解,可以跳到下一部分。1.经典的DDD四层架构在这种架构中,上层模块可以调用下层模块,反之亦然。即:接口——>应用|域名|基础设施应用——>域|infrastructuredomain——>infrastructurelayered角色:用户界面层/表现层:负责向用户展示和解释用户命令应用层:定义软件要完成的任务,并命令和协调领域对象执行不同的操作。该层不包括业务领域知识领域层/模型层:系统的核心,负责表达业务概念、业务状态信息和业务规则。即包含了该领域(问题领域)所有的复杂业务知识抽象和规则定义。该层的主要重点是领域对象的分析。基础设施层可以从实体、值对象、聚合(聚合根)、领域服务、领域事件、仓储、工厂等开始:一是为领域模型提供持久化机制,只有在软件运行时才需要规划需要持久能力;二是为其他层提供通用的技术支持能力,如消息通信的实现、通用工具、配置等;Bob大叔在2012年提出的一个架构模型,顾名思义就是让架构更简洁。依赖规则:使用一组同心圆来表示软件的不同区域。一般来说,你越深入,你的软件水平就越高。外圈是战术和执行机制,内圈是核心原则。这个规则规定软件模块只能依赖内部,内部部分对外部模块一无所知,即内部不依赖外部,外部依赖内部。同样,外圈使用的数据格式不应该在内圈使用,特别是如果这些数据格式是由外圈的框架生成的。这样做的最大好处是当系统的外部模块需要改变时(例如,更换现有的过时的数据库系统),系统内部模块不需要做任何改变。3、六边形架构六边形架构(HexagonalArchitecture)又称端口适配器模式,由AlistairCockburn于2005年提出。六边形架构将系统分为内部(internalhexagon)和外部。内部代表应用的业务逻辑,外部代表应用的驱动逻辑、基础设施或其他应用。它通过内部端口与外部系统通信。端口代表某种协议,并在API中呈现。一个端口可能对应多个外部系统,不同的外部系统需要使用不同的适配器,适配器负责协议转换。这使得应用程序可以由用户、程序、自动化测试、批处理脚本以一致的方式驱动,并且可以在与实际运行的设备和数据库隔离的情况下进行开发和测试。4.菱形架构作用于限界上下文的菱形对称架构从领域驱动设计的分层架构和六边形架构中吸取养分,通过它们的融合形成以领域为轴的内外分层对称结构。内部领域模型主要基于领域层,外部网关层根据方向分为北向网关和南向网关。通过这个架构,可以清楚的说明整个限界上下文的组成:北向网关的远程网关,北向网关的本地网关,领域层的领域模型,南向网关的端口抽象,适配器南向网关的实现,以领域模型为核心。对称发散在边界内形成清晰的逻辑层次,前端UI不包含在限界上下文的边界内。各组成元素之间的协作关系呈现出清晰直观的南北呼应关系。5.CQRSCQRS(CommandQueryResponsibilitySegregation)意为命令查询责任分离,是一种与领域驱动设计(DDD)和事件溯源相关的架构模式。GregYoung在2010年创造了这个术语,CQRS的内容基于BertrandMeyer的CQS设计模式。CQRS架构将写入和读取分开,它提出了单独的API,一个专用于更改应用程序状态的命令路由,另一个专用于返回有关应用程序状态信息的查询路由。2.工程架构的分层设计是基于每个架构都有自己的优点和缺点。我们结合公司现状,取长补短,整合出一套适合自己的架构。以经典的DDD四层架构为骨架,以其他优秀的架构思想为指导,将CQRS命令/查询职责分离应用于DDD应用层,处理复杂的操作/复杂的查询。干净的架构应用于DDD领域层和基础设施层,接口和实现被拆解。转到不同的层次,将技术代码与业务代码分开。菱形建筑指引着我们。内部领域模型主要基于领域层,两种方式向南北发散——北向网关(领域层之上)提供本地网关(如Controller、MQListener)和远程网关(如API包裹);南向网关(领域层以下)负责端口抽象(如仓库接口)和适配器实现(如对外API封装实现)。公司的Base框架将基本的CRUD接口封装在dal包中,应用于数据访问层,作为领域层和基础设施层的粘合剂,简化了链接。当然,任何事物都有它的两个方面。整合了各种框架之后,也有它的优缺点。优点:通过分离业务和技术代码,有利于业务的迭代升级和维护;业务驱动而非技术/数据驱动,通过编写代码可以积累一定的业务知识;对领域知识和技术知识进行分类,以提高代码的可重用性。缺点:从业者业务分析能力高,经典MVC架构转型难度大;层次很多,在写代码之前要考虑好逻辑应该写在哪一层;规则较多,不如MVC架构灵活,不适合简单的业务系统:学习和迁移成本比较高,需要对DDD有更好的理解,设计时间较长(基金组已经实践DDD领域建模2年)。3、项目代码构建案例看代码之前,我们先来看领域建模:通过领域模型的分析,将内部交易中心划分为五个模块:内部转账、规则中心、内部存储、内部销售、和内部采购。DDD对应的每个模块都是一个聚合,所有的聚合组成一个DDD有界上下文(内部事务上下文)。上一篇文章提到过,限界上下文是我们划分微服务的重要依据。下面我们结合DDD架构图和领域建模,看看工程代码应该怎么放。基于Maven的DDD项目,我们将顶层结构按照api和service分为两个模块。api包的作用:api包的定位是跨服务的顶层契约。服务包的所有层都可以依赖api包。使用api类,feign层不划分业务,api包只定义契约不写业务逻辑,避免了业务逻辑变化导致的api包升级服务包的作用:service包是项目的顶层实现,DDD四层架构体现在service包中,应用程序入口与DDD的四层在同一个目录下。此外,还有一种主流的服务包模块划分方式——直接将服务包的api、application、domain、infrastructure作为四个独立的模块。优点是可以通过pom依赖的方式来限制层与层之间的依赖关系,开发者可以在编码阶段发现依赖问题并及时改正,但缺点也很明显——不够灵活,项目会变重.1.接入层(api)接入层也叫用户接入层。主流是以interface或者api命名的。基于包默认按字母排序的原因,我推荐使用“api”来命名接入层,但要注意。服务包的api层和api包是不一样的。接入层是一个很薄的层,负责直接连接前端请求或feign实现(门面中的Controller)、数据转换(汇编器)、输入/输出参数等契约类(请求/响应)在顶层API中统一定义了包控制器,负责对数据进行预校验,具体业务逻辑交给应用服务或领域服务实现,可以直接调用应用服务方法或领域方法。业务划分在接入层不明显,更多是基于前端模块划分。controller,而且当业务复杂的时候,肯定是跨域的,所以就没有再细分门面下的业务包组装器。数据转换负责处理复杂的数据转换。简单的数据转换可以显式调用工具类的转换方法2.应用层(application)应用层的主要功能是业务编排、转发、校验等,处理交叉聚合和领域事件逻辑。复杂的操作/复杂的查询也反映在这一层(CQRS)。应用服务AppService是一个简单的逻辑封装。接入层不能直接调用领域层获取结果。它可以在这一层安排封装聚合方法。应用层可以依赖领域层,但不依赖访问层,所以传入参数到应用层要么是基本类型,要么在访问层汇编器中进行一层转换,要么输入输出参数是在api包中定义。事件一般是跨聚合或者跨服务的,所以在应用层定义事件,在应用层处理事件发布/订阅访问层可以直接调用领域层,不需要经过应用层3、领域层(domain),或者说模型层,系统的核心,负责表达业务概念、业务状态信息和业务规则,包括领域内所有复杂的业务知识抽象和规则定义。领域层只表达业务,不写技术代码,不依赖业务中的其他层。域聚合以业务命名。聚合包含聚合下的所有模型(DO对象)、仓库接口、领域服务。领域模型是领域聚合。下面的业务核心模型,以XxxDO命名,依然采用贫血模型,只包括少量原子操作,不包括跨模型数据处理、持久化操作等,仓库以repository命名。领域层只定义仓库接口,不写仓库实现领域。工厂工厂在设计模式上不同于工厂模式。领域工厂主要负责领域对象的复杂构建,如领域对象生成、属性填充等,由于交叉聚合的存在,工厂包不在聚合中,处于同一层级作为域聚合的外部API接口和外部框架代码封装在一个浅层,放在外部聚合包下,接口以ExXxxService命名。实现类还在基础层,起到防腐接口的作用。因为领域建模最终体现在领域层,所以在我们建模的时候,需要考虑领域层的代码怎么写。领域建模只是在核心属性和核心行为的聚合中表达了跨多个模型的复杂业务逻辑。领域服务中领域模型写的方法只写原子操作,不包括CRUD持久化操作。难点:无法实现“所建即所得”的模式。复杂的代码无法通过领域模型的简单方法来表达完整的模型。模型只能表达核心业务行为。所谓充血模型,在实现的时候可能更多的是拆分为领域工厂和领域服务。,应用服务中的实现4.基础设施层(infrastructure)基础设施层作为项目的基础设施,编写与业务无关的代码,比如技术框架,工具类,还有一个重要的功能,就是写仓库类的实现,对外服务的实现类。基础设施层的仓库(repository)实现领域层定义的仓库接口,数据访问层(dao)也定义在仓库下,实体中定义数据库实体(PO对象),命名为XxxPO或XxxEntity,遵循companyframework的命名方式,使用XxxEntityparam是一个特殊的层,一般定义查询数据库的参数。基于公司的Base框架,repository在定义接口时依赖param对象。domain层不应该依赖infrastructure层(DIP原则)是有道理的,但是Param和PO对象密切相关,所以param对象基于CleanArchitectural原则放在infrastructure层,其他框架代码,工具类和配置类放在基础设施层。业务代码与技术代码分离后,万一技术代码升级,业务代码需要尽可能少改动。总结以下重点:领域层是业务的核心层,聚合之间需要明确划分边界,而接入层和应用层涉及交叉聚合,基础设施层侧重于存储实现和技术框架,所以我们只在领域层划分业务包,相应的领域建模根据聚合划分边界,定义领域模型的仓库接口。在领域工厂(模型构建)、领域服务(跨模型)或应用服务(交叉聚合、事件)中使用接口来分离业务代码和技术代码。业务迭代时,只需修改领域层和基础设施层的仓库实现即可;技术框架升级时,修改基础设施层即可,以免再次修改业务代码,降低错误解耦的成本。在写代码之前,我们需要考虑清楚不同的代码应该写在什么地方。结合前人优秀的工程架构思想和公司目前的技术架构,可以集成一套灵活的适合自己的DDD。我们不能复制它,更不用说为了DDD而进行DDD了。.四、难点分析1、用充血模型还是贫血模型?其实除了常见的充血模型和贫血模型外,还有不太常用的失血模型和血肿模型。区别如下:失血模型:纯数据类,只包含Getter/Setter,一般没有这样的设计贫血模型:包含模型的Attributes,Getter/Setter,非持久化的原子域逻辑。持久化逻辑放在业务层(比如Service类)。充血模型:比贫血模型多了持久化操作和大部分业务逻辑,在实例化的时候会得到很多关联模型,不一定需要。血腥模型:只有两层领域对象和DAO。事务在领域逻辑中的封装是基于现有的Spring框架和个人以往的代码编写经验。在代码实现层面,依然采用贫血模型进行对比。合适的。2.放应用服务还是域服务?应用服务在应用层,领域服务在领域层。我怎么知道把业务代码放在哪里?应用服务的作用:负责表现层和领域层的协调,协调业务对象执行特定的应用任务(编排业务),代码逻辑相对灵活,易于编排大粒度的操作,事务管理在此处理领域服务的作用:负责表达业务概念、业务状态信息、业务规则。它是业务软件的核心,放置了相对原子的核心代码。具有很强的封装性和复用性。操作细粒度,不管理事务,领域模型应该是无意识的其实难点在于业务代码的识别,考验我们对业务的理解和思考。如果我们能够清楚地预测到未来会有明显的变化,那么在设计之初就应该设计得更加灵活;对变化的把握不是明确的,也不是不确定的,只要满足当前的业务需求即可。我们无法避免过度设计或设计不足,但如果结构合理,代码清晰,修改成本不会特别高。建议开发者尽可能与领域专家(业务人员或产品经理)交流,把握代码未来的发展方向。3、特殊代码如何分类?除了常规的简单业务代码外,还涉及到将复杂的业务代码拆分成不同的类别,最典型的就是设计模式的使用。工厂模式:根据不同的条件生成相应的对象,普通领域工厂,领域服务,应用服务策略模式:根据不同的条件执行相应的逻辑,定义一个策略接口和多个策略实现,普通领域服务,应用服务中的观察者模式:不使用发布/订阅模型,可以使用基础设施层的SpringEvent解耦责任模型的代码链:将复杂的业务逻辑拆分到各个责任链类中执行,公共领域服务,应用服务原则上,其中就是核心逻辑在哪一层Detach层,避免代码散落各处。五、一些体会DDD领域建模的三大步骤:划分边界、统一语言、组织模型。DDD项目的实施主要有四大步骤:整合框架思路、确定划分思路、模型代码映射、特殊代码分类。以上,从DDD领域建模到DDD项目实施已经完成,欢迎大家一起讨论。如果需要DDD项目的demo,也可以联系我。相信80%的技术面试官都会对DDD感兴趣。如果你也掌握了DDD,那么你就又掌握了一种面向人民币的编程。
