基础
学习DDD看的书
- 《实现领域驱动设计》
基础只是,概念- 《领域驱动设计:软件核心复杂性应对之道 》
使用,流程,串联
千万不要先看 《领域驱动设计:软件核心复杂性应对之道 》,不然你会哭的,因为你看的和天书基本没啥区别,要先看 《实现领域驱动设计》,然后在去看 埃文斯的那本书。
领域模型
领域模型是某个业务领域的软件模型,通常通过对象模型来实现。
贫血模型和充血模型
- 在贫血模型中,对象中只有属性值和公有的get set 方法,几乎没有业务逻辑,完全就是一个数据集。
- 在充血模型中,数据与对应的业务逻辑被封装在一个类中,这种模型满足面向对象的封装特性,是典型的面向对象编程。
- 充血模型和贫血模型是领域对象和非领域对象的重要区别。
失忆症
- 当业务逻辑出现多次变化的适合,直接散落在项目中的各个业务逻辑很可能忘记其本来的意图,而业务的迭代,可能会导致项目越来越复杂,这样就会出现,除作者以外,甚至作者,看这段代码的适合会蒙圈,这种一般称为贫血模型导致的失忆症。
是充血模型的好处
- 复用程度高,表达能力强,适合复杂的业务逻辑处理
- 业务逻辑的变动,很可能会导致领域模型的变动,但是,这个在传统的开发中也是不可避免的,但是充血模型将逻辑高内聚,反而不容易漏改逻辑
举例 -- 这个可以不看
一个简单的主子订单模型,加上付款操作
在传统模式下,可见我们的对象只是一个数据集合,除了传递数据,并没有做任何操作,这个只是一个简单的付款,如果之后,业务场景复杂后,很可能出现,这个一个业务逻辑,哪里一个业务逻辑
public class Order {
private Long orderId;
private String orderNo;
private Long amount;
private Integer status;
---get set 方法 ---
}
public class OrderItem {
private Long id;
private Long orderId;
private Long orderNo;
private String itemName;
private Long payAmount;
--- get set 方法---
}
付款方法
public void pay(OrderReq orderReq){
Order order = dao1.query(orderReq);
List<OrderItem> orderItems = dao2.query(orderReq);
---业务逻辑
order.setStatus(PAY);
for (OrderItem orderItem : orderItems) {
----业务逻辑
orderItem.setPayAmount(orderReq.getPayAmount());
}
transaction.execute(){
dao1.save(order);
dao2.save(orderItems);
}
}
业务更新后,很可能
public void pay(OrderReq orderReq){
Order order = dao1.query(orderReq);
List<OrderItem> orderItems = dao2.query(orderReq);
---业务逻辑
order.setStatus(PAY);
if(会员){
order.setStatus(PAY1);
}
if(优惠){
order.setStatus(PAY2);
}
for (OrderItem orderItem : orderItems) {
----业务逻辑
if(优惠){
orderItem.setStatus(PAY2);
}
if(会员){
orderItem.setStatus(PAY1);
}
orderItem.setPayAmount(orderReq.getPayAmount());
}
transaction.execute(){
dao1.save(order);
dao2.save(orderItems);
}
}
领域是什么
在实现领域驱动一书中这样解释
从广义上讲,领域(Domain)即是一个组织所做的事情以及其中所包含的一切。商业机构通常会确定一个市场,然后在这个市场中销售产品和服务。每个组织都有它自己的业务范围和做事方式。这个业务范围以及在其中所进行的活动便是领域。当你为某个组织开发软件时,你面对的便是这个组织的领域。这个领域对于你来说应该是明晰的,因为你在这个领域中工作。
在我看来,领域其实就是我们的业务模块,或者说是我们的实际业务就是领域。
子域
对一个大的领域的问题进行根据业务职能的划分,例如在一个电商项目中,商品,订单,支付,履约,结算,库存,物流,发票,用户就是不同的子域。而在子域的划分中,也会产生不同的类型,核心子域,通用子域,支撑子域等等。
限界上下文
- 特定模型的限界应用。限界上下文使团队所有成员能够明确地知道什么必须保持一致,什么必须独立开发。
- 限界上下文为业务流程在一个划定的界限,由一个或者多个子域组成。
- 在基于某一个角度,将多个子域划分进一个限界上下文
- 限界上下文即是一个特定的解决方案,在一个特定的限界上下文只使用一套通用语言,并且保证它的清晰性和简洁性。
问题空间和解决问题空间
- 问题空间:是我们业务所遇到的挑战,问题空间是领域的一部分,对问题空间的开发将产生一个新的核心域,问题空间是核心域和其他子域的组合。
- 解决问题空间:如何实现软件以解决这些业务挑战,包括一个或多个限界上下文,即一组特定的软件模型来解决问题。
上下文映射图
-
表示限界上下文和他们呢的集成关系,下面就是一个上下文映射图,
U表示上游(Upstream),D表示下游(Downstream)
领域驱动架构
系统分层

