一文搞懂DDD

最近看了很多和DDD相关的内容,这篇文章对DDD做一个总结,希望可以通过这篇文章不但知道什么是DDD,而且还可以知道如何实施DDD

一、什么是DDD

DDD(领域驱动设计) 的研究方法与自然科学的研究方法类似。当人们在遇到复杂问题时,通常的做法就是将问题一步一步地细分,再针对细分出来的问题域,逐个深入研究,探索和建立所有子域的知识体系。DDD 是一种处理高度复杂领域的设计思想,它试图分离技术实现的复杂性,并围绕业务概念构建领域模型来控制业务的复杂性,以解决软件难以理解,难以演进的问题。DDD不是架构,而是一种架构设计方法论,它通过边界划分将复杂业务领域简单化,帮我们设计出清晰的领域和应用边界,可以很容易地实现架构演进。

DDD 包括战略设计和战术设计两部分,战略设计主要从业务视角出发,建立业务领域模型,划分领域边界,建立通用语言的限界上下文,限界上下文可以作为微服务设计的参考边界。如何进行战略涉及:

1.1 战略设计

DDD 战略设计会建立领域模型,领域模型可以用于指导微服务的设计和拆分。事件风暴是建立领域模型的主要方法,它是一个从发散到收敛的过程。它通常采用用例分析、场景分析和用户旅程分析,尽可能全面不遗漏地分解业务领域,并梳理领域对象之间的关系,这是一个发散的过程。事件风暴过程会产生很多的实体、命令、事件等领域对象,我们将这些领域对象从不同的维度进行聚类,形成如聚合、限界上下文等边界,建立领域模型,这就是一个收敛的过程。在战略层面,DDD非常强调针对业务问题的分析和分解,通过识别核心问题域来降低分析的复杂度。

1.2 战术设计

领域驱动设计DDD在战术建模上提供了一个元模型体系(如下图),通过这个元模型我们会对战略建模过程中识别出来的问题子域进行抽象,而通过抽象来指导最后的落地实现。这里我们谈的战术阶段实际就是这样一个抽象过程。这个抽象过程由于元模型的存在实际是一定程度模式化的。这样的好处是并非只能技术人员参与建模,业务人员经过一定的培训也是完全可以理解的。在带领不少团队实践建模的过程中,业务人员参与战术设计也是我要求的。


二、基础概念

领域:DDD 的领域就是这个边界内要解决的业务问题域,也就是范围、边界。简单来说,就认为是公司的某块业务好了。
子域:领域可以进一步划分为子领域。我们把划分出来的多个子领域称为子域,每个子域对应一个更小的问题域或更小的业务范围。
子域,核心域:决定产品和公司核心竞争力的子域是核心域,它是业务成功的主要因素和公司的核心竞争力。
子域,通用域:没有太多个性化的诉求,同时被多个子域使用的通用功能子域是通用域。
子域,支撑域:还有一种功能子域是必需的,但既不包含决定产品和公司核心竞争力的功能,也不包含通用功能的子域,它就是支撑域。
通用语言:团队需要一种通用语言来进行沟通。这样的通用语言尽量以业务语言为主,而非技术语言。一开始的通用语言可能不尽完美,但它就像是代码一样,经常需要重构。例如:“创建一个订单”就比“插入一条订单数据”更容易让领域专家明白谈话的背景。
限界上下文:用来封装通用语言和领域对象,提供上下文环境,保证在领域之内的一些术语、业务相关对象等(通用语言)有一个确切的含义,没有二义性。比如用户,在推荐好友(可能关注年龄、性别、地域)或是浏览商品(可能关注喜好、历史购买记录)的时候有着不同的含义。

关于这些概念可以看美团的这个图理解一下:

理论上限界上下文就是微服务的边界。我们将限界上下文内的领域模型映射到微服务,就完成了从问题域到软件的解决方案
值对象:当一个对象用于对事务进行描述而没有唯一标识时,它被称作值对象(Value Object)。例:地址都是中国北京市西城区。代码中的形态:

实体:当一个对象由其标识(而不是属性)区分时,这种对象称为实体(Entity)。比如:最简单的,公安系统的身份信息录入,对于人的模拟,即认为是实体,因为每个人是独一无二的,且其具有唯一标识(如公安系统分发的身份证号码)。

在DDD中,实体和值对象是很基础的领域对象。实体一般对应业务对象,它具有业务属性和业务行为;而值对象主要是属性集合,对实体的状态和特征进行描述。但实体和值对象都只是个体化的对象,它们的行为表现出来的是个体的能力。

聚合:是一组相关对象的集合,作为一个整体被外界访问,可以理解为聚合由业务和逻辑紧密关联的实体和值对象组合而成的,聚合是数据修改和持久化的基本单元,每一个聚合对应一个仓储,实现数据的持久化。聚合有一个聚合根和上下文边界,这个边界根据业务单一职责和高内聚原则,定义了聚合内部应该包含哪些实体和值对象,而聚合之间的边界是松耦合的。按照这种方式设计出来的微服务很自然就是“高内聚、低耦合”的。那怎么理解聚合呢,看下图:

