承接系列的上一篇,本次我回来分享如何结合 Clean Architecture 与 DDD 实现一个分层架构。
项目的目录结构
上图是项目的第一层目录,分为 application
,domain
,facade
,infrastructure
四个部分。接下来分别介绍这四个层的作用。
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,对应的实现分别在 rest
与 ws
目录下。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
目录下 dto
,service
下的类会越来越多,由于缺乏进一层的抽象,导致后续的开发人员很难理解。
领域事件引入的事务问题
在引入领域事件之后,一部分的业务流程变为了异步调用,因此事务边界的管理变得更为复杂,在某些情况下无法达到事务一致性的要求。这无疑增加了开发者的心智负担,也提升了不少测试的难度。在这种情况需要进一步加深对业务的理解,尽量将事务特性从业务规则中移除或是绕开。
架构复杂性的提升
架构复杂性的提升带来的是开发人员学习的成本提升,在实践中,我们发现很多时候开发人员的代码中引入了错误的依赖关系,例如 domain 的方法签名中有来自于 dto 的对象,或是 facade 中引入了 domain 的领域对象。对于这种问题比较好的解决方案是加强 code review,加强开发人员对分层思想的理解,以及引入 Unit test your Java architecture - ArchUnit 这样的框架,在 CI 时对代码的依赖关系进行静态检查。
小结
本次介绍了项目中使用的 DDD 分层架构实现与遇到的问题,其实并没有一种完全正确或是适合任何项目的分层架构,掌握背后的思想与学会如何做出妥协才是一个架构师的工作。下一篇我会介绍项目中如何实现 Entity(实体),Value Object(值对象) 与 Aggregate(聚合)。
欢迎关注我的微信号「且把金针度与人」,获取更多高质量文章