- 自我运算:根据业务规则对属性值进行运算的行为。
在进行领域设计建模时,要善于运用值对象而非内建类型去表达细粒度的领域概念。相比于内建类型,值对象的优势有:
- 值对象在类型层面就可以表达领域概念,而不仅仅依赖命名;
- 值对象可以封装领域行为,进行自我验证,自我组合,自我运算。
7.3.1.3 聚合
聚合的基本特征:
- 聚合是包含了实体和值对象的一个边界。
- 聚合内包含的实体和值对象形成一棵树,只有实体才能作为这棵树的根。
- 外部对象只允许持有聚合根的引用,以起到边界控制作用。
- 聚合作为一个完整的领域概念整体,其内部会维护这个领域概念的完整性。
- 由聚合根统一对外提供履行该领域概念职责的行为方法,实现内部各个对象之间的行为协作。
7.3.1.4 工厂
聚合中的工厂:一个类或方法只要封装了聚合对象的创建逻辑,都可以认为是工厂。表现形式如下:
- 引入专门的聚合工厂(尤其适合需要通过访问外部资源来完成创建的复杂创建逻辑)
- 聚合自身担任工厂(简单工厂模式)
- 服务契约对象或装配器(assembler)担任工厂(负责将外部请求对象DTO转换为实体)
- 使用构建者组装聚合
注意!这里工厂创建的基本单元是聚合,而非实体,注意与实体中的创建行为区分。
7.3.1.5 资源库
资源库是对数据访问的一种业务抽象,用于解耦领域层与外部环境,使领域层变得更为纯粹。资源库可以代表任何可以获取资源的仓库,例如网络或其他硬件环境,而不局限于数据库。
一个聚合对应一个资源库。领域驱动设计引入资源库,主要目的是管理聚合的生命周期。资源库负责聚合记录的查询与状态变更,即“增删改查”操作。资源库分离了聚合的领域行为和持久化行为,保证了领域模型对象的业务纯粹性。
值得注意的是,资源库的操作单元是聚合。当我们定义资源库的接口时,接口的入参应该为聚合的根实体。如果要访问聚合内的非根实体,也只能通过资源库获得整个聚合后,将根实体作为入口,在内存中访问封装在聚合边界内的非根实体对象。
资源库与数据访问对象(DAO)的区别:
根本区别在于,数据访问对象在访问数据时,并无聚合的概念,也就是没有定义聚合的边界约束领域模型对象,使得数据访问对象的操作粒度可以针对领域层的任何模型对象。数据访问对象(DAO)可以自由地操作实体和值对象。没有聚合边界控制的数据访问,会在不经意间破坏领域概念的完整性,突破聚合不变量的约束,也无法保证聚合对象的独立访问与内部数据的一致性。
其次,资源库是基于领域模型对存储系统进行的抽象,因此资源库中的方法命名可以表达领域概念;而数据访问对象(DAO)是存储系统对外暴露的抽象,其方法命名更贴合数据库本身的操作。
7.3.1.6 领域服务
聚合通过聚合根的领域行为对外提供服务,而领域服务则是对聚合根的领域行为的补充。因此,我们应该尽量优先通过聚合根的领域行为来满足业务服务。
那什么场景下我们会需要用到领域服务呢?有如下两个:
- 生命周期管理。为了避免领域知识的泄露,应用服务不会直接引用聚合生命周期相关的服务(工厂、资源库接口),而聚合根实体一般不会依赖资源库接口,此时就需要领域服务进行组合对外暴露。
- 依赖外部资源。为了保证聚合的稳定性,聚合根实体不会依赖防腐层接口。因此,当聚合对外暴露的服务需要设计外部资源访问时,就需要通过领域服务来完成。
7.3.1.7 领域事件
领域事件属于领域层的领域模型对象,由限界上下文中的聚合发布,感兴趣的聚合(同一限界上下文/不同限界上下文)可以进行消费。而当一个事件由应用层发布,则该事件为应用事件。
引入领域事件首要目的是更好地跟踪实体状态的变更,并在状态变更时,通过事件消息的通知完成领域模型对象之间的协作。
领域事件的特征:
- 领域事件代表了领域的概念;
- 领域事件是已经发生的事实(表示事件的名称应该是过去时,比如Committed);
- 领域事件是不可变的领域对象;
- 领域事件会基于某个条件而触发。
领域事件的用途:
- 发布状态变更;
- 发布业务流程中的阶段性成果;
- 异步通信。
领域事件应该包含:
- 身份标识,即事件ID,为通用类型的身份标识;
- 事件发生的时间戳,便于记录和跟踪;
- 属性需要针对订阅者的需求,在增强事件和反向查询之间进行权衡。增强事件指属性中包含订阅者所需的所有数据;反向查询则是属性包含事件ID,当订阅者需要数据时通过事件ID进行反向查询。
在领域设计模型中,聚合是最小的设计单元。
7.3.2.1 设计的经验法则
这里有四条经验法则:
- 在聚合边界内保护业务规则不变性。
- 聚合要设计得小巧。
- 通过身份标识符关联关系其他聚合。
- 使用最终一致性更新其他聚合。
下面展开讲述法则1和法则3。
法则1 在聚合边界内保护业务规则不变性。
法则1包含了两个关键点:a) 参与维护业务规则不变性的领域概念应该置于同一个聚合内;b) 在任何情况下都要保护业务规则不变性。比如,在sms系统中分数和绩点具有转换关系,这是业务规则的不变性,因此这两个概念被放在了同一个聚合边界内;当出现老师修改分数的场景时,需要保证绩点的换算同时被执行。由于这里绩点对象是值对象,不需要关心其生命周期管理的问题。当业务规则涉及到多个实体时,就需要通过本地事务来保证规则不变性(即实体间基于业务规则的数据一致性)。
法则3 通过身份标识符关联其他聚合。
注意这里强调了关联关系,关联关系会涉及聚合A对聚合B的生命周期管理的问题,对于这种聚合间的关联关系,我们通过身份标识建立关联。而当聚合A引用聚合B,但不需要对聚合B进行生命周期管理时,我们认为这是一种依赖关系(比如方法中的入参,而非类中的属性),对于聚合间的依赖关系,我们可以通过对象引用(聚合根实体的引用)的方式建立依赖。(PS:假设设计之初难以判断聚合之间到底是关联关系,还是依赖关系,我们就统一使用身份标识符作为关系引用即可)
聚合间的依赖关系通常分为两种方式
- 职责的委派:一个聚合作为另一个聚合的方法参数, 就会形成职责的委派。
- 聚合的创建:一个聚合创建另外一个聚合,就会形成实例化的依赖关系。
7.3.2.2 设计步骤
1. 理顺对象图
分析对象是实体还是值对象。
2. 分解关系薄弱处
聚合本质是一个高内聚的边界,因此我们可以根据领域对象之间关系的强弱来定义出聚合的边界。对象间的关系由强到弱可以分为:泛化关系,关联关系和依赖关系。其中关联关系和依赖关系在 7.3.2.1 小节已讲述,而泛化关系可以理解为是继承关系(即父子关系)。
泛化关系
虽然泛化关系是强耦合关系,但是根据对业务理解的视角不同,会产生不同的设计:
- 整体视角:调用者并不关心特化的子类之间的差异,而是将整个继承体系视为一个整体。此时应以泛化的父类作为聚合根。
- 独立视角:调用这只关注具体的特化子类,体现了概念的独立性,此时应以特化的子类作为独立的聚合根。
关联关系
上述提到过,聚合间的关联关系会涉及聚合A对聚合B的生命周期管理,这其实是一个比较宽松的约束。那聚合内实体的关联关系应该是怎么样的呢?生命周期一致的、共存亡的,当主实体被销毁时,从实体也随之会被销毁。比如商品实体和商品明细实体。而在示例-SMS中,成绩和总成绩会被定义为两个聚合,原因是总成绩在成绩锁定后被统计,随后将不再发生改变,可见两者不存在上述的共存亡的关联关系。
PS: 实际上根据关联关系来区分边界的方法同样适用于限界上下文的边界划分。比如示例-SMS中的课程和成绩生命周期不同,先有课程,后有成绩;而且成绩锁定后,课程被撤销也不会对成绩有影响,因此就可以定义出课程上下文和成绩上下问。
依赖关系
依赖关系主要体现的是实体间的职责委派和创建行为,可以分到不同的聚合边界。
3. 调整聚合边界
根据业务规则调整聚合边界。为了维护业务规则的不变性,相关的实体应该至于同一个聚合边界内。
7.3.3 设计服务这里的服务是对应用服务、领域服务、领域行为(实体提供的方法)和端口(资源库接口、防腐层接口)的统称。
7.3.3.1 分解任务
业务服务包含若干个组合服务,组合服务包含若干个原子服务。领域行为和端口都可以认为是原子服务。
7.3.3.2 分配职责
应用服务:匹配业务服务,提供满足业务需求的服务接口。应用服务自身并不包含任何领域逻辑,仅负责协调领域模型对象,通过它们的领域能力组合完整一个完整的应用目标。
领域服务:匹配组合服务,执行业务功能,若原子任务为无状态行为或独立变化的行为,也可以匹配领域服务。控制多个聚合与端口之间的协作,由它来承担组合任务的执行。
领域行为:匹配原子服务,提供业务功能的业务实现。强调无状态和独立变化,由实体提供。
端口:匹配原子服务,抽象对外资源的访问,主要的端口包括资源库接口和防腐层接口。
虽然上述给出了应用服务、领域服务、领域行为和端口与业务服务、组合服务和原子服务的匹配关系,但是对于应用服务、领域服务、领域行为和端口之间的关联关系却还不清晰,这里结合书中内容和个人实践给出一个参考。
应用服务:核心职责是编排聚合间的领域服务。
- 领域服务
- 防腐层接口:当多聚合间领域服务进行协作后需要访问外部资源,此时相关的防腐层逻辑应该至于应用层。(防腐层是上下文映射的方式,并非领域模型特有)
- 工厂:特指服务契约对象或装配器担任工厂,即将DTO转换为实体的工厂。
- 领域行为:在上述工厂创建实体后,若只需要调用实体的领域行为,而不需要涉及生命周期管理,可直接在应用服务中进行调用。
领域服务:细粒度的领域对象可能会把领域层的知识泄露到应用层中。这产生的结果是应用层不得不处理复杂的、细致的交互,从而使得领域知识蔓延到应用层或用户界面代码当中,而领域层会丢失这些知识。明智地引入领域层服务有助于在应用层和领域层之间保持一条明确的界限,因此应用层多数情况下也不会直接引用聚合的领域行为。
- 工厂
- 领域行为
- 防腐层接口:聚合内需要依赖外部资源,则将防腐逻辑收拢在领域服务中。
- 资源库接口
领域行为:不要关联资源库和防腐层接口。
7.3.4 示例-SMS的领域设计模型
聚合设计:
服务设计:
下面只罗列非查询类的服务设计。