前言
之前看了很多关于领域驱动设计(DDD)的课程和书籍,发现都没有切合实际的去描述领域驱动设计到底是什么,可能看了几本书也不知道DDD到底该怎么落地。这里因为我在项目中实践了DDD,正好借此机会做一些总结,顺便借助费曼学习法去把DDD讲出来,也是对自己的理解做一些深思和反省。(文中的所有观点均来自博主自己的理解)
从面向对象到软件设计
作为一个Java程序员,接触最早的就是面向对象思想了:面向的是大自然的万物,对象是其一种抽象形式;软件不是凭空想象的,是需要现实和理论去支撑的,如果说业务问题是导向,软件设计就是宏观全貌,而面向对象的思想是将想法落地的微观实现。那么我们领域驱动设计是:用程序去描述客观事实,教我们如何系统的理解业务和将业务落地到实现的一套方法论。(顺便补充一下,DDD不是设计原则,软件的设计始终只有六个字:高类聚,低耦合)
当然,经过上面的理解,可能每一个程序员都会怀疑:那么既然有了面向对象,为什么还要DDD呢?
这里我们也可以从:“DDD为什么突然火了?”这个问题来回答,前几年单机服务盛行的时代,大家都在一个大单体服务里编码,遇不到复杂的业务问题,实在不行加一个类,CV一段代码就解决了问题。但是随着业务的发展,加上 Martin Fowler 提出微服务架构后,大家都逐渐将自己的大单体拆分成微服务,然后就出现了各种各样的问题:微服务真的是按业务拆分还是技术拆分呢,拆分的规则又是什么呢?为什么要拆微服务呢?拆的越细越好吗?随着机器运维的成本和开发维护的成本指数上升,最终会有一个根本的问题:微服务到底该如何划分,业务和微服务拆分的边界都在哪里?
领域驱动设计就真的脱颖而出了,它主要解决的是:
- 主要是为了解决软件的复杂域问题,如果之前的业务问题不复杂,完全可以说在大单体服务里就可以解决
- 领域驱动设计是一套软件设计的方法论,其最终的目的是让我们分离业务和技术的复杂性,在微服务的边界和拆分上有很大的指导性(说是指导性,因为我们的微服务永远不可能按照一套不变的规则划分(划分),可能需要考虑更多因素,比如:不同团队、适配拆分、性能伸缩性等)
...(多余的点我就不列了,可能更多点Google的更好)
实际上软件的复杂性来自于业务的逐渐扩展,软件的复杂性也逐渐升高,复杂性不可能因为一项技术的引入而降低,也没有银弹能真正解决所有问题,我们只能借助DDD的一些方法和思想,让我们可以降低这些问题的复杂性,让我们更加专注业务的问题而已。
领域驱动设计
这里我们引用一下DDD的官方解释:
领域驱动设计是一种处理理⾼高度复杂域的设计⽅方法,试图分离技术实现的复杂性, 围绕业务概念构建领域模型来控制业务的复杂性,以解决软件难以理理解, 难以演化等问题。团队应⽤用它可以成功地开发复杂业务软件系统,使系统在增⼤大时仍然保持敏敏捷。
When you remember that DDD is really just “OO software done right”, it becomes more obvious that strong OO experience will also stand you in good stead when approaching DDD.
嗯,很难理解,我当初看了很多遍也没理解,我觉得可以把它归结为上面说到的几个字:用程序去描述客观事实,教我们如何系统的理解业务和将业务落地到实现的一套方法论,它是一套方法论,主要是为了解决软件的复杂性问题,让我们开发人员更好的理解业务、实现业务、演进业务的发展的一种方法。我们先从两个基本原则来了解它:
领域驱动设计的原则
综上,那么我认为的DDD设计原则就两点:
- 统一语言
统一语言并不陌生,《架构整洁之道》书中有一个经典的例子:盲人摸象,每个人形容的大象是不一样的,有的认为像蛇(尾巴),有人认为像墙(肚子),有人认为像柱子...
这些也告诉我们,我们在业务理解时,可能某些时候都只关注了自己的部分,技术人员关注技术名词,业务人员关注业务表达,而我们DDD的核心原则就是首先要让我们的语言统一,让我们大家表达的含义一致,定义领域模型术语,无论从事件风暴到划分限界上下文(统一问题域和核心域),最后到模型的字段定义(假想一下你和你的同事都定义了一个业务含义相同的“发货单号”字段),统一语言这一点都会是贯穿整个项目周期的。
- 用面向对象的思想实现业务
这个词我第一次理解的时候也感觉到很惊讶,高大上的DDD其实就是在表达一个思想:我们的软件落地时其实就是在描述的是我们生活中真正客观存在的事物。 了解这个观点,我们需要先了解两个问题:
- 我们软件为什么要分层?
实际上就是把逻辑和控制的分离,从MVC到洋葱模型再到六边形架构,甚至是整洁架构,他们的核心都是在:Domain(业务逻辑载体)。如下图所示:
- MVC 强调Model、View、Controller分离,让我们将逻辑和控制分离,业务逻辑凝聚到Service层
- 六边形架构是一种架构模式,强调业务逻辑内聚,所有的控制处理都在端口和适配层处理,不变的是业务逻辑,变化的是上下游接口适配
- 整洁架构应该是上面的一种更细粒度、更完善的一种分层思想,将所有的接口、数据库、用户交互都放在了外层,我们更加关注业务逻辑,而承载这个逻辑的业务模型叫做业务实体
它们都有一个很大的共性:业务逻辑是我们关注的核心,所有外部的交互或者技术实现,只是支撑而已,软件设计的同时就需要抛开这些因素,只关注我们最关心的部分:核心业务逻辑
我们的业务、需求都是核心逻辑,而在外圈的都是适配、接口、动作,我们应该关注业务,关注业务的实现,此时就出现了业务的承载体:领域模型
- 我们为什么设计模型?
我们的业务就是核心逻辑,我们需要使用更加丰富、饱满的充血模型(接下来会细说)来表达,此时的对象就是我们关注的核心域(接下来会细说)载体
充血模型 VS 贫血模型
这个问题,我们应该从OO思想加组合来理解:假设要用程序来描述人要进食,这个过程需要人体的各个器官进行协调完成,首先是通过外部方式将食物送进口腔,由牙齿的咀嚼和舌头的搅拌,再由喉咙吞咽,从食道进入胃中,在通过胃里进行初步消化,将饮食变成食糜,然后传入小肠后,在脾的运化作用下,营养物质被吸收;
整个过程中,人这个“实体模型”由吃食物这个动作触发,然后由人体内部的多个参与的部分对象协调完成的,这个就是组合和局部的关系,我们并不需要外力(service)将食物碾碎之后设置(setter)给胃(胃此时就是一个贫血模型=值对象),人对外暴露的只有吃食物这个方法,我们需要做的就是将食物送到嘴部,整个人体器官就会协调完成消化的过程;我们如果按照充血模型这种方式建模,那么代码形状应该是下面的形状:
public class Person {
private final Mouth mouth;
private final Esophagus esophagus;
private final Stomach stomach;
private final Intestine intestine;
public Person() {
this.mouth = new Mouth();
this.esophagus = new Esophagus();
this.stomach = new Stomach();
this.intestine = new Intestine();
}
public void eat(Object food) {
// 进食。
Object paste = this.mouth.chew(food); // 咀嚼形成浆糊。
paste = this.esophagus.transfer(paste); // 通过食道传送食物。
Object chyme = this.stomach.fill(paste); // 填充到胃里形成食糜。
this.intestine.absorb(chyme); // 在肠道里吸收营养
// ... 成长
// ... 便秘
}
// 嘴
private static class Mouth {
public Object chew(Object food) {
return food;
}
}
// 食道
private static class Esophagus {
public Object transfer(Object paste) {
return paste;
}
}
// 胃
private static class Stomach {
public Object fill(Object paste) {
return paste;
}
}
// 肠道
private static class Intestine {
public void absorb(Object chyme) {
// absorbing...
}
}
}
如果按照传统的方式建模:那么就是外部有一个嘴,外部力量将食物设置给嘴,调用嘴咀嚼的方法,然后用service将食物再设置给食道...贫血模型如下所示
public class AnemiaPerson {
private Object nutrition;
public void set(Object nutrition) {
this.nutrition = nutrition;
}
}
class AnemiaPersonService {
public static void main(String[] args) {
AnemiaPerson person = new AnemiaPerson();
// 进食。
Mouth mouth = new Mouth();
Esophagus esophagus = new Esophagus();
Stomach stomach = new Stomach();
Intestine intestine = new Intestine();
Object paste = mouth.chew(new Object()); // 咀嚼形成浆糊。
paste = esophagus.transfer(paste); // 通过食道传送食物。
Object chyme = stomach.fill(paste); // 填充到胃里形成食糜。
Object nutrition = intestine.absorb(chyme); // 在肠道里吸收营养
person.set(nutrition);
// ... 成长
person.growing();
// ... 便秘
person....();
}
}
// 嘴
class Mouth {
public Object chew(Object food) {
return food;
}
}
// 食道
class Esophagus {
public Object transfer(Object paste) {
return paste;
}
}
// 胃
class Stomach {
public Object fill(Object paste) {
return paste;// ...
}
}
// 肠道
class Intestine {
public Object absorb(Object chyme) {
// absorbing...
}
}
相信这个例子,大家也理解了充血模型(实体)和贫血模型(值对象)的关系,其实就是面向对象的本质:一个对象是拥有状态和行为的,而不是数据的承载体,而其中我们最关注的逻辑:业务逻辑都在充血模型中,它是我们的核心组成部分。
实际上,这种方式建模和传统的UML建模最大的区别就是:我们只是用程序的语言描述了业务的细节,并且业务人员也可以参与进来指导我们的软件设计;UML在建模时常常强调1:1、1:N...关系等,但实际上那是技术(关系型数据库)的部分,真正了解业务的业务人员也没有参与进来,自然也看不懂这些设计(软件就朝着每个技术人员理解的不同方向发展,最后成为一个大泥球),数据的存储可以是DB亦或是文件,和领域建模应该没有关系,技术实现只是细节,我们应该从业务的角度去思考模型的组成,而其中OO思想是核心,DDD就是指导我们如何将业务和技术拆分的一个方法论。
小节
本文从个人的经历出发,面向对象和领域驱动设计是相辅相成的,领域驱动设计就是:用程序去描述客观事实,教我们如何系统的理解业务和将业务落地到实现的一套方法论,后来我们了解了DDD的理论,最后用几个小栗子熟悉了DDD的设计原则:
- 统一语言
- 面向对象的思想实现业务
基于这两个原则,顺便还讲解了充血模型和贫血模型的区别:对象是拥有状态和行为,而不是数据的承载体。它与传统的UML方式设计和建模最大的区别就是:我们只是用程序的语言描述了业务的细节,让真正理解业务的人员参与进来,明确我们的软件设计和方向,使用“统一语言”指导软件朝着核心价值方向发展。
本文到此暂时告一段落,DDD分为战略设计和战术设计,涉及的名词文中都有一部分提及,接下来我会逐渐从战略到战术分析和解析,逐一解释和总结。
最后这里抛一个问题,胃、肠道、食道、嘴...等器官都是人的一部分,这个时候的人将所有的器官组合,他们的生命周期是一致的,并且是强依赖性,没有办法相互独立;与之对应的,还有我们的主角就是聚合:表达一组内聚关系的相关对象集合,那么他们的关系是什么呢?我们为什么要用聚合?