
1. 业务系统和非业务系统
领域驱动设计(DDD),我们现如今讲的非常多,好像在业务软件开发中,哪哪儿都在用,但是就是说不清楚 DDD 到底是什么意思。
这其实与软件的发展有关,年轻的开发者没有接触过早期的软件开发,对很多技术的演变不甚了解,因此一些在老开发看来水到渠成的概念,对年轻的开发者而言,却似是而非,比如 DDD,现如今的业务软件开发,基本都离不开这个概念,年轻开发者一开始开发,就沿着前辈们搭好的框架一顿开发,对他们而言,业务软件开发,似乎天生就应该是这样的架构,就应该遵循先需求评审、再代码实现,因此虽然他们一直在按照 DDD 的方式开发软件,却对 DDD 不甚了解,犹如雾里看花。
其实 DDD 很简单,他不是一种新的编码技术或编码风格,他只是一种模型驱动的设计方法:通过领域模型捕捉领域知识,使用领域模型构建更易维护的软件。
1.1 非业务系统(早期软件开发)
上面定义有点拗口哈,这里举个例子,在 DDD 之前,软件开发一般是定义问题,解决问题,在解决问题中,一般是从开发者视角来看待如何解决问题的,因此开发者习惯于利用自己熟悉的工具来解决问题------数据结构。
开发者对于数据结构非常了解,并且知道数据结构关联的各种算法,比如树关联着深度优先(DFS)、广度优先(BFS)等,开发者在面对问题时,通常会思考这个问题可以拆分为哪些小问题,每个小问题可以用什么数据结构结合什么算法来解决,就跟我们刷算法题一样,你看这种思考问题的方式对于开发者而言非常自然,并且这也算一种模型解决问题的方式,只不过这里的模型(数据结构)跟业务没有关联,即构造与领域无关的模型。
1.2 业务系统
上面解释了非业务系统(早期软件)开发的方式,这种方式对开发者友好且自然,但对业务方而言,就是灾难,你难道指望业务方跟你提需求的时候利用数据结构跟你沟通吗?相信我,业务方会打死你的,这根本没法玩,业务方只会按照自己对业务的理解,直接定义问题而不会思考这个问题利用当前的数据结构是否能解,是否好解。
观众朋友们应该发现问题了,如何协调业务方跟开发者一起讨论需求,解决问题呢?答案是使用一套业务和开发都能理解的语言来进行沟通,而这,也就是 DDD 要干的事。
DDD 首先会对领域进行建模 ,有了模型后,会基于该模型,创建一套统一语言 ,业务方和开发者利用统一语言进行需求沟通,然后对模型进行优化提炼 ,开发者根据模型来实现业务功能,将模型跟实现关联在一起 ,因此模型的改动就是实现的改动,就能达到业务方也能轻松定义实现的目的。
这看起来是不是很熟悉?没错,这就是我们现在开发正在用的方式,构造一种专用模型(领域模型),将相关的业务流程与功能转化成模型行为,就能避免开发者与业务方出现认知偏差。
这种方式在现在看来,天经地义,这也是为什么我前面说其实现在我们基本都在用 DDD,但是大家对 DDD 却似是而非,脑袋里有些模糊的想法,但就是无法对 DDD 做出准确定义的原因。
DDD 很难定义还有另外一个原因,在不同的领域中应该使用什么领域模型,并没有现成做法,对于某些领域,比如电商,已经有成熟的领域模型,比如用户、订单、商品等,我们经常在用却不会去思考为什么这样划分,因为从实践来看这样划分就是非常成熟的划分方式,但是对一些新兴领域,如何划分领域确定边界就是一个比较难的问题,因此成熟领域不用我们动脑筋,并且目前大家做的基本都是较为成熟的领域,因此一旦需要对新兴领域进行 DDD,我们就会感到无从下手。
其实对于如何实现 DDD,如何提炼领域模型,也是有方法的,那就是知识消化。
2. 提炼模型:知识消化
知识消化分为五个步骤:
- 将模型和实现关联(模型即代码)
- 基于模型提炼统一语言(业务与开发沟通的桥梁)
- 创建富含知识的模型(定义模型)
- 提炼模型(修改模型)
- 头脑风暴和尝试(需求分析和实现)
在这五步中,将模型和实现关联 起来,是前提和基础,如果模型和实现割裂分离,就会变成更接近业务的分析模型 和更接近实现的设计模型,这种时候,分析模型就会退化成纯粹的沟通需求的工具,很容易脱离实现的约束而变得天马行空,比如"根据手机壳的颜色变换主题颜色",不着边际。
基于模型提炼统一语言,将业务方变为模型的使用者,通过统一语言对模型进行需求讨论,实际就是基于模型对需求进行讨论。
后三步构成了提炼知识(模型优化)的循环,定义->修改->实践->定义,通过统一语言讨论需求,发现模型中缺失或者缺点,精炼修改模型,然后实现验证,进而重新定义新模型,然后更新统一语言,整个流程循环起来后,就能将模型不断完善。
3. 模型和实现关联
如何将模型和实现关联起来?对于用惯了面向对象的程序员而言,似乎不言而喻,封装即可,即对领域模型对象,封装其行为,使得对外界而言,只需要了解领域对象及其行为(方法)即可,这也是我们常说的充血模型。
3.1 贫血和充血
贫血对象模型:对象仅仅是对数据进行简单封装,而关联关系和业务的逻辑都散落在对象范围之外,整体跟过程式代码风格类似,一个方法仅仅解决一个问题,并且方法跟对象没有关联。
充血对象模型:与某个概念相关的行为和逻辑,都被封装到领域对象中,跟面向对象的封装非常相似,对象不仅有数据,还有行为,这就是富含知识的模型,或者称之为充血模型。
上面对于贫血和充血,如果还不太理解,那我下面举一个例子:
java
class UserDAO {
...
public User find(long id) {
try(PreparedStatement query = connection.createStatement(...)) {
ResultSet result = query.executeQuery(....);
if (rs.next)
return new User(rs.getLong(1), rs.getString(2), ....);
....
} catch(SQLException e) {
...
}
}
}
class SubscriptionDAO {
...
// 根据用户Id寻找其所订阅的专栏
public List findSubscriptionsByUserId(long userId) {
...
}
// 根据用户Id,计算其所订阅的专栏的总价
public double calculateTotalSubscriptionFee(long userId) {
...
}
}
上面就是典型的贫血模型,User 和订阅是分割的概念,都能通过数据库进行 crud(可能会有外键的关联),与之相反的是充血模型,如下:
java
class User {
// 获取用户订阅的所有专栏
public List getSubscription() {
...
}
// 计算所订阅的专栏的总价
public double getTotalSubscriptionFee() {
...
}
}
class UserRepository {
...
public User findById(long id) {
...
}
}
上面就是典型的充血模型,是不是很熟悉,跟我们现在写的面向对象的代码非常相似,订阅是 User 的一部分,无法脱离 User 存在,因此相关的订阅行为都被封装到 User 中。
从上面的代码中,我们也能解释一些 DDD 中的比较抽象的概念:
- 实体:具有唯一标识和生命周期的业务对象,比如上面的 User
- 值对象:描述业务属性的不可变对象,需要依附实体才能存在,比如上面订阅的课程
- 聚合根:领域模型的完整性守护者
实体和值对象都还好理解,但是聚合根就比较抽象了,我一开始单纯的认为聚合根就是实体行为的集合,但后面发现这个定义不准确,聚合根应该是包含实体状态、行为方法、边界控制 三者于一体的概念,为什么这么说呢,因为聚合根的核心使命是保护内部一致性,行为只是实现手段:
- 没有状态的行为 → 退化为无意义的工具方法
- 没有行为的状态 → 退化为贫血模型
- 状态+行为+边界 → 完整的领域守护者
外部只能通过聚合根来访问和操作领域实体。
3.2 现行 DDD 实现
上面有提过,DDD 不是一种编码方式或者风格,而是一种模型驱动的设计方法:通过领域模型捕捉领域知识,使用领域模型构建更易维护的软件。
核心是提炼模型,对应代码中的领域对象,只要使用充血模型(合理使用),就能将模型和实现关联起来,也就是按照 DDD 的设计方式进行了实现,因此 DDD 和充血模型我理解是强关联的,任何贫血模型理论上讲都不算是 DDD,但这跟我们现有的开发模式似乎并不一致,比如我们会在领域 module 中定义好领域实体,然后并不将所有的实体方法都放在实体中,而是会写一些 service 用来实现领域实体的行为,下面以我们项目中使用的一种架构来进行说明:
如上,领域实体我们是单独定义了一个 module,外部可以依赖领域实体,比如 infra 层能将跟数据相关的对象转换为领域实体对外暴露,这就对领域实体 module 有一个要求,不能引用其他层或者其他 module,不能注入 bean 来实现某些逻辑,否则就会很容易发生循环引用等问题,那在这个模型中,似乎领域实体仅仅是单纯的贫血模型,聚合根不存在,分散在各个 service 中,但为什么我们还将我们的实现称为符合 DDD 的实现呢?
这是因为纯粹的充血也会有问题------实体膨胀,一个实体可能会变成上帝实体,所有功能包含在内,使其膨胀非常迅速,因此一般很少使用纯粹的充血模型,而是会使用一个折衷的方案,部分充血模型 或有限行为领域对象,这种方式能巧妙地平衡领域纯度和工程实用性。
在这种折衷的实现中,领域实体对外部无任何依赖,但是其也不是单纯的贫血对象,其内部会封装一些状态转移、状态校验、自包含的计算、不变条件的维护等行为,将对外部服务有依赖的行为放入服务中,也就是上面 domain-core 中,这样既避免了"贫血模型的知识碎片化",又规避了"充血模型的实体膨胀",是大多数业务系统的最佳实践选择。