领域驱动设计-进阶概念

本文作为学习笔记,内容来自“极客时间”专栏《手把手教你落地DDD》,如有侵权,请及时告知,必当及时删除。

1 聚合

聚合的两个重要特征

  • 具有整体与部分的关系。举个例子,有“员工”和“技能”两个领域对象,那么“员工”就是一个整体,而“技能”是“人”的一个部分,我们就可以说“员工”和“技能”具有整体和部分的关系。
  • 具有不变规则,而且这种不变规则在并发的时候可能被破坏。要防止规则的破坏,仅仅锁住一条技能记录是不够的,必须把员工和所有技能作为一个整体锁住才能解决。或者说,员工和他的所有技能确定了一个事务边界。

具有这两个特征的一组领域对象,就叫聚合(Aggregate)
聚合的表示方法:例如上面提到的“员工”和“技能”这样一组聚合,可以用下面这种方式表达出来。

1705221609856.png

在一个聚合里,像员工这样代表整体的实体就是聚合根。一个聚合只有一个聚合根。一般我们约定,聚合包的名字和聚合根的名字是一样的。

由聚合的概念,可以推出三条推论

  • 首先,作为部分的实体,只能属于一个聚合根,不可能属于多个聚合根。比如说,一条技能信息,只能属于一个员工,不能属于多个员工。又比如说,我的手只能是我一个人的手,不能同时又是其他人的手。
  • 其次,我的手是不能“跳槽”的。不能今天是我的手,明天就变成了别人的手。也就是说,一个聚合的一部分,不能再变成其他聚合根的一部分。
  • 再次,由前两条自然可以推出,聚合根被删除,那么聚合中的所有对象都会被删除。
  • 最后,还有一个“标识”的问题。在业务上,为了识别每个实体,实体必然要有一个标识。例如,人的标识,可以是身份证号。如果这个人是学生,那么他的标识也可以是学号。注意,这里说的标识是一个业务概念,而不是技术概念,和数据库表中常见的没有业务概念的 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 到多条工时记录;
  • 一条工时记录必须关联且仅关联一个工时项。

image.png

泛化是一种强大的抽象机制,能够同时表现出不同对象间的共性和个性。

识别泛化的两个方向:

  • 一个方向是先识别出了子类,然后从子类中归纳出共性,形成父类。
  • 另一种是先识别出父类,然后发现这个类中的不同对象有一些显著的差异,需要再分成两个子类。

权衡泛化的两个视角:

  • 业务视角,实际上是业务人员和技术人员都理解的视角。站在这个视角,我们要考虑:引入泛化后,有没有在模型里增加新的知识,有没有使模型更加简洁,更容易理解?
  • 而站在技术视角,就要考虑这个模型是否能自然、直接地映射到设计模型和代码。

使用泛化的时机:

  1. 假如只有特性值不同,那么用特性值为对象分类就可以了,不必使用泛化。(特性值就是一个类型,例如有黑马、白马、棕马,那么马就有一个颜色的属性,而黑、白、棕就是三个特性值。)
  2. 如果特性种类不同,那么很可能要采用泛化。(还是马的例子,除了颜色,还有品种这个属性,例如阿拉伯马、荷兰矮马)
  3. 如果在业务规则、操作接口或操作实现方面有共性和个性,首先考虑在实现上是否可以使用策略模式,如果可以,那么在领域模型中就不必泛化,否则考虑泛化。

领域模型的三种关系:

  1. 实例和实例之间的关系。也可以说是对象和对象之间的关系。当我们谈关联和聚合的时候,说的就是实例之间的关系。比如说组织和员工之间具有一对多关联,实际上是说一个组织实例可以有多个员工实例。
  2. 类和类之间的关系。泛化其实就是类和类之间的关系,而不是实例和实例之间的关系。当我们说圆形是图形的子类的时候,实际上是说,圆形这一类事物,是图形这一类事物的子集。
  3. 类和实例之间的关系。比如圆形这个类和某个具体的圆之间的关系。或者前面说的普通工时项和学习时间之间的关系。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335

推荐阅读更多精彩内容