美文网首页
领域驱动设计-进阶概念

领域驱动设计-进阶概念

作者: JBryan | 来源:发表于2024-01-13 22:38 被阅读0次

    本文作为学习笔记,内容来自“极客时间”专栏《手把手教你落地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. 类和实例之间的关系。比如圆形这个类和某个具体的圆之间的关系。或者前面说的普通工时项和学习时间之间的关系。

    相关文章

      网友评论

          本文标题:领域驱动设计-进阶概念

          本文链接:https://www.haomeiwen.com/subject/meofodtx.html