- 一般我们使用聚合或者工厂对复杂的聚合进行创建,而不是直接在应用层创建
- 领域模型用于发布领域事件时,应用层可以将订阅方注册到任意数量的事件上,这样的好处是可以对事件进行存储和转发。同时,领域模型只需要关注自己的核心逻辑;领域事件发布器也可以保持轻量化,而不用依赖于消息机制的基础设施,可以使用Spring的event事件。

命令和查询职责分离——CQRS
- 如果一个方法修改了对象的状态,该方法便是一个命令(Command),它不应该返回数据。在Java和C#中,这样的方法应该声明为void。
- 如果一个方法返回了数据,该方法便是一个查询(Query),此时它不应该通过直接的或间接的手段修改对象的状态。在Java和C#中,这样的方法应该以其返回的数据类型进行声明。
防腐层
- 当你的领域和其他领域存在数据交互时,你需要一个防腐层作为两个领域之间的纽带。这会给你带来很多“数据转换”的代码,但是对于业务多变的系统来讲,它能保护你的领域。
- 在我们的微服务架构中,一般来说指当前服务与其他服务进行交互的适合,并不直接调用,而由一个中间层,来进行判断,处理,缓冲。
实体
- 一种对象,它不是由属性来定义的,而是通过一连串的连续事件和标识定义的。
- 实体应该具有唯一标示,如订单号,身份证号,用户id
- 唯一标示可以有用户产生(如手机号)/系统产生(如uuid)/持久化机制生成(如mysql主键)
- 实体的其他信息可以进行修改,但是无论如何修改,唯一标示是不变的,其还是一个相同的实体。
值对象
- 一种描述了某种特征或属性但没有概念标识的对象。
- 度量或描述,它度量或者描述了领域中的一件东西。
- 不变性,可以作为不变量,一个值对象创建了就不能改变了
-
概念整体,它将不同的相关的属性组合成一个概念整体。一个值对象可以只处理单个属性,也可以处理一组相关联的属性。在这组相关联的属性中,每一个属性都是整体属性所不可或缺的组成部分,这和简单地将一组属性组装在对象中是不同的。如果一组属性联合起来并不能表达一个整体上的概念,那么这种联合并无多大用处。
即他可以是单个属性,也可以是多个属性,但必须是有关联性的,而不是一堆属性的聚合。 - 可替换性,当度量和描述改变时,可以用另一个值对象予以替换。
- 值对象相等性,它可以和其他值对象进行相等性比较
- 无副作用行为,它不会对协作对象造成副作用
最小化集成
在所有的DDD项目中,通常存在多个限界上下文,这意味着我们需要找到合适的方法对这些上下文进行集成。当模型概念从上游上下文流入下游上下文中时,尽量使用值对象来表示这些概念。这样的好处是可以达到最小化集成,即可以最小化下游模型中用于管理职责的属性数目。使用不变的值对象使得我们做更少的职责假设。
这里我理解的最小化集成,就是我们的限界上下文直接的参数应该是值对象
领域服务
- 领域中的服务表示一个无状态的操作,它用于实现特定于某个领域的任务
- 慎重使用领域服务,能在聚合和实体中完成的操作,不要放在领域服务中,过度使用领域服务将会造成贫血领域模型。
- 领域服务通常和领域内相关的聚合放在同一模块(目录)下。
什么是领域服务
- 执行一个显著的业务操作过程。
2.对领域对象进行转换。
3.以多个领域对象作为输入进行计算,结果产生一个值对象。
领域事件 Domain Event
- 领域事件用于发布和捕捉发生在领域内的一些事情,领域事件通常是由某个聚合发出,在整个领域范围内订阅。某某方法执行完成、某某数据修改成功、某某接口调用结束等等都可以理解为领域内事件,这些事件结束后我们往往需要做一些进一步的操作,比如:订单领域完成下单操作后,物流领域需要开始物流。这是在两个领域之间具备先后关系的事件,问题在于完成下单之后怎么通知物流领域呢?直接调用吗?领域事件认为,在有需要时,当订单创建完成,应当对外发布一个携带了必要对象信息的订单创建完成事件(OrderCreatedEvent)消息,由订阅方订阅到该事件消息,进而完成后续业务。
- 领域事件是对领域内发生的活动进行的建模
- 我觉的领域事件就使用事务消息就比较好,别的书里说的感觉用处不大
模块
- 可以理解为 package分包
- 模块表示了一个命名的容器,用于存放领域中内聚在一起的类。
- 将类放在不同模块中的目的在于达到松耦合性。
- 由于DDD中的模块并不是一个通用的存储区域,因此对其进行适当的命名是重要的。
- 事实上,模块名是通用语言的重要组成部分。
- 模块应该包含一组具有高内聚性的概念集合,这样做的好处是可以在不同的模块之间实现松耦合
模块的命名规范
- 通常模块名就是包名,一般来说是公司层面统一的
- 先考虑模块,再是限界上下文,因为模块的目的在于组织那些内聚在一起的领域对象
聚合
- 将实体和值对象在一致性边界之内组成聚合(Aggregate)
- 聚合就是一组相关对象的集合,我们把聚合作为数据修改的单元。外部对象只能引用聚合中的一个成员,我们把它称为根。在聚合的边界之内应用一组一致的规则。
聚合的原则
- 在一致性边界之内建模真正的不变条件:
不变条件是指某个业务规则,这个规则应该总是保持一致的。这里的一致通常是指事务一致性,因为聚合作为我们操作和读取数据的基本单元,这就要求我们在提交事务时,该聚合边界内的所有对象都应当保持一致。 即在一个事务中只修改一个聚合实例,需要保证事务一致性 - 设计小聚合
因为聚合需要保证在一个事务中只能修改一个聚合,所以聚合需要更加紧凑,逻辑更加一致,保证在单个聚合的事务一致性,多个聚合直接,需要实现最终一致性。 - 通过唯一标识引用其他聚合
即聚合直接引用的适合使用唯一标示引用,而不是直接引用聚合对象。在java中,通俗标示,就是我们因为另一个聚合的唯一id,而不是直接引用对象,这样我们的聚合会更加轻量级,减少内存,对与java来说,对gc也有好处。 - 在边界之外使用最终一致性
即多个聚合直接需要保持最终一致性,一般使用事件驱动来完成最终一致性
工厂 Factory
- 将创建复杂对象和聚合的职责分配给一个单独的对象,该对象本身并不承担领域模型中的职责,但是依然是领域设计的一部分。工厂应该提供一个创建对象的接口,该接口封装了所有创建对象的复杂操作过程,同时,它并不需要客户去引用那个实际被创建的对象。对于聚合来说,我们应该一次性地创建整个聚合,并且确保它的不变条件得到满足
- 隐藏创建细节,方便调用
资源库 Repository
- 我理解的在我们的开发过程中,资源库就是我们的dao层,与数据存储进行交互的。一般资源库分为面向集合的资源库和面向持久化的资源库
- 面向集合的资源库
面向集合的资源库是指资源库就像一个集合,在向一个Set集合中添加聚合时,如果重复添加是不会成功的,从集合中获取到的实例也可以直接进行修改而不需要写会操作,因为从集合中获取到的对象是引用类型,这意味着任何修改都会直接生效。类似Hibernate,但是用的不多
- 面向持久化的资源库
面向持久化的资源库就是我们熟悉的数据库操作,但是我们需要把实现细节进行封装,资源库应该尽可能地模拟一个集合的操作方式,将细节封装,例如防止聚合添加重复的校验不再由客户端做,调用方不关心细节,可以完全作为一个集合的方式使用。
还有
- 对事务的管理绝对不应该放在领域模型和领域层中,而应该放在应用层
资源库和dao的区别
- DAO主要从数据库表的角度来看待问题,并且提供CRUD操作,DAO相关的模式通常只是对数据库表的一层封装。
- 而资源库采用面向集合的方式,而不是面向数据访问的方式。
- 所以 资源库 可以看作 DAO,但是DAO不一定是资源库
基础设施
基础设施的职责是为应用程序的其他部分提供技术支持。
集成限界上下文
- 常见的手段包括开放领域服务接口、开放HTTP服务以及消息发布-订阅机制。
领域对象的生命周期

- 使用FACTORY(工厂)来创建和重建复杂对象和AGGREGATE(聚合),从而封装它们的内部结构。
- 在生命周期的中间和末尾使用REPOSITORY(存储库)来提供查找和检索持久化对象并封装庞大基础设施的手段。
https://zhuanlan.zhihu.com/p/350033901
https://tech.meituan.com/2017/12/22/ddd-in-practice.html
