阅读 156

DDD 实践手册(2. 实现分层架构)

承接系列的上一篇,本次我回来分享如何结合 Clean Architecture 与 DDD 实现一个分层架构。

项目的目录结构

上图是项目的第一层目录,分为 applicationdomainfacadeinfrastructure 四个部分。接下来分别介绍这四个层的作用。

Application Layer

application 对应了 DDD 中的「应用层」,同时也对应了 Clean Architecture 中的 Application Business Rule。从项目中的实践而言,它作为「粗粒度」业务的入口,也有人喜欢称之为一个 Use Case。在这一层中不应该包含复杂的业务规则,而是对下层的 domain (领域层)进行协调,对业务逻辑进行编排。需要注意的是这一层只应该依赖于下层的 domain 层与 infrastructure 层。

我们再看一下 application 内部是怎么划分的:

dto 目录存放了的是 application 对上层暴露服务所接受的参数类型,也就是大家熟悉的 Data Transfer Object。service 目录则是之前提到的「粗粒度」的服务接口,这些服务需要做的就是按照业务逻辑将 dto 对象转化为 domain 层的领域对象,并调用相关领域对象的方法完成业务逻辑。如果需要还会调用 infrastructure 的服务。再次强调,这部分的服务不应该涉及到复杂,核心的业务逻辑。

Domain Layer

domain 是 DDD 的核心层,具体的目录结构如下:

domain 之下的是名为 bc1 的目录,这里指代项目中某个业务的 Bounded Context(限界上下文),关于 BC 的概念会在后续的文章中详细讲解。在 bc1 之下的才是详细的领域分层。

exception 目录中定义了领域层相关的异常,即一般称之为的 BusinessException,代表违反某些业务逻辑的异常,例如账户余额不足等。model 目录中定了领域对象,一般建议使用「充血模型」进行建模。repository 中定义了领域对象对应的「仓库」,关于 Repository 的概念也会在后续文章中专门讲解。service 则是定义了「领域服务」对象,如果认为 model 定义了业务模型,是名词,那么领域服务就是动词。

最后我们说一下 event 目录。在一个完整的领域模型中,我们往往需要划分多个不同的 Bounded Context,但是不同的 BC 之间应该怎么交互呢?Eric Evans 的书中提供了集中不同的解决方案,例如自定义 DSL,防腐层等。而在我们具体的项目中,我们更倾向于使用基于「领域事件」的交互方式,这样不仅不会破坏各个 BC 间的封装,也移除了各自间的耦合。producer 中是事件的发送方,handler 是具体处理事件的对象。关于领域事件也会在后续专门介绍。

Facade Layer

facade 是整个系统对外暴露服务的部分,具体目录结构如下:

系统对外暴露两种协议的服务,即 RESTful 风格的 API 与 Web Service,对应的实现分别在 restws 目录下。facade 层的工作是基于协议对客户端提供的数据进行校验,然后将数据转化为 application 层所需的 dto 对象,并调用 application 提供的服务。facade 中不应该有任何的业务规则与逻辑,只是完成数据对象的转换。

Infrastructure Layer

infrastructure 层主要负责提供底层的纯技术服务,具体目录结构如下:

这一层的功能都比较直白,是大家熟悉的具体技术实现,与领域模型没有任何的依赖关系,这里就不再赘述了。

问题与思考

以上是我们实际项目中结合 Clean Architecture 与 DDD 的分层实现,它的好处很明显,能够比传统的三层架构更好的兼顾领域层的隔离,整个的依赖关系也非常清晰明了,方便开发人员理解,所以我着重谈一些遇到的问题与思考。

繁琐的数据对象转换

从系统的分层架构来看,一共有三种类型的数据对象,分别为 DTO,Domain,PO(Persistence Object)。在实现一个业务功能时往往发生多次数据对象的转换,且大部分时间都是 getter 与 setter 的操作,非常的冗繁。

为了解决这个问题我们引入了 Model Mapper 作为对象映射框架,省去了一些多余的代码。但是依然存在着另一个问题。考虑到 DDD 中的另一个概念: Aggregate(聚合),当从 PO 转换为 Domain 时,需要以 eager 模式从存储中加载所有的数据,相对而言丧失了延迟加载的优化特性。

模糊的 Module 与 Bounded Context

在 DDD 理论中 Module 与 Bounded Context 是不同的东西,在上述的分层架构中,领域层有着明确的 BC 划分,但是在其他层却没有这些。最直接的现象就是随着系统功能的逐渐增加,业务规则日益复杂,application 目录下 dtoservice 下的类会越来越多,由于缺乏进一层的抽象,导致后续的开发人员很难理解。

领域事件引入的事务问题

在引入领域事件之后,一部分的业务流程变为了异步调用,因此事务边界的管理变得更为复杂,在某些情况下无法达到事务一致性的要求。这无疑增加了开发者的心智负担,也提升了不少测试的难度。在这种情况需要进一步加深对业务的理解,尽量将事务特性从业务规则中移除或是绕开。

架构复杂性的提升

架构复杂性的提升带来的是开发人员学习的成本提升,在实践中,我们发现很多时候开发人员的代码中引入了错误的依赖关系,例如 domain 的方法签名中有来自于 dto 的对象,或是 facade 中引入了 domain 的领域对象。对于这种问题比较好的解决方案是加强 code review,加强开发人员对分层思想的理解,以及引入 Unit test your Java architecture - ArchUnit 这样的框架,在 CI 时对代码的依赖关系进行静态检查。

小结

本次介绍了项目中使用的 DDD 分层架构实现与遇到的问题,其实并没有一种完全正确或是适合任何项目的分层架构,掌握背后的思想与学会如何做出妥协才是一个架构师的工作。下一篇我会介绍项目中如何实现 Entity(实体),Value Object(值对象) 与 Aggregate(聚合)。

欢迎关注我的微信号「且把金针度与人」,获取更多高质量文章

关注下面的标签,发现更多相似文章
评论