前面几篇文章中,笔者给大家阐述了 DDD 领域驱动设计的三大过程,重点围绕如何通过战略设计与战术设计进行 DDD 领域模型分析以及沉淀,但是还没有涉及到工程层面的落地。所有的这些架构理论或者设计模式到最后都是为了让我们的代码结构更加清晰,扩展性以及维护性更强。从而开发出 bug 少稳定性更好的应用。因此本文重点介绍如何进行 DDD 工程化落地。
当我们完成边界上下文的划分以及领域模型的构建之后,就需要进行微服务的工程结构设计了。在进行工程结构落地之前,我们需要先确定微服务内部的领域分层结构。首先我们要思考一个问题,为什么要进行领域分层呢?实际上领域分层就是一种分而治之的思想,主要为了避免将代码工程开发成一坨大泥球,各种业务复杂逻辑以及技术细节都糅合在一起,导致工程后期难以维护同时也会削弱领域模型的完整性。另外通过领域分层设计,更加容易开发出高内聚低耦合的软件服务,在模块复用以及扩展性方面也会有更好的表现。
搞清楚为什么进行领域分层之后,我们来确定下如何进行微服务内部的领域分层,因为分层设计的好坏直接决定了我们微服务的工程结构合理性以及后期团队落地的效果。不过遗憾的是,真正的领域驱动设计在怎么规范工程结构上面实际也没有非常明确具体的规范,因此我们需要根据自己的实践经验以及思考和理解来进行划分设计。下图中左边的分层方式是 Eric Evans 在《领域驱动设计》中提出的,但是这种分层方式实际上是存在明显不足的。为什么这么说呢?
大家都比较熟悉 MVC 的开发方式,因此在团队中进行 DDD 落地的时候,很多同学有疑问为什么要让基础设施层反向依赖领域层呢,大家都觉得很别扭。按照正常逻辑来说,领域模型发生变化后需要进行持久化保存,很明显是领域层依赖基础设施层,但是在工程落地的时候还是基础设施层依赖领域层,这是为什么呢?实际上无论是什么样的架构都遵循这样的设计原则,我们都认为业务领域是核心域,核心域对外部的依赖越少越好,因此需要实现将技术复杂度与业务复杂度相分离。那么在 基于 DDD 的架构中,领域层就是核心层因此它的对外依赖越少越好,也就是说应该是非核心依赖核心而不是核心依赖非核心。
在我们以往的开发模式中,一般都是 service 接口去调用 dao 接口进行相关的数据操作,但是我们发现一旦我们进行一些优化操作,比如增加缓存来提升数据查询的效率,我们就需要修改 service 层的代码,但是实际上增加缓存属于技术实现细节,并不在业务范畴之内,可实际情况就是技术细节有变化就会影响到业务层,因此这样的状况明显是不合理的。
因此上图中优化后的依赖倒置,表面上是基础设施层依赖领域层,其本质是技术实现细节依赖于接口抽象,这是一种编程思想的转变。将 repo 层的接口定义在 domain 层,具体实现细节由基础实施层去完成,这样实现了对于技术实现细节的解耦。同时不仅保证了 domain 层模型的稳定性,也提升了基础设施层实现的灵活性。
在介绍各层对象之前,我们先思考一个问题。为什么每一层都要有不同的数据模型对象呢?不同分层在进行接口调用的时候,每次都要进行模型对象转换,很多时候对象中的参数还都是一样的,这样做不是多此一举吗?这也是我在团队中推行 DDD 领域驱动设计落地的时候,很多同学提出来的疑问。
但是大家有没有想过一个问题,假设我们使用一个模型数据对象来串接代码中的各个分层,如果哪一天数据库表字段增加了或者修改了,那么这个变化会在各个分层中蔓延开来,这样即使做了应用分层但是实际上和一个大泥球的应用没有什么本质区别,另外对于核心的领域层来说也需要屏蔽底层细节变化对于领域模型的影响,避免领域模型稳定性问题。因此为了避免上述问题的发生,各个分层应该都有数据自己的模型数据对象,各司其职。
VO(View Object,视图对象):该层的视图数据对象主要的作用就是将应用层的数据进行组装后形成用于页面展示的数据。
DTO(Data Transfer Object,数据传输对象):DTO 主要作为 Application 层的入参和出参,用于用户接口层与应用层之间的数据传输。比如接口参数中的 Command、Query 以及事件 Event,以及 Request、Response 等都属于 DTO 的范畴。DTO 的价值在于适配不同的业务场景的入参和出参,避免让业务对象变成一个万能大对象。
Model(领域对象):领域对象是我们常说的核心的领域模型对象,它的字段和方法应该具备强烈的业务语义,和持久化方式无关。也就是说,Entity 和 PO 很可能有着完全不一样的字段命名和字段类型,甚至嵌套关系。Entity 的生命周期应该仅存在于内存中,不需要可序列化和可持久化。
PO (Persistent Objec,持久化对象):实际上是我们在日常工作中最常见的数据模型。但是在 DDD 的规范里,PO 应该仅仅作为数据库物理表格的映射,不能参与到业务逻辑中。为了简单明了,PO 的字段类型和名称应该和数据库物理表格的字段类型和名称一一对应,这样我们不需要去跑到数据库上去查一个字段的类型和名称。
上文中分别说到了领域分层结构以及各个数据对象的不同含义和用途,那么我们接下来就看下各个数据对象在 DDD 的各个领域分层中是怎么进行数据流转的吧。
在用户接口层,它需要接收来自 WEB 端、APP 端以及其他的外部数据请求,并将请求通过 DTO 向应用层进行传递,根据应用层返回的 DTO 数据,再将 DTO 转化为页面需要呈现的 VO 数据。
我们通过 Query 对象表示查询,用 Command 对象表示数据操作。当请求到达应用层后,如果需要调用外部服务的接口,那么我们需要通过应用层的防腐层进行调用。为什么需要防腐层呢?主要就是为了隔离变化,防止外在服务的数据变化影响应用层的代码,如果真的需要修改那么直接在防腐层中进行修改就好。
在领域层,我通常使用的是 model,可以理解为业务领域模型,主要包括实体以及值对象。在应用层会将 model 作为参数进行领域层接口的调用完成核心的业务逻辑。在一些其他的书中,很多人喜欢使用 DO 来作为领域层的数据承载对象,但是我个人还是觉得 model 更适合,因为从名称上面更好理解一点,更加直观一点。
领域层中包含了仓储的接口,具体的实现在基础设施层中,这是一种依赖倒置的设计方式,实现领域层与基础层的解耦。大致的数据转化流向如下图所示。
在确定好领域分层各层的依赖关系之后,我们需要设计下具体可落地的工程结构,如下图所示。
starter 层:该层属于用户接口层,服务的启动类也在该层,主要负责服务的启动以及对外提供 REST 接口或者 RPC 接口。
business 层:主要负责业务逻辑的编排,不负责具体的业务逻辑,因此该层应该是比较薄的。
integration 层:ACL 层,即防腐层,主要与外部服务接口进行交互,它的存在主要为了将微服务本身的业务模型与外部服务的模型进行隔离,避免外部服务模型的变化影响到自身服务领域模型的稳定性。
domain 层:领域层属于核心层,所有的业务领域模型以及领域服务都在该层,沉淀了整个业务域中的业务领域模型,也就说核心的业务逻辑都落在此层,同时定义了 repository 层的接口。
common 层:通用层,主要放一些支撑其他业务的代码,比如各种工具类,各种常量定义、错误码定义以及多语言等。
repository 层:属于基础设施层,主要负责与数据库、Redis 等进行交互,实现领域层定义的接口。
本文主要和大家聊了怎样进行 DDD 领域驱动设计的落地,分析了为什么要进行领域分层以及为什么要实现依赖倒转的领域分层结构,同时基于依赖倒转的领域分层结构设计了可落地的微服务工程结构。希望通过本文可以为大家在落地 DDD 的时候提供一点工程结构设计的思路。后面的文章将从代码层面入手和大家分享下如何通过代码实现 DDD 落地。
领取专属 10元无门槛券
私享最新 技术干货