领域驱动设计标准分层架构
四层模型演变
当前,业界比较通用的DDD架构采用的是四层模型,从下到上依次为基础设施层、领域层、应用层和用户接口层。具体的分层架构见图。
这里我们主要聊聊领域层设计。
领域层
领域层,亦称模型层,属于业务系统中最核心的一层,整个系统中几乎所有的业务逻辑均会在该层实现。本层主要包含领域模型和领域服务。
(1)领域模型
领域模型用来抽象复杂的业务逻辑,将其转换为便于理解的概念图模型,一般由实体和值对象构成。它与数据模型的不同点在于:数据模型描述的是对象的持久化方式,而领域模型表述的是领域中各个类,以及各类之间的关系。
(2)领域服务
领域服务可以认为是领域模型的一种补充,因为在实际建模过程中,一些概念本质上是一些操作,它们会涉及到多个领域对象,并且需要协调这些领域对象来完成这个操作,如果强行将这个操作归类到某个对象,那么这个对象就会承担一些本不属于它的职责,进而出现对象职责不明确的现象,此时,就需要领域服务来承载这些操作,用来串联多个领域对象。比如,在书籍管理项目中,我们在建模时,考虑到合理性将书籍和章节分别定义为了单独的实体。当作者要发布书籍新章节内容时,我们需要同时新增章节内容 和 更新书籍章节总数信息,这时我们便引入了领域服务,用来承载聚合操作。
如下是一个用户转账的领域服务,涉及2个领域对象,一个是源账号,另一个是目标账号。
public class AccountTransferService {
public void transfer(Account source,Account target,Money money) {
}
}
DDD几个核心领域概念
实体 —— 唯一标识
实体对应的英语单词为Entity。提到实体,你可能立马就想到了代码中定义的实体类。在使用一些ORM框架时,比如Entity Framework,实体作为直接反映数据库表结构的对象,就更尤为重要。特别是当我们使用EF Code First时,我们首先要做的就是实体类的设计。在DDD中,实体作为领域建模的工具之一,也是十分重要的概念。
但DDD中的实体和我们以往开发中定义的实体是同一个概念吗?
不完全是。在以往未实施DDD的项目中,我们习惯于将关注点放在数据上,而非领域上。这也就说明了为什么我们在软件开发过程中会首先做数据库的设计,进而根据数据库表结构设计相应的实体对象,这样的实体对象是数据模型转换的结果。
在DDD中,实体作为一个领域概念,在设计实体时,我们将从领域出发。
实体是一个具有身份和连贯性的概念,它具有以下几个特征:
- 实体是数据(属性)和行为(业务逻辑关系)的结合体;
- 每个实体都有自己的唯一标识,判断两个实体对象是否相等,是通过唯一标识来判断的。比如,两个实体对象,如果唯一标识相等,即使其他属性不相等,这两个实体也会认为是同一个。实体的其他属性不相等,表征的是同一个实体在其生命周期的不同阶段。
- 实体的唯一标识属性值是不可变的,其他属性值是可变的。
值对象 —— 不变性
值对象一般会作为一个属性存放于一个实体内部,它具有以下几个特征:
- 值对象不需要唯一标识,判断两个值对象是否相等,是通过值对象内部所有属性值是否相等来判断的。
- 值对象的属性值是不允许变化的,即值对象的实体在创建之后就不会变了,如果要改变其属性值,就需要先把此对象删除,然后重新创建一个新对象。
订单对象中的商品信息、地址信息就是值对象。
书籍对象中的书籍分类,书籍标签等就是值对象。
聚合
聚合是一组具有内聚关系的领域对象(包括实体和值对象)的集合,这里的一组可以是一个或多个实体。每个聚合都会有一个根实体(亦称聚合根),它主要用来和外界交互,即外部对象如果想访问聚合内的实体,必须先访问聚合根,然后聚合根再和内部要访问的实体进行交互。
还是拿小说平台项目举例说明,一篇小说,它包含小说基础信息 [标题,封面,分类,书签,作者,书源等信息],这一组合就是一个聚合,其中,“小说信息”可以设置为这个组合的聚合根。
注意:
聚合内的内容具有一致性,即:需要在事务中修改一个聚合的内容。如果没有一致性要求,那么应该就不属于一个聚合。
通过唯一标识来引用其他聚合或实体。
如果聚合创建复杂,推荐使用工厂方法来屏蔽内部复杂的创建逻辑。
在传统数据模型中,一般认为每个实体都是对等的,可以单独修改任意一个实体;在DDD中,聚合内对象的修改必须按照统一的业务规则来完成,聚合是数据修改、持久化的基本单元。
聚合设计的原则:
设计小聚合。小聚合可以降低数据冲突,规避业务过大。
通过唯一标识引用其他聚合。
聚合内保持数据强一致,聚合外保持数据最终一致。
通过应用层实现跨聚合调用。
聚合代码
public class Aggregate<R extends Versionable> {
protected R root;
protected R snapshot;
protected DeepComparator deepComparator;
Aggregate(R root, DeepCopier copier, DeepComparator deepComparator) {
this.root = root;
this.snapshot = (Versionable)copier.copy(root);
this.deepComparator = deepComparator;
}
public R getRoot() {
return this.root;
}
public R getRootSnapshot() {
return this.snapshot;
}
public boolean isChanged() {
return !this.deepComparator.isDeepEquals(this.root, this.snapshot);
}
public boolean isNew() {
return this.root.getVersion() == 0;
}
}
什么是聚合根
简单的理解就是把关联紧密的实体放到一起,对外提供统一的访问,外界不能直接访问内部的实体。
--作者“杰克”
举例解释
简单的理解 聚合根就是一个泛型List,比如:学生有基本信息,班级信息;那么我们就把 “学生”当做聚合根, 这个学生聚合根里有有学生的基本信息和班级信息。
官方解释
聚合是一个关联对象的集群,我们将其作为一个单元来处理数据更改。每个集合都有一个根和一个边界。边界定义了聚合内部的内容。根是聚合中包含的单个特定实体。
根是聚合中唯一允许外部对象保存对[.]的引用的成员。
聚合根具有关联实体的ID,关联实体,以及对关联实体的行为,将关联比较紧密的实体组成一个整体,对外提供统一的访问。
怎样设计聚合?
DDD 领域建模通常采用事件风暴,它通常采用用例分析、场景分析和用户旅程分析等方法,通过头脑风暴列出所有可能的业务行为和事件,然后找出产生这些行为的领域对象,并梳理领域对象之间的关系,找出聚合根,找出与聚合根业务紧密关联的实体和值对象,再将聚合根、实体和值对象组合,构建聚合。
下面我们以小说阅读场景为例,看一下聚合的构建过程主要都包括哪些步骤。
仓储
首先说明下仓储被设计出来的初衷,在领域模型中,对象被创建出来后一般会在内存中活动,待其不活动了后,需要将其进行持久化存储。然后,当我们需要重建对象时,需要根据对象当前状态进行重建。可见这整个过程中,会频繁的与数据库(广义的数据库,包括关系型数据库、NoSql数据库等)打交道,进行对象的创建、组装等。因而,能否提供一种机制,帮助我们管理领域对象以及做对象持久化,仓储并应运而生了。
仓储,又称资源库,它具有以下几个特征:
- 仓储是连接领域层和基础设施层的桥梁,一般将仓储接口定义放在领域层,仓储的具体实现放在基础设施层。这样做的好处是:解耦了领域层与ORM之间的联系,任何ORM相关的变更,只需要修改仓储的实现便可,对于领域层仓储接口的定义一般是不需要做修改的。
- 仓储里面存储的对象一定是聚合,因为领域模型中都是以聚合来划分业务边界的,所以在实际应用中,我们只会对聚合设计仓储。同理,我们在仓储中做数据更新、删除等操作时,应该以聚合为单位进行操作,而不是仅操作聚合中的某一个实体。
仓储接口定义
/**
* 仓储范型接口
*
* @author yangyanping
* @date 2023-07-20
*/
public interface Gateway<K, T extends Versionable> {
Aggregate<T> getByKey(K key);
void store(Aggregate<T> aggregate);
void remove(Aggregate<T> aggregate);
}
Factory(工厂模式)
在创建对象时,有些聚合需要实体或值对象较多,或者关系比较复杂,为了确保聚合内所有对象都能同时被创建,同时避免在聚合根中加入与其本身领域无关的内容,一般会将这些内容交给Factory处理。
Factory的主要作用:封装聚合内复杂对象的创建过程,完成聚合根、实体、值对象的创建。
工厂类接口代码:
public interface DomainFactory<T> {
}
DDD学习
DDD(领域驱动设计)_ddd领域模型设计_小飞哥wzf的博客-CSDN博客
DDD-经典四层架构应用_ddd四层架构_是下雨天啊的博客-CSDN博客