聚合根:是这个聚合的根节点,主要目的是为了避免由于复杂数据模型缺少统一的业务规则控制,而导致聚合、实体之间数据不一致性的问题。聚合根也称为根实体,它不仅是实体,还是聚合的管理者。首先它作为实体本身,拥有实体的属性和业务行为,实现自身的业务逻辑。其次它作为聚合的管理者,在聚合内部负责协调实体和值对象按照固定的业务规则协同完成共同的业务逻辑。

值对象、实体、聚合以及聚合根描述完之后,怎么理解这三者的区别和联系呢?
1. 聚合根本身也是实体,它是整个聚合的入口,是全局唯一的
2. 实体就是我们理解的对象,有ID,有生命周期,只不过这个ID在聚合内唯一即可,生命周期也依赖于聚合根
3. 值对象其实就是字段,用于描述实体状态的

当业务功能涉及到多个聚合的时候,有多种方式进行处理,其中有两种方式使用比较普遍,一种是领域服务,另一种是领域事件。
领域服务:会导致在领域层会存在大量的领域服务类,这种方式实现的效果同传统的三层框架中的服务层实现的效果差不多,唯一区别只在于领域服务是将具体的业务逻辑放到领域对象中,而服务层是将所有的业务逻辑直接写在服务层中。

根据DDD的经典分层架构来看,应用服务属于应用层,用来表述应用行为;领域服务属于领域层,用来表述领域行为。应用行为描述了一个具体操作从开始到结束的每一个环节,而领域行为是对应用行为的细化,用来处理具体的某一个环节。比如,我们手机购物,从购物车结算这一场景来举例,这就是一个应用行为。而这个应用行为又主要包括金额计算、支付、生成订单,这些子环节就可以理解为一个领域行为。

领域事件:当一个业务功能涉及到多个聚合的时候,通过同步事件来进行聚合之间的交互,拿CQRS来说,领域对象中的业务逻辑是由命令处理器来调用的,当一个业务设计到聚合A-->聚合B-->聚合C三个聚合,但聚合A处理完相应的业务逻辑后,修改自身状态,然后发送领域事件到事件总线,然后由事件总线将这个事件立即分发到聚合B,并依次到聚合C,那就意味这在聚合A执行业务逻辑之前,必须手动地将聚合B和C同时注册到事件总线中(这个注册必须在之前的命令处理器中进行),当所有业务逻辑都处理完成之后再将其进行反注册,以取消B和C对A产生事件的关注。

领域服务 VS 领域事件
领域服务内部可以对多个领域对象(比如聚合根)进行操作。所以某些DDD框架将领域服务作为完成流程操作的主要工具,允许使用者在领域服务中注入多个仓储,从而对多个聚合根进行操作。而“领域事件”呢,它通过发布领域事件来达到不同领域对象的交互。那么到底应该使用“领域服务”还是“领域事件”呢? 先回答自己是否需要引入事件模型。如果“是”,那么请优先考虑使用领域事件。这是很容易让人头晕的两个对象,下面我将用两句话让您感受他们的使用场景:

A:快递在入库时需要进行规格检查,比如是否超重等
该场景,我们除了引入“快递”这一聚合根之外,没有引入其他领域对象。那么此处的“检查”操作,该行为应该交给谁呢? 给“快递”? 快递自己检查自己? 显然不对,所以当某行为不属于一个实体或者值对象时,我们就需要引入一个领域服务了。

B:当快递被投递到营业点时,证明快递已经到达,配送员将打电话给用户进行派送。
该场景中,我们已经发现了有“快递”、“营业点”、“快递员”等领域对象,如果要完成一个“快递到达”的用例,我们会如何操作呢?调用"营业点"的“收纳进快递”,并且接下来是调用“快递员”的“配送快递”。此处涉及到多个聚合根之间的交互,那么是选用领域服务还是领域事件呢? 如果您基于事件建模,可以采用领域事件,反之,您可以使用领域服务。

工厂
在针对大型的复杂领域进行建模时,聚合、实体和值对象之间的依赖关系可能会变得十分复杂。在某个对象中为了确保其依赖对象的有效实例被创建,需要深入了解对象实例化逻辑,我们可能需要加载其他相关对象,且可能为了保持其他对象的领域不变性增加了额外的业务逻辑,这样即打破了领域的单一责任原则(SRP),又增加了领域的复杂性。那如何去创建复杂的领域对象呢?因为复杂的领域对象的生命周期可能需要协调才能进行创建。这个时候,我们就可以引入创建类模式——工厂模式来帮忙,将对象的使用与创建分开,将对象的创建逻辑明确地封装到工厂对象中去。DDD中工厂的主要目标是隐藏对象的复杂创建逻辑;次要目标就是要清楚的表达对象实例化的意图。而工厂模式是计模式中的创建类模式之一。借助工厂模式我们可以很好实现DDD中领域对象的创建。

