典型的领域驱动设计应用架构
对典型的领域驱动设计 (Domain Driven Design) 应用架构的理解和使用经验的总结。
只有有了好的应用架构,应用才能有拓展性,才能方便其他开发人员投入到这个项目的开发和维护,这个项目才能拥有足够的活力。
领域模型的项目结构
一个基于 DDD 设计模式的 SpringBoot 项目结构 通常划分如下
其中
config: 所有 SpringBoot 的配置文件都放置在这里
controller: 所有项目的 API 入口 controller 文件
dao: Data Access Object,在一些项目 (如 Hibernate) 中也可以叫 repository。这里的类负责处理向数据库,其他 API 或者文件系统增删改查数据
domain: 从数据库的角度可以比较好理解,一个 domain 类一般代表数据库中的一张表,有着数据库表结构中相对应的字段
service: 该目录中存储了所有 Service 的 Interface。服务是业务逻辑处理的地方,通过 Interface 对业务逻辑的定义和实现进行解耦是进行单元测试的前提
service/dto: 有的时候 controller 定义的 API 返回的值不一定就恰好是 domain 代表的数据,它可能是多个 domain 的集合。这种情况就需要有一个 Data Transfer Object 对数据进行集合和变换以满足前端需求
service/impl: 该目录下的类为 Service interfaces 的实现
utils: 该目录下为工具类,一般只应该包含静态方法
领域模型的 4 种模式
领域设计模式(Domain Driven Design) 是由 Martin Fowler 提出的一种设计模式,其模型一般可以分为4大类:
失血模式: 简单来说,就是 Domain Object 只有属性的getter/setter方法的纯数据类,所有的业务逻辑完全由 Service 完成
贫血模式 (Anemia Domain Model) : 简单来说,就是 Domain Object 包含了不依赖于持久化的领域逻辑,而那些依赖持久化的领域逻辑 (与 DAO 层打交道的逻辑) 被分离到 Service 层
充血模式: 充血模型和第二种模型差不多,所不同的就是如何划分业务逻辑,即认为,绝大多业务逻辑都应该被放在 Domain Object 里面(包括持久化逻辑),而 Service 层应该是很薄的一层,仅仅封装事务和少量逻辑,不和 DAO 层打交道
胀血模式: 取消 Service 层,只剩下 Domain Object 和 DAO 两层,在 Domain Object 上面封装事务
失血模式
这种模式下 Domain Object 就只是数据库表在 POJO (Plain Old Java Object) 上的一个映射。
优点:
简单。所有业务逻辑都在 Service 层实现,Service 通过调用 DAO 中的方法对 Domain 所代表的数据进行存取
缺点:
anti-OOP。按照 Object-Oriented Programming 的理念,一个 Object 应该包括它所代表的事物的方法。比如
Cat
类所生成的一个实例cat
应该有meow()
这个方法,而不应该把meow()
这个方法放到CatService
里去
贫血模式 (Anemia Domain Model)
这种模式和上面失血模式最重要的区别是 Domain Object 和 Service Implementation 中都包含了领域逻辑,其划分标准是
依赖于持久化(换一种说法就是需要通过 DAO 向数据库读写数据)的领域逻辑分离到 Service 层
不依赖于持久化的领域逻辑包含在 Domain 中
Martin Fowler一直主张该模型。
优点:
各层单向依赖,结构清楚,易于实现和维护
设计简单易行,底层模型非常稳定
缺点:
比较紧密依赖的持久化 Domain Logic 被分离到 Service 层,显得不够 Object-Oriented
Service 层过于厚重
充血模式
充血模型和第二种模型差不多,所不同的就是如何划分业务逻辑,即认为,绝大多业务逻辑都应该被放在 Domain Object 里面(包括持久化逻辑),而 Service 层应该是很薄的一层,仅仅封装事务和少量逻辑,不和DAO层打交道。
优点:
更加符合 OO 的原则
Service 层很薄,只充当 Facade 的角色,不和 DAO 打交道
缺点:
DAO 和 Domain Object 形成了双向依赖,复杂的双向依赖会导致很多潜在的问题
如何划分 Service 层逻辑和 Domain 层逻辑是非常含混的,在实际项目中,由于设计和开发人员的水平差异,可能导致整个结构的混乱无序
考虑到 Service 层的事务封装特性,Service 层必须对所有的 Domain Object 的逻辑提供相应的事务封装方法,其结果就是 Service 完全重定义一遍所有的 domain logic,非常烦琐,使得和贫血模型没有什么区别了
胀血模式
取消 Service 层,只剩下 Domain Object 和 DAO 两层,在 Domain Object 上面封装事务。
Ruby on Rails 就是这种模式,甚至 Domain 和 DAO 也直接合并了,但是这一部分是由 Ruby 这门语言决定的。 Ruby 的动态 metaprogramming 特性和 module 之间的继承关系在编译器的层面上实现了逻辑分层, AOP 和 Dependency Injection,从而使得 Unit Test 中所依赖的 mock & stub 手段不需要通过设计模式中的分层和 Java 中的 interface 来实现。
在 Java 项目中,其优点是:
简化了分层
比较符合 OO
缺点:
很多不是 domain logic 的 Service 逻辑也被强行放入 Domain Object ,引起了 Domain Object 模型的不稳定
Domain Object 暴露给 controller 层过多的信息,可能引起意想不到的副作用
Controller vs Service
什么应该放在 Controller 里呢,一般标准如下:
API endpoints exposure 应该放在 Controller 里
安全相关逻辑,比如 param filter, user authentication & authorization 应放在 Controller 里
Request 中的收到数据的校验应该放在 Controller 里
下一步 Controller 应该调用 Service 方法并传入参数,获取 dto response 回复给前端
经验总结
在这四种模型当中,失血模型和胀血模型应该是不被提倡的。而贫血模型和充血模型从技术上来说,都已经是可行的了。但是我个人仍然主张使用贫血模型。其理由:
参考充血模型第三个缺点,Service 层只是 Domain 层的一个映射,已经没有太大意义了,反而增加了工作量(在 Domain 中定义好了的一个 domain logic 在 Service 层需要再包装一遍)
参考充血模型第三个缺点,不同的 Services 的逻辑集中在一个 Domain 中,必然使得 Domain 及其厚重
domain object和DAO的双向依赖在做大项目中,考虑到团队成员的水平差异,很容易引入不可预知的潜在bug
如何划分 domain logic 和 service logic 的标准是不确定的,往往要根据个人经验,有些人就是觉得某个业务他更加贴近 domain,也有人认为这个业务是贴近 service 的。由于划分标准的不确定性,带来的后果就是实际项目中会产生很多这样的争议和纠纷,不同的人会有不同的划分方法,最后就会造成整个项目的逻辑分层混乱。这不像贫血模型中我提出的按照是否依赖持久化进行划分,这种标准是非常确定的,不会引起争议,因此团队开发中,不会产生此类问题
贫血模型的 domain object 确实不够 rich,但是我们是做项目,不是做研究,好用就行了,管它是不是那么纯的 OO 呢?
Last updated