DDD系列文章第7篇:可落地的DDD分层架构 -- 知识铺
『没有中间商赚差价』在商业领域里已经是深入人心,但在架构领域里往往是相反的,很多时候需要加入『中间商』来降低上下游系统的耦合。分层架构不仅仅在DDD很普遍,其他架构风格也是靠『分层』这把利器一路披荆斩棘。本篇来到研发最喜欢的DDD架构和代码部分,正所谓talk is cheap,show me the code,架构理念再炫酷,总要落地才能实现业务价值。
做技术的都知道有一个非常重要的设计原则叫『低耦合和高内聚』,但怎么衡量它呢?我相信你去问一千人会得到一千个答案,但我相信无论哪个答案它都会体现分层的精神。因为架构领域里有一句几乎是最著名的话:没有什么架构问题是加一层解决不了的,如果不行就再加一层。
架构要完成它治理复杂度这一主要目标的常用手段是分层思想。分层是个泛义上的说法,细说分层的话可以包含分而治之、分离关注点、代码分层。现在的商业软件系统为了满足日益增长的用户体验要求,以及互联网的流行和跨系统协作变得频繁,系统复杂度是持续在增加的。如何能够让研发人员聚焦在企业核心业务相关的系统上是架构师应该重视的。
分离关注点是软件发展的趋势。亚当斯密在他著名的《国富论》里举了制针手工工厂的例子,说明了分工协作能大大提高劳动生产力。同样道理,系统和模块的分层可以让每个层次都能够保持专注、沉淀能力、发挥出特长,从而达到很高的集体效率。软件开发技术或流程的每一次大的突破都离不开分离关注点的思维方式。
除此之外像网络的七层模型、分布式消息队列系统、API网关等等都是分层思想的体现。DDD的分层架构是要对应用系统再一次分离,把应用系统的代码分层次组织,特别是分离技术代码和业务代码,让上帝的归上帝,恺撒的归恺撒。
02
—
DDD的分层架构
大多数人的眼里DDD只是一种设计方法论,DDD分层架构又指什么?一般指DDD的分层设计,也就是在限界上下文内部通过多层次方式组织代码,严格说这只是一种架构风格。这里有必要先解释一下架构风格和架构模式的区别:
-
架构风格指设计理念、设计方法论,也可能有具体的设计规范,但没有代码载体,没法拿来即用。
-
架构模式是具体实现的泛化,同时具有实际的代码载体,只要场景匹配就可以拿来即用。
举例说:
-
RESTFul只是一个架构设计风格,对应的设计规范是JAX-RS。Jersey、RESTEasy和Restlet是RESTFul架构模式,能拿来就用实现一个满足RESTFul风格的网络服务。
-
微服务架构只是一种架构风格,Spring Cloud、Dubbo等才是微服务架构模式,开箱即用。
因此DDD分层架构只是一种架构风格,包含了一些设计规范,但市面上对应的能拿来即用的架构模式不多,因此落地DDD时往往需要自己弥补这个环节。
2.1 分层架构的演进
DDD分层架构经常会和六边形架构、洋葱架构、整洁架构放在一起讨论,以至于其实每个人说的分层架构都不一样。首先它们都属于前面说的架构风格范畴,是个逻辑概念,并不指代具体的某个物理框架代码,先来看下它们出现的顺序和各自的架构特点。
2.1.1 三层架构
在2003年Eric Evans提出分层架构之前的主流架构风格是三层架构,每层的功能职责是:
-
表示层:负责与用户进行直接交互,保证用户体验。如B/S系统中的Web页面,表示层将用户输入的数据传入下层,从下层接受输出数据。
-
业务逻辑层:可以理解为对数据进行加工处理,包括数据结构设计,业务规则校验和逻辑计算。
-
数据访问层:主要是对数据库而不是对数据的访问和操作,如数据表的增删改查。
在主流的前后端分离开发模式下,表示层更多承担着来自用户界面的网络请求响应。对应Spring开发的Java Web程序里就是controller的接口定义,业务逻辑层对应的是Service的实现,数据访问层对应的是DAO层。
三层架构有以下特点:
-
【优点】体现了分层的好处,关注点分离后各层各司其职,一定程度上降低了耦合。
-
【缺点】耦合仍然存在,业务发展到一定规模后业务逻辑层的代码会很杂乱,很难维护。既包含业务逻辑相关代码也包含集成框架中间件以及其他应用系统的代码。
-
【缺点】业务逻辑层依赖数据访问层,使得业务逻辑层不纯粹。数据库表对象的设计变动会反向影响到业务逻辑层的业务实体对象,进而涉及很多修改的地方。
2.1.2 分层架构
最早的分层架构是由Eric Evans在2003年的DDD书籍里提出的,目的是为了解决三层架构中业务逻辑层难以维护和演进的缺点。
-
用户界面层:和三层架构第一层基本保持不变。
-
应用服务层:从三层架构里的业务逻辑层剥离出领域建模里的应用服务。
-
领域层:从三层架构里的业务逻辑层剥离出的核心业务逻辑,也即领域模型所在层。
-
基础设施层:包含原三层架构的数据访问层,也包括从三层架构里的业务逻辑层剥离出的技术框架、中间件系统、其他应用系统的相关代码。
分层架构有以下特点:
-
【优点】第一次把代码的层次归属与DDD的领域建模对应起来了,匹配了业务逻辑和业务实体的建模结果。
-
【优点】在保持三层架构的好处基础上,对三层架构做了进一步细分,每层的职责更加明确。
-
【缺点】仍然没有解决业务逻辑(应用层+领域层)层依赖基础设施层的问题。即没有消除因为技术设施层里具体技术实现的变化可能导致的应用服务层和领域层的变化。
2.1.3 六边形架构
六边形架构是Alistair Cockburn在2005年提出的,它把系统分为内部和外部。内部代表业务逻辑,外部代表业务逻辑的入口调用和底层依赖。内部通过端口和外部进行通信,端口代表了某种协议,往往以API呈现。不同形态的外部要顺利访问到内部之前,或者内部需要依赖外部之前都需要经过必要的适配实现。因此六边形架构实际的名字叫端口适配器架构。因为端口可能有多个,比如入口侧有用户界面、API、命令行、消息流、甚至是系统定时任务脚本等方式访问内部业务逻辑,出口侧可能会依赖数据库、缓存系统、中间件系统、外部应用系统等等,因此架构图就是一个多边形形状,只是六个端口比较形象因而得名为六边形架构。六边形架构本身和DDD没有直接关系,在《实现领域驱动设计》一书中,作者将六边形架构应用到领域驱动设计的实现。对内部业务逻辑融入了应用服务和领域模型,对外部的依赖融入了资源库等领域概念。下图是结合DDD元素的六边形架构:
六边形架构有以下特点:
-
【优点】对分层做了进一步明确,隔离了内部的业务逻辑和外部的主动或被动的依赖。很好地体现了DDD的思想,分开了业务和技术实现。带来很多好处,比如可测试性高,外部可替代性高。
-
【优点】解决了业务逻辑不应该依赖基础设施层的问题,实现了依赖倒置,即外部各适配器依赖内部的端口,适配器是对端口的实现。
2.1.4 洋葱架构
洋葱架构是2008年Jeffrey Palermo结合六边形架构和DDD提出的,因此每层的命名和分层架构里的命名一致。从外部到内部每层都用圆圈表示,因为架构图很像洋葱而得名。它跟上图的六边形架构很接近。只是对内部的业务逻辑再划分了一个领域服务层。
-
第一层是用户接口、基础设施,测试用例等外部驱动或依赖。
-
第二层是应用服务层。
-
第三层是领域服务层。
-
第四层是领域模型层。
如图所示,依赖的方向是外层依赖内层。注意:是不是意味着要访问内部的领域模型一定要严格经过应用服务层和领域服务层?其实未必,有些业务逻辑没有领域服务也不用特意构建领域服务,也就意味着应用服务可以直接依赖领域模型。
2.1.5 整洁架构
整洁架构是2012年Rob Martin提出的,跟洋葱架构类似,只是每层的叫法没有对应DDD的概念。
-
最外圈的网络、界面、外部接口、数据库、设备跟程序本身无直接联系,表达的是程序的周边环境。
-
Controllers、Gateways、Presenters:本质上是各类适配器,即分层架构里的用户接口层和基础设施层。
-
User Cases是应用服务层
-
Entities对应领域层
整洁架构和六边形和洋葱架构类似,每层的依赖很清晰,外层依赖内层。达到领域层不受业务之外的其他因素干扰的效果。
2.1.6 小结
针对DDD分层架构的依赖不够合理如何改进这个点后来演变出了好几个大同小异的架构,它们的本质都是在解决稳定和不稳定的依赖关系,以及更加明确地定义每层的职责。这也反映了代码和DDD模型的对齐,以及层次职责和界限的划分是值得多多思考的。写出能运行的代码很容易,写出容易理解和维护的代码很难,写出一个团队都能理解和维护的代码更难。
2.2 一种可落地的分层架构
结合上述几种架构的优点,这里提出一个改良版本的分层架构。把所有和业务逻辑有关的内容锁定在领域模型层和应用服务层,把业务逻辑的各种依赖从入口和出口两个方向分为用户接口层和基础设施层。这个分层架构笔者也在多个实际项目实践过,带给大家的认知和上手成本低,可维护性强。
2.2.1 架构图
2.2.2 各层设计原则
用户接口层**:**
负责入口方向的外部依赖。对外暴露接入端点,使得内部的业务逻辑能够以服务的方式供外部系统消费。根据外部系统的特点(如接口协议、数据格式)设计出对应的接口。比如便于Web界面和APP消费的RESTFul接口形式、面向第三方服务的开放接口形式、消息队列监听形式、定时任务形式等等。往往是一种消费形式就对应一个接口。接口层要负责把不同外部系统的接口协议和参数转成下层的应用服务熟悉的协议和参数。另有几个注意事项:
-
不要有任何业务逻辑的内容。
-
不要直接依赖基础设施层,而是通过业务逻辑形成对基础设施层的间接依赖。
应用服务层**:**
本层负责把业务服务以合适的粒度暴露给外部消费。 它和用户接口层的接口设计粒度不一样,一个应用服务应该对应一种业务能力,可以等同于一个用户用例。比如健康码查询是一种业务能力,它只要设计成一个应用服务,但入口的接口可能设计为多个,用来接入多种触发查询的系统。因此本层主要干两件事:
-
通过编排各种领域模型的能力组装出一个用户用例。就拿创建订单这个用例来说,往往涉及了订单聚合的创建(包括了订单业务逻辑处理)、订单资源库的保存、领域事件的发布。
-
提供业务能力的服务过程中必要的一些横向事情。比如启动数据库事务,打印日志,捕捉和处理异常。
领域模型层**:**
负责通过合适的领域模型来承载业务逻辑,DDD建模结果应该在本层得到体现。不应出现任何跟具体技术相关的词汇,比如资源相关的操作只有Repository,没有JDBC、MyBatis、Redis等具体的技术实现。另外还需要针对每种业务逻辑必要的底层依赖设计出合适的适配接口。
基础设施层**:**
本层主要负责实现各种适配接口,典型的有:
-
对资源存储的访问比如数据库、缓存系统
-
对组织内部其他系统的适配逻辑,一般通过内部rpc协议访问
-
对组织外部第三方厂商系统的适配逻辑
-
对基础框架中间件的适配逻辑,比如消息队列、配置中心、文件系统、监控系统等等。
2.2.3 代码结构详解
下面详细说明分层架构的代码结构。
模块名 | 一级包名 | 二级包名 | 三级包名 | 功能说明 |
interface | rpc api | rpc协议类型的接口,供自有前端或内部其他系统调用。可以不再分包,一般是一个聚合一个文件 | ||
restful api | 开放接口,供第三方应用调用。可以不再分包,一般是按照一个聚合一个文件 | |||
subscriber | 消息队列订阅类接口。可以不再分包,一般是一个聚合一个文件 | |||
common(可选) | 本层特有的通用类、异常类、常量、枚举。通用功能建议以二方库形式复用 | |||
application | service | 应用服务的集合,视情况而定要不要按聚合再分包。一般是一个聚合一个文件 | ||
convertor | dto和entity转换 | |||
domain | aggregate | 聚合的集合 | ||
按聚合分包 | 按照一个聚合一个文件夹(包) | |||
entity | 包含聚合相关的实体类 | |||
valueobject | 包含聚合相关的值对象 | |||
repository | 包含聚合相关的资源库接口 | |||
event | 包含聚合相关的领域消息结构 | |||
adaptor | 依赖外部服务的所有适配接口的集合 | |||
rpc | 内部其他服务接口(内部rpc协议) | |||
restful | 外部服务接口(纯http协议、restful协议) | |||
event | event publisher事件发布接口,是否按聚合分二级包视情况而定 | |||
service | 领域服务的集合 | |||
按聚合分包 | 不属于任何聚合的领域服务可以找合适名字命名包 | |||
common(可选) | 本层特有的通用类、异常类、常量、枚举。通用功能建议以二方库形式复用 | |||
infrastructure | adaptor | 领域模型层的适配接口实现 | ||
event | 领域事件接口实现,视情况而定要不要按聚合再分包 | |||
rpc | rpc调用内部系统,视情况而定要不要按聚合再分包 | |||
restful | http调用外部系统,视情况而定要不要按聚合再分包 | |||
repository | 聚合abc | 资源库接口实现,以聚合为单位分包,内含orm的具体实现、entity和po互转convertor | ||
common(可选) | 本层特有的通用类、异常类、常量、枚举。通用功能建议以二方库形式复用 |
2.2.4 必要命名规范
项目里的模块名和包名上节已经解释过了,下面介绍一些类名的命名规范。好的命名可以让人见名知意,也便于在组织里养成对代码的结构化思维。
领域元素 | 英文命名 | 命名规则 | 举例 |
实体 | Entity | 类名以Entity结尾 | OrderEntity |
值对象 | Value Object | 类名以Vo结尾。有些现存的视图类UI类对象已经用Vo结果,建议改为xxUo | AddressVo |
领域服务 | Domain Service | 以动词命名领域服务类和方法,并以DomainService结尾 | ValidatingOrderDomainService |
工厂 | Factory | 类名以Factory结尾 | OrderFactory |
资源库 | Repository | 类名以Repository结尾 | OrderRepository |
领域事件 | Domain Event | 类名以名词+动词过去式+Event命名 | OrderCreatedEvent |
应用服务 | Application Service | 类名以AppService结尾 | PlaceOrderAppService |
DTO | Data Transfer Object | DTO往往指代了接口的入参和出参,以xxDto为结尾表示。注:如果有必要也可以进一步细分。比如写接口的入参以xxCmd结尾,读接口的入参以xxQry结尾,面向UI组装的出参以xxUo结尾,其他场景的出参以xxDto结尾 | OrderAddCmd、OrderQry、OrderUo、OrderDto |
对应db表的数据对象 | Persistent Object | 类名以Po结尾 | OrderPo |
应用服务层转换类 | xxDtoConvertor | 在DTO和entity间相互转换 | OrderDtoConvertor |
基础设施层转换类 | xxPoConvertor | 在PO和entity间相互转换 | OrderPoConvertor |
应用服务层和领域模型层里的函数命名 | 尽量用带领域语义化的词语命名,尽量不要用getxxx,setxxx等泛化词汇 | PlaceOrder | |
测试类 | XXXTest | 单元测试、集成测试类 | OrderEntityTest |
2.2.5 运行态数据流
03
—
结语
DDD的分层架构聚焦领域模型和业务逻辑,规范了如何组织业务代码,以及如何隔离技术框架代码。主要考虑限界上下文(BC)内部的结构。本文讲解了分层架构的作用和演变,最后给出了一个可落地的分层架构,下文将通过实际例子介绍如何在分层架构下编写适配领域模型的代码。
- 原文作者:知识铺
- 原文链接:https://index.zshipu.com/geek001/post/20240710/DDD%E7%B3%BB%E5%88%97%E6%96%87%E7%AB%A0%E7%AC%AC7%E7%AF%87%E5%8F%AF%E8%90%BD%E5%9C%B0%E7%9A%84DDD%E5%88%86%E5%B1%82%E6%9E%B6%E6%9E%84--%E7%9F%A5%E8%AF%86%E9%93%BA/
- 版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可,非商业转载请注明出处(作者,原文链接),商业转载请联系作者获得授权。
- 免责声明:本页面内容均来源于站内编辑发布,部分信息来源互联网,并不意味着本站赞同其观点或者证实其内容的真实性,如涉及版权等问题,请立即联系客服进行更改或删除,保证您的合法权益。转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。也可以邮件至 sblig@126.com