本次我回来分享如何结合 Clean Architecture 与 DDD 实现一个分层架构。

项目的目录结构

image.png

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

Application Layer

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

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

image.png

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

Domain Layer

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

image.png

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 是整个系统对外暴露服务的部分,具体目录结构如下:

image.png

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

Infrastructure Layer

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

image.png

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

问题与思考

以上是我们实际项目中结合 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(聚合)。

作者:JoshuaJin
链接:https://juejin.im/post/5e44b33af265da574b79060d
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。