资源库
通俗点讲,资源库(Repository)就是用来持久化聚合根的。从技术上讲,Repository和DAO所扮演的角色相似,不过DAO的设计初衷只是对数据库的一层很薄的封装,而Repository是更偏向于领域模型。另外,在所有的领域对象中,只有聚合根才“配得上”拥有Repository,而DAO没有这种约束。DDD 中的仓储主要有两个核心功能:1. 持久化实体: 将实体持久化到底层的数据库中;2. 重建实体: 实体持久化后当我们需要再次使用的时候需要从数据库的数据重建实体。

在OrderRepository中,我们只定义了save()和byId()方法,分别用于保存/更新聚合根和通过ID获取聚合根。这两个方法是Repository中最常见的方法,Repository所扮演的角色只是向领域模型提供聚合根而已,就像一个聚合根的“容器”一样,这个“容器”本身并不关心客户端对聚合根的操作到底是新增还是更新,你给一个聚合根对象,Repository只是负责将其状态从计算机的内存同步到持久化机制中,从这个角度讲,Repository只需要一个类似save()的方法便可完成同步操作。

DAO和Repository关系
也许大家会觉得DAO是用来处理数据库的,Respository也是用来处理数据库的,那这两者之间有没有什么关系呢?在一般的业务中,我们通常使用DAO来解决数据表和代码模型之间的差异性,比如将某个POJO对象直接存取到数据库中,一般 DAO 和数据库表是一对一的关系,属于数据库敏感组件;Repository在DDD中用于解决实体模型和持久化逻辑之间的差异性,为业务逻辑屏蔽底层存储的具体细节,这时候业务模型是不感知底层数据表的,只关心自己的业务。Repository中通常一个实体会对应多张表结构,Repository是承上启下的组件;正因为仓储是针对实体维度的持久化,所以我们业务中只应该依赖于仓储的接口,当我们修改持久化逻辑的时候只需修改仓储实现或者替换新的实现类注入到业务层即可,不需要业务层做任何改动。

防腐层
亦称适配层。在一个上下文中,有时需要对外部上下文进行访问,通常会引入防腐层的概念来对外部上下文的访问进行一次转义。有以下几种情况会考虑引入防腐层:需要将外部上下文中的模型翻译成本上下文理解的模型;不同上下文之间的团队协作关系,如果是供奉者关系,建议引入防腐层,避免外部上下文变化对本上下文的侵蚀;该访问本上下文使用广泛,为了避免改动影响范围过大。

失血模型

Domain Object 只有属性的 getter/setter 方法的纯数据类,所有的业务逻辑完全由 business object 来完成。

贫血模型
简单来说,就是 Domain Object 包含了不依赖于持久化的领域逻辑,而那些依赖持久化的领域逻辑被分离到 Service 层。注意这个模式不在 Domain 层里依赖 DAO。持久化的工作还需要在 DAO 或者 Service 中进行。

充血模型
充血模型和第二种模型差不多,区别在于业务逻辑划分,将绝大多数业务逻辑放到Domain中,Service是很薄的一层,封装少量业务逻辑,并且不和 DAO 打交道:更加符合 OO 的原则;Service层很薄,只充当Facade的角色。

胀血模型
只剩下Domain Object和DAO两层,在Domain Object的Domain Logic上面封装事务。

三、领域驱动设计过程

 

参考以下文章中内容或图片
https://insights.thoughtworks.cn/ddd-architecture-design/
https://tech.meituan.com/2017/12/22/ddd-in-practice.html
https://www.cnblogs.com/laozhang-is-phi/p/9916785.html
https://www.cnblogs.com/netfocus/p/5145345.html
https://www.cnblogs.com/sheng-jie/p/7097129.html
https://www.cnblogs.com/uoyo/p/12421553.html
https://www.jdon.com/46490
http://xurui.pro/2019/05/22/%E9%A2%86%E5%9F%9F%E9%A9%B1%E5%8A%A8%E8%AE%BE%E8%AE%A1-%E8%81%9A%E5%90%88%E6%A0%B9/
https://www.cnblogs.com/sheng-jie/p/7215812.html
https://liberty-yuan.gitbooks.io/ddd/content/docs/ddd-factory-repo.html
https://www.cnblogs.com/davenkin/p/ddd-coding-practices.html
https://zhuanlan.zhihu.com/p/91525839
https://www.cnblogs.com/vivotech/p/12857823.html
http://dockone.io/topic/DDD

おすすめ

転載: blog.csdn.net/maoyeqiu/article/details/112788122