本文作为学习笔记,内容来自“极客时间”专栏《手把手教你落地DDD》,如有侵权,请及时告知,必当及时删除。
1 聚合
聚合的两个重要特征:
- 具有整体与部分的关系。举个例子,有“员工”和“技能”两个领域对象,那么“员工”就是一个整体,而“技能”是“人”的一个部分,我们就可以说“员工”和“技能”具有整体和部分的关系。
- 具有不变规则,而且这种不变规则在并发的时候可能被破坏。要防止规则的破坏,仅仅锁住一条技能记录是不够的,必须把员工和所有技能作为一个整体锁住才能解决。或者说,员工和他的所有技能确定了一个事务边界。
具有这两个特征的一组领域对象,就叫聚合(Aggregate)。
聚合的表示方法:例如上面提到的“员工”和“技能”这样一组聚合,可以用下面这种方式表达出来。
在一个聚合里,像员工这样代表整体的实体就是聚合根。一个聚合只有一个聚合根。一般我们约定,聚合包的名字和聚合根的名字是一样的。
由聚合的概念,可以推出三条推论:
- 首先,作为部分的实体,只能属于一个聚合根,不可能属于多个聚合根。比如说,一条技能信息,只能属于一个员工,不能属于多个员工。又比如说,我的手只能是我一个人的手,不能同时又是其他人的手。
- 其次,我的手是不能“跳槽”的。不能今天是我的手,明天就变成了别人的手。也就是说,一个聚合的一部分,不能再变成其他聚合根的一部分。
- 再次,由前两条自然可以推出,聚合根被删除,那么聚合中的所有对象都会被删除。
- 最后,还有一个“标识”的问题。在业务上,为了识别每个实体,实体必然要有一个标识。例如,人的标识,可以是身份证号。如果这个人是学生,那么他的标识也可以是学号。注意,这里说的标识是一个业务概念,而不是技术概念,和数据库表中常见的没有业务概念的 ID 是不同的。
聚合的作用:
- 首先,聚合不仅是“被动地”实现不变规则,它还为我们提供了一个新的视角,可以更细致地和业务人员讨论业务规则。从这个视角去思考过去做过的系统,我们很可能会发现一些遗漏的业务规则。
- 其次,开发人员过去一般认为事务只是一个技术概念。现在我们可以看到,事务其实是来源于业务规则的,本质上是个业务问题。也就是说,聚合在业务规则和事务之间建立了起联系。
- 再次,我们在模型上为每个聚合建了一个包,可以认为,聚合是一种特殊的模块。这样,模型的层次就变得更清晰了。同时,我们也可以把聚合当作一个粗粒度的概念单位进行思考,降低了认知负载。
- 最后,不少开发人员编程时觉得事务范围的大小不好把握。聚合作为一个事务边界,给出了事务范围的下限,为开发时确定事务范围提供了参考。
聚合的实现:
通过上面的领域模型图,我们不难写出代码
// imports ...
public class Emp {
private Long id;
private Long orgId;
private String idNum;
private List<Skill> skills;
private List<WorkExperience> experiences;
// other getters and setters ...
// 对 skills、experiences 和 postCodes 的操作 ...
}
聚合外部对象对非聚合根对象只能读,不能写,必须通过聚合根才能对非根对象进行访问。例如,员工和技能作为一个聚合,外部对象想要访问员工的技能,不能直接访问技能,而是要通过员工这个聚合根,才可以访问到技能。
2 值对象
假设我们现在有一个业务场景,用来描述员工的工作经验,有开始时间、结束时间、工作单位三个属性,对于这个场景,我们有如下规则:
- 开始时间不能晚于结束时间;
- 时间段不能重叠。
如果我们的项目只有这一个场景用到开始时间、结束时间这两个属性,以及上述两条规则,那么我们可以把这个逻辑写到员工工作经验里面。
但是假设我们的项目里面,还有一个合同领域、项目领域,这两个领域也都有开始时间和节数时间,并且也要实现上面两条规则。
这个时候,我们可以创建一个时间段对象,把有关的数据和逻辑封装起来。领域模型如下所示:
image.png
根据这个领域模型,不难写出代码如下所示:
package chapter18.unjuanable.domain.orgmng.emp;
import java.time.LocalDate;
public class Period {
private LocalDate start;
private LocalDate end;
private Period(LocalDate start, LocalDate end) {
//创建时校验基本规则
if (start == null || end == null || start.isAfter(end)) {
throw new IllegalArgumentException(
"开始和结束日期不能为空,且结束日期不能小于开始日期!");
}
this.start = start;
this.end = end;
}
//用于构造对象
public static Period of(LocalDate start, LocalDate end){
return new Period(start, end);
}
// 判断是否与另一时间段重叠
public boolean overlap(Period other) {
if (other == null) {
throw new IllegalArgumentException("入参不能为空!");
}
return other.start.isBefore(this.end)
&& other.end.isAfter(this.start);
}
// 合并两个时间段
public Period merge(Period other) {
LocalDate newStart = this.start.isBefore(other.start) ?
this.start : other.start;
LocalDate newEnd = this.end.isAfter(other.end) ?
this.end : other.end;
return new Period(newStart, newEnd);
}
public LocalDate getStart() {
return start;
}
public LocalDate getEnd() {
return end;
}
}
应用了时间段对象的工作经验类就变成了后面这样。
//imports ...
public class WorkExperience {
private Long id;
private Period period;
protected String company;
protected WorkExperience(Period period, LocalDateTime createdAt, Long createdBy) {
super(createdAt, createdBy);
this.period = period;
}
// setters and getters ...
}
在 DDD 里,像员工这样有单独的标识(员工号),除了员工号,其他属性都可以改变的对象,就叫做实体(Entiy);像时间段这样没有单独的标识,并且各个属性都不可改变的对象,就叫值对象(Value Object)。
那么在程序上,怎么实现这种概念上的不变性呢?我们只需要把这些对象的属性值,作为构造器参数传入来创建对象,而不提供任何方法来改变对象就可以了。另外,如果在程序里面,想要实现对象的共享,可以参考设计模式中的“享元”。
值对象的主要优点是,不论在内存还是数据库里,都可以选择共享和不共享的方式。这种灵活性,可以使我们在实现的时候,基于性能等原因进行优化。而这些优点,都是值对象的不变性带来的。
一般建议,在领域模型图里,实体之间的关系用关联来表达,而实体和值对象之间的关系用属性来表达。
3 限定
还是对于员工和工作经验的例子,一个员工可以有多条工作经验,但限定在一个时间段的话,那么最多就只能有一条工作经验了。
image.png
限定机制起到了两个作用:
- 表达了更丰富的语义,把原来用注解说明的约束变成了更严格的符号;
- 简化了关联关系的多重性,把原来的一对多,在形式上,变成了一对一。
4 泛化
假设我们现在需要实现一个“报工时”的功能,先简单介绍下业务背景:
- 一个项目可以有0到n个子项目;
- 我们需要在项目或者子项目上面填报工时;
- 一条工时记录要么关联项目,要么关联子项目,但不能两者都关联,也不能两者都不关联。
这个时候,我们可以把项目和子项目抽象为工时项,刚才的业务逻辑,用工时项再来描述一遍,就是:
- 一个工时项可以关联 0 到多条工时记录;
- 一条工时记录必须关联且仅关联一个工时项。
泛化是一种强大的抽象机制,能够同时表现出不同对象间的共性和个性。
识别泛化的两个方向:
- 一个方向是先识别出了子类,然后从子类中归纳出共性,形成父类。
- 另一种是先识别出父类,然后发现这个类中的不同对象有一些显著的差异,需要再分成两个子类。
权衡泛化的两个视角:
- 业务视角,实际上是业务人员和技术人员都理解的视角。站在这个视角,我们要考虑:引入泛化后,有没有在模型里增加新的知识,有没有使模型更加简洁,更容易理解?
- 而站在技术视角,就要考虑这个模型是否能自然、直接地映射到设计模型和代码。
使用泛化的时机:
- 假如只有特性值不同,那么用特性值为对象分类就可以了,不必使用泛化。(特性值就是一个类型,例如有黑马、白马、棕马,那么马就有一个颜色的属性,而黑、白、棕就是三个特性值。)
- 如果特性种类不同,那么很可能要采用泛化。(还是马的例子,除了颜色,还有品种这个属性,例如阿拉伯马、荷兰矮马)
- 如果在业务规则、操作接口或操作实现方面有共性和个性,首先考虑在实现上是否可以使用策略模式,如果可以,那么在领域模型中就不必泛化,否则考虑泛化。
领域模型的三种关系:
- 实例和实例之间的关系。也可以说是对象和对象之间的关系。当我们谈关联和聚合的时候,说的就是实例之间的关系。比如说组织和员工之间具有一对多关联,实际上是说一个组织实例可以有多个员工实例。
- 类和类之间的关系。泛化其实就是类和类之间的关系,而不是实例和实例之间的关系。当我们说圆形是图形的子类的时候,实际上是说,圆形这一类事物,是图形这一类事物的子集。
- 类和实例之间的关系。比如圆形这个类和某个具体的圆之间的关系。或者前面说的普通工时项和学习时间之间的关系。
网友评论