在公司对领取业务、结算业务、资金业务应用 DDD 进行领域建模的两年,失去了许多好评,也面对过不少质疑,总体来说还是能播种不少,这对团队成员了解业务起着很大作用。近半年始终在钻研 DDD 的落地实战,现在已修得阶段性成绩,急不可待与大家分享我的落地教训。
DDD 分为 策略设计 与战术设计。一般来说,领域建模是属于策略层的,而 DDD 工程落地是属于战术层的,两者是否联合应用,视理论状况而定,比方传统的 MVC 架构也能应用 DDD 进行领域建模,DDD 架构最好是先做 DDD 领域建模。
最新上线的一个微服务——外部交易中心,咱们应用了 DDD 架构来落地,心愿看完对大家有启发。
工程架构分层实践
在工程落地之前,咱们有必要先理解下支流的工程架构或架构思维都有哪些,对这些实践有所理解的,也能够间接跳过看下一个局部。
1、经典 DDD 四层架构
在该架构中,下层模块能够调用上层模块,反之不行。即:
- Interface ——> application | domain | infrastructure
- application ——> domain | infrastructure
- domain ——> infrastructure
分层作用:
- 用户界面层 / 体现层:负责向用户显示解释用户命令
- 应用层:定义软件要实现的工作,并且指挥协调畛域对象进行不同的操作。该层不蕴含业务畛域常识
- 畛域层 / 模型层:零碎的外围,负责表白业务概念,业务状态信息以及业务规定。即蕴含了该畛域(问题域)所有简单的业务知识形象和规定定义。该层次要精力要放在畛域对象剖析上,能够从实体,值对象,聚合(聚合根),畛域服务,畛域事件,仓储,工厂等方面动手
- 基础设施层:一是为畛域模型提供长久化机制,当软件须要长久化能力时候才须要进行布局;二是对其余层提供通用的技术支持能力,如音讯通信,通用工具,配置等的实现;
2、整洁架构思维
整洁架构 (Clean Architecture) 是由 Bob 大叔在 2012 年提出的一个架构模型,顾名思义,是为了使架构更简洁。
依赖规定:用一组同心圆来示意软件的不同畛域。一般来说,越深刻代表你的软件档次越高。外圆是战术是实现机制,内圆的是外围准则。
这条规定规定软件模块只能向内依赖,而外面的局部对里面的模块无所不知,也就是外部不依赖内部,而内部依赖外部。同样,在里面圈中应用的数据格式不应被内圈中应用,特地是如果这些数据格式是由里面一圈的框架生成的。
这样做的最大益处是当零碎的内部模块不得不扭转时(比方,替换已有的过期的数据库系统),零碎的内层模块不须要做任何扭转。
3、六边形架构
六边形架构(Hexagonal Architecture),又叫做端口适配器模式,是由 Alistair Cockburn 在 2005 年提出的。
六边形架构将零碎分为外部(外部六边形)和内部,外部代表了利用的业务逻辑,内部代表利用的驱动逻辑、基础设施或其余利用。外部通过端口和内部零碎通信,端口代表了肯定协定,以 API 出现。
一个端口可能对应多个内部零碎,不同的内部零碎须要应用不同的适配器,适配器负责对协定进行转换。这样就使得应用程序可能以统一的形式被用户、程序、自动化测试、批处理脚本所驱动,并且,能够在与理论运行的设施和数据库相隔离的状况下开发和测试。
4、菱形架构
作用于限界上下文的菱形对称架构从畛域驱动设计分层架构与六边形架构中吸取了养分,通过对它们的交融造成了以畛域为轴心的内外分层对称构造。
外部以畛域层的畛域模型为主,内部的网关层则依据方向划分为 北向网关 与南向网关。通过该架构,可清晰阐明整个限界上下文的组成:
- 北向网关的近程网关
- 北向网关的本地网关
- 畛域层的畛域模型
- 南向网关的端口形象
- 南向网关的适配器实现
限界上下文以畛域模型为外围向南北方向对称发散,从而在边界内造成清晰的逻辑档次,前端 UI 并未蕴含在限界上下文的边界之内。每个组成元素之间的协作关系体现了清晰直观的自北向南的调用关系。
5、CQRS
CQRS(Command Query Responsibility Segregation)意为命令查问职责拆散,它是一种与畛域驱动设计 (DDD) 和事件溯源相干的架构模式。Greg Young 在 2010 年发明了这个术语,CQRS 的内容基于 Bertrand Meyer 的 CQS 设计模式。
CQRS 架构将写入和读取离开,它提出了独自的 API,一个专用于更改应用程序状态的命令路由,另一个专用于返回无关应用程序状态信息的查问路由。
工程架构分层设计
基于各个架构有其本人的优缺点,咱们联合公司的现状,取其长避其短,交融一套适宜本人的架构。
- 以经典 DDD 四层架构为骨架,其余优良架构思维作领导
- CQRS 命令 / 查问职责拆散,利用到 DDD 应用层,解决简单操作 / 简单查问
- 整洁架构利用到 DDD 畛域层与基础设施层,接口与实现拆到不同层,把技术代码与业务代码拆散
- 菱形架构领导咱们,外部以畛域层的畛域模型为主,向南北两个办法发散——北向网关(畛域层以上)提供本地网关(如 Controller、MQListener)与近程网关(如 API 包);南向网关(畛域层以下)负责端口形象(如仓库接口)与适配器实现(如内部 API 封装实现)
- 公司的 Base 框架在 dal 包封装了根底 CRUD 接口,利用到数据拜访层内,作为畛域层与基础设施层的粘合剂,简化链接
当然,任何事物有其两面性,交融各个框架后,也有其优缺点——
长处:通过拆散业务与技术代码,有利于业务迭代降级保护;业务驱动而非技术 / 数据驱动,通过写代码就能积攒肯定的业务知识;将畛域常识和技术常识分类,从而进步代码的可重用性。
毛病:对从业人员业务剖析能力较高,难以从经典 MVC 架构转变过去;层级较多,写代码前需思考分明逻辑应该写在哪一层;规定较多,没有 MVC 架构灵便,不适用于简略业务零碎;学习老本与转移老本比拟高,须要对 DDD 有更好的了解和更长的设计工夫(资金组践行 DDD 领域建模 2 年)。
工程代码构建案例
看代码之前咱们先看下领域建模:
通过畛域模型剖析,外部交易中心分为 外部调货、规定核心、外部出入库、外部销售、外部洽购 这五大模块,每一个模块对应 DDD 就是一个聚合,所有聚合造成一个 DDD 的限界上下文(外部交易上下文),之前的文章提到,限界上下文就是咱们划分微服务的一个重要依据。
接下来,咱们联合 DDD 架构图与领域建模,看看工程代码应该怎么放。
基于 Maven 的 DDD 工程,顶层构造咱们按 api、service 划分为两个 module。
api 包的作用:
- api 包的定位是跨服务的顶层契约,service 包所有层都能够依赖 api 包
- api 包定义了对外透出的枚举 / 常量、入参、出参、API 接口等,为了方便使用 api 类,feign 层不作业务划分
- api 包只定义契约不写业务逻辑,防止因业务逻辑变更引发的 api 包降级
service 包的作用:
- service 包是工程的顶层实现,DDD 四层架构在 service 包体现
- Application 程序入口与 DDD 的四层处于同一目录
此外,针对 service 包还有另一种支流的 module 划分形式——间接把 service 包的 api、application、domain、infrastructure 作为四个独立的 module,长处是能通过 pom 依赖的形式来限度层与层之间的依赖,开发人员能在编码阶段发现依赖问题及时修改,但毛病也显著——不够灵便,工程也会变得较重。
1、接入层(api)
接入层又叫用户接入层,支流用 interface 或 api 命名,基于包默认按字母排序的起因,我倡议应用“api”来命名接入层,但要留神,service 包的 api 层与 api 包是不同的作用。
- 接入层是很薄的一层,负责间接对接前端申请或 feign 实现(facade 里的 Controller)、数据转换(assembler),入参 / 出参等契约类(request/response)对立定义在顶层的 api 包
- Controller 负责对数据做前置校验,具体业务逻辑则交给应用服务或畛域服务实现,可间接调用应用服务办法或畛域办法
- 业务划分在接入层不显著,更多是基于前端模块进行划分 Controller,且业务简单时必然存在畛域穿插,故 facade 下没有再细分业务包
- assembler 数据转换负责解决简单的数据转换,简略的数据转换可显式调用工具类的转换方法
2、应用层(application)
应用层次要作用是业务编排、转发、校验等,解决跨聚合、畛域事件逻辑,简单操作 / 简单查问也在此层体现(CQRS)。
- 应用服务 AppService 是一种简略逻辑封装,接入层无奈间接调用畛域层拿到后果的,可在此层编排封装聚合办法
- 应用层可依赖畛域层,但不可依赖接入层,所以传参进应用层要么是根底类型,要么在接入层 assembler 做一层转换,要么入参出参定义在 api 包
- 事件个别状况是跨聚合或跨服务的,所以事件定义在应用层,在应用层处理事件的公布 / 订阅
- 接入层可间接调用畛域层,不通过应用层
3、畛域层(domain)
畛域层或称为模型层,零碎的外围,负责表白业务概念、业务状态信息以及业务规定,蕴含了该畛域所有简单的业务知识形象和规定定义。
- 畛域层只表白业务,不写技术代码,在业务上不依赖其余层
- 畛域聚合以业务来命名包,聚合内蕴含该聚合下所有模型(DO 对象)、仓库接口、畛域服务
- 畛域模型 model 是畛域聚合下的业务外围模型,以 XxxDO 命名,仍旧采纳贫血模型,只蕴含大量原子性操作,不蕴含跨模型数据处理、长久化操作等
- 仓库应用 repository 命名,畛域层只定义仓库接口,不写仓库实现
- 畛域工厂 factory 与设计模式里的工厂模式不同,畛域工厂次要负责畛域对象的简单构建,如畛域对象生成、属性填充等,因为存在跨聚合的状况,所以 factory 包并不在聚合内,与畛域聚合同层级
- 内部 API 接口、内部框架代码做一层浅封装,放在 external 聚合包下,以 ExXxxService 命名接口,实现类还是在基础设施层,起接口防腐作用
因为领域建模最终体现在畛域层内,在咱们建模时就要思考畛域层的代码如何写。
- 领域建模时只表白外围属性与外围行为
- 聚合内跨多个模型的简单业务逻辑,写在畛域服务内
- 畛域模型的办法只写原子性的操作,但不包含 CRUD 长久化操作
一些难点:
- 无奈实现模型的“所建即所得”,简单代码无奈通过畛域模型的简略几个办法表白残缺
- 模型只能表白外围的业务行为,所谓的充血模型在落地时可能更多地拆分到畛域工厂、畛域服务、应用服务中实现
4、基础设施层(infrastructure)
基础设施层作为工程的基础设施应用,编写与业务无关的代码,如技术框架、工具类,此外还有一个重要的性能,要写仓库的实现类、内部服务的实现类。
- 基础设施层的仓库(repository)实现了畛域层定义的仓库接口,数据拜访层(dao)也定义在仓库下,数据库实体(PO 对象)定义在 entity,以 XxxPO 或 XxxEntity 命名,这里遵循了公司框架的命名形式,应用了 XxxEntity
- param 是比拟非凡的一层,该类个别定义查询数据库的参数。基于公司的 Base 框架,repository 定义接口时依赖了 param 对象,按情理畛域层不应依赖基础设施层(DIP 准则),但 Param 又跟 PO 对象非亲非故,所以把 param 对象放在了基础设施层
- 基于 Clean 架构准则,其余框架性代码、工具类、配置类都放在基础设施层,业务代码与技术代码拆散后,万一降级技术代码,对业务代码做起码改变
咱们再来看一下全貌:
通过理论案例,总结以下重要几点:
- 畛域层是业务最外围的一层,聚合之间的边界须要划分清晰,而接入层、应用层波及跨聚合,基础设施层关注仓储实现与技术框架,所以咱们只在畛域层划分业务包,对应领域建模按聚合划分边界,并定义畛域模型的仓储接口
- 充血模型建模,贫血模型落地,把外围业务行为按需划分到畛域模型(原子性、非长久化)、畛域工厂(构建模型)、畛域服务(跨模型)或应用服务(跨聚合、事件)中
- 应用接口拆散业务代码与技术性代码,当业务迭代时,批改畛域层和基础设施层的仓库实现即可;当技术框架降级时,批改基础设施层即可,不至于把业务代码也批改一遍,缩小出错老本
DDD 工程落地思考的是代码的归类划分问题,重点在于 业务边界的辨认、业务和技术代码的解耦。写代码前须要思考分明不同的代码应该写到哪里,联合前人优良的工程架构思路与公司以后的技术架构,整合一套灵便的、适宜咱们本人的 DDD,不能照搬,更不能为了 DDD 而 DDD。
疑难剖析
1、用充血模型还是贫血模型?
其实除了常见的充血模型、贫血模型,还有不罕用的失血模型、胀血模型,区别如下:
- 失血模型:只蕴含 Getter/Setter 的纯数据类,个别不会有这种设计
- 贫血模型:蕴含模型属性、Getter/Setter 与非长久化的原子畛域逻辑,长久化逻辑放在业务层(如 Service 类)
- 充血模型:比贫血模型多了长久化操作与绝大多数业务逻辑,实例化时会拿到很多不肯定须要的关联模型
- 胀血模型:只有畛域对象与 DAO 两层,在畛域逻辑上封装事务
基于现有的 Spring 框架,以及集体以往的代码编写教训,在代码落地层面还是以贫血模型进行较失当。
2、放应用服务还是畛域服务?
应用服务在应用层,畛域服务在畛域层,我怎么晓得业务代码该放哪里?
应用服务的作用:
- 负责展示层与畛域层之间的协调,协调业务对象来执行特定的应用程序工作(编排业务)
- 放绝对灵便的代码逻辑,易于编排
- 操作粒度较大,事务管理在此解决
畛域服务的作用:
- 负责表白业务概念,业务状态信息以及业务规定,是业务软件的外围
- 放绝对原子性的外围代码,封装性与复用性强
- 操作粒度较细,不治理事务,畛域模型不应该意识到事务的存在
其实,难点在于 辨认业务代码,考验咱们对业务的了解水平与思考水平,如果能够显然预料到将来会产生显著的变动,则应该在设计之初更灵便地设计好;如果对将来的变动把握并不清晰或不确定,满足以后业务需要即可。
咱们无奈防止适度设计还是设计有余,但如果架构正当,代码清晰,改起来老本不会特地大。这里提倡开发者尽量多与领域专家(业务人员或产品经理)沟通,以把握代码将来的走向。
3、非凡代码如何归类?
除了惯例的简略业务代码,还波及到简单业务代码拆分到不同类的问题,最典型的是使用设计模式。
- 工厂模式:依据不同条件生成相应对象,常见畛域工厂、畛域服务、应用服务内
- 策略模式:依据不同条件执行相应逻辑,定义一个策略接口和多个策略实现,常见畛域服务、应用服务内
- 观察者模式:应用公布 / 订阅模式代替,可使用基础设施层的 SpringEvent 来解耦代码
- 责任链模式:拆分简单业务逻辑到各个责任链类执行,常见畛域服务、应用服务内
原则上,外围逻辑在哪一层拆就放在哪一层,防止代码散落到各处。
一些教训
DDD 领域建模三大步:划分边界、对立语言、组织模型。
DDD 工程落地四大步:整合框架思维、确定划分思路、模型代码映射、非凡代码归类。