DDD领域驱动设计

写在前面的话:项目运用DDD建议

一、 DDD 的概念

1、DDD 是一种开发复杂软件的系统化的方法学和思想。

2、DDD 建立在面向对象方法学和敏捷软件开发方法之上,一方面保留了面向对象的精华,另一方面又弥补了早期方法的不足。

3、DDD 从面向对象和敏捷中提炼出了一套原则、模式和实践,使面向对象方法学在企业应用中更加容易学习和掌握。

4、DDD 的核心是领域建模。领域模型是浓缩的领域知识。此外,DDD 还重视业务与技术人员的沟通,以及如何应对变化。

5、而 DDD 正是顺应了时代的要求,才日益普及起来。

二、事件风暴

1、事件风暴的主要过程

        事件风暴分为识别领域事件、识别命令、识别领域名词三个步骤

2、领域事件

① 不要把技术事件当成领域事件

        领域事件一定要是领域专家所关注的,用的是业务术语。像数据库事务已回滚、缓存已命中之类的技术术语,不是领域事件,不在这个阶段讨论。

② 查询功能不算领域事件

        领域事件应该是对某样事物产生了影响,并被记录的事情。一般是某个事物的创建、修改和删除。还有一种情况是向其他人或者系统发消息,例如“通知邮件已发送”也算领域事件,因为接收方可能会通过进一步处理来影响某些事物。

3、事件风暴也可以用表格的形式来保存

4、保存领域规则

5、建立词汇表

三、领域建模实践

0、领域建模流程

1、领域建模基本概念        

        包括 UML、领域对象(domain object)、实体(entity)、类(class)、实例(instance)等。

2、建模流程

        事件风暴识别出的领域名词出发,开始进行领域建模。首先假定每个领域名词都是一个实体。然后识别实体之间的关联。关联可以分为三种:一对一、一对多和多对多。而这些不同的关联,可以用多重性来表达。

        一个实体 A 最多可以对应多少个实体 B?

        一个实体 A 最少可以对应多少个实体 B?

        一个实体 B 最多可以对应多少个实体 A?

        一个实体 B 最少可以对应多少个实体 A?

4、“抽象”和个性提取

        通过“抽象”,找出领域名词中并没有直接揭示出来的实体。例如把企业、开发中心等抽象成组织和组织类别,把管理员、人事人员等抽象成岗位。这样的模型更能反映出业务的本质,从而更加灵活。

 3、实体自身上的“自关联”

        通过分析“取员工上级”的操作,识别出了员工和组织之间关于组织负责人的关联。

5、业务规则和约束

6、关联关系

7、划分模块

四、数据库设计

1、领域模型中的实体映射为数据库中的表;领域模型中的属性,映射成表中的字段。同时还要根据需求补充更多的字段。

2、外键的逻辑关系还是存在的。我们用虚线箭头表示这种逻辑上的外键关系,称为虚拟外键。

3、对于多对多关联,我们必须增加一个关联表,其中包括了两个实体表各自的主键。另外,关联上的多重性决定了外键字段的非空约束。

4、租户管理进行数据库设计

组织实体建表

        一个一对多关联,在数据库设计时可以映射成一个外键。

       关联上的多重性决定了外键字段的非空约束。

5、多对多关联。

        我们必须增加一个关联表

五、分层架构

1、领域层(细粒度)

        用来封装领域数据和逻辑。这一层与领域模型直接对应,是整个系统的核心。

2、应用层(粗粒度)

        作为领域层的“门面”,把领域层封装成更粗粒度的服务供外部使用,并且处理事务、日志等横切关注点。

3、主动适配器(controller)

        用来接收来自外部的请求。屏蔽具体的输入输出技术。

4、被动适配器,用来访问外部资源(repository)

        被动适配器和主动适配器都属于适配器层,区别在于调用的方向不同。适配器层与具体输入输出和资源访问技术有关,而应用层和领域层与具体技术无关。这样我们就分离了技术和业务的关注点。

5、common 层

        用于存放工具和框架。这一层对前面的各层进行支撑。

六、依赖倒置

        第一步,从仓库抽出一个接口,原来的仓库成为了这个接口的实现类,

        第二步,把这个接口移动到领域层。

七、DDD 的表意接口(Intention-Revealing Interfaces)模式

        违反了表意接口模式,常常会出现过长的函数和注释这两个坏味道,我们可以通过抽取函数这个重构手法进行了解决。

        把每个规则都抽成了独立的方法,而且每个方法都按含义进行了命名。比如说“租户必须有效”这个规则的方法名就是 “tenantShouldValid”。

        用表意接口封装了状态属性,使外界不需要直接关心状态转换的细节,同时消除了特性依恋的坏味道

public boolean isEffective() { 
    return status.equals(OrgStatus.EFFECTIVE); 
}


//不再依赖 Org 的内部状态 
if (!org.isEffective()) { 
    throw new BusinessException("该组织不是有效状态,不能撤销!"); 
}

八、“应用逻辑”和“领域逻辑”

        领域逻辑应该放在领域层,不属于领域逻辑的代码逻辑通常称为“应用逻辑”。“应用逻辑”和“领域逻辑”的本质区别在于是否蕴含着业务逻辑

        创建一个叫 OrgValidator 的类,也就是组织校验器,把规则都放到这个类里面,通过一个统一的 validate() 方法来调用。然后,把 OrgValidator 放到领域层。

九、DDD 的工厂(Factory)模式

        对于复杂领域对象的创建,可以采用 DDD 的工厂(Factory)模式。这个模式有多种实现方式,对于参数较少的情况,可以直接用参数调用工厂,参数较多时,则可以采用 Builder 模式。

package chapter11.unjuanable.domain.orgmng;
// import...

public class OrgBuilder {
    //用到的 validator
    private final CommonValidator commonValidator;
    private final OrgTypeValidator orgTypeValidator;
    private final SuperiorValidator superiorValidator;
    private final OrgNameValidator orgNameValidator;
    private final OrgLeaderValidator orgLeaderValidator;

    //用这些属性保存创建对象用到的参数
    private Long tenantId;
    private Long superiorId;
    private String orgTypeCode;
    private Long leaderId;
    private String name;
    private Long createdBy;

    public OrgBuilder(CommonValidator commonValidator
            , OrgTypeValidator orgTypeValidator
            , SuperiorValidator superiorValidator
            , OrgNameValidator orgNameValidator
            , OrgLeaderValidator orgLeaderValidator) {
        //注入各个 Validator... 
    }

    // 为builder 的 tenant 属性赋值,然后返回自己,以便实现链式调用
    public OrgBuilder tenantId(Long tenantId) {
        this.tenantId = tenantId;
        return this;
    }
    // 其他5个属性赋值与 tenantId 类似 ...
    
    public Org build() {
        validate();

        Org org = new Org();
        org.setOrgTypeCode(this.orgTypeCode);
        org.setLeaderId(this.leaderIc);
        org.setName(this.name);
        org.setSuperiorId(this.superiorId);
        org.setTenantId(this.tenantId);
        org.setCreatedBy(this.createdBy);
        org.setCreatedAt(LocalDateTime.now());

        return org;
    }

    private void validate() {
        commonValidator.tenantShouldValid(tenantId);
        orgTypeValidator.verify(tenantId, orgTypeCode);
        superiorValidator.verify(tenantId, superiorId, orgTypeCode);
        orgLeaderValidator.verify(tenantId, leaderId);
        orgNameValidator.verify(tenantId, name, superiorId);
    }
}

十、程序里模块的划分方式,

        有按性质分和按耦合分两种,通俗地说就是“打横”分和“打竖”分,我们建议采用“先横后竖”的方法。

        entity、repository、factory 和 domainservice 几个子包,然后把各个类移进去。

        打竖分,也就是把 service 和这个 service 用到的 DTO 放在同一个包内,提高了模块的内聚性。

十一、领域建模:捕获行为需求和事件风暴

十二、聚合

1、聚合是 DDD 里的一个重要模式

        ① 如果一组对象具有整体部分关系

        ② 并且需要维护整体上的不变规则

2、聚合核心

        表示整体的那个实体叫做聚合根。

3、聚合在模型中表示

        ① 使用<<aggregate root>> 的衍型来表示聚合根;

        ② 在关联上用空心菱形符号表示整体部分关系;

        ③  并用一个包把聚合包起来,包的名字一般和聚合根的名字相同。

        ④ 在识别客户经理等聚合的时候,我们还介绍了派生关联。

                业务人员最关心的就是当前客户经理,历史变更信息只在少数情况下才用到。所以,领域专家希望强调当前客户经理这个概念。因此,我们保留了这个关联。

4、聚合的其他特征

        ① 表示部分的实体只能属于一个聚合,并且不能再变成其他聚合的一部分;

        例如:一条技能信息,只能属于一个员工,不能属于多个员工。又比如说,我的手只能是我一个人的手,不能同时又是其他人的手。

        ② 聚合根被删除的话,整个聚合的实体都要被删除; 

        ③ 聚合根有全局标识,非聚合根实体只有局部标识。

        例如:例如,员工是聚合根,员工号是全局标识。而工作经验没有必要进行全局编号,只需要在聚合内部编个号就可以了。例如,001 号员工的第 1 份工作经验、第 2 份工作经验等等。

5、聚合的作用

        ① 确保不变规则

        ② 为我们增加了一个分析业务规则的视角,将业务规则和事务联系起来,增加了模型的清晰度,并且使开发人员更容易确定事务的范围。

                a、模型上为每个聚合建了一个包,可以认为,聚合是一种特殊的模块。这样,模型的层次就变得更清晰了。

                b、我们也可以把聚合当作一个粗粒度的概念单位进行思考,降低了认知负载。

十三、聚合实现

1、数据库设计

领域模型

数据库表

2、对象关联

public class Org{
    private List<Emp> members;
    // other fields ...
    // getters and setters ...
}
public class Emp{
    private Org org;
    // other fields ...
    // getters and setters ...
}

3、ID 关联

public class Org{
    // fields ...
    // getters and setters ...
}
public class Emp{
    private Long OrgId;       // 员工所属组织的ID
    // other fields ...
    // getters and setters ...
}

4、聚合代码的封装

领域模型

实现模型

5、对非聚合根的封装

        ① 原则:聚合外部对象对非聚合根对象只能读,不能写,必须通过聚合根才能对非根对象进行访问。

        ② 包级私权限封装构造器和方法

                技能(Skill)类

package chapter15.unjuanable.domain.orgmng.emp;
// imports ...

public class Skill extends AuditableEntity {
    private Long id;            // 只读
    private Long tenantId;      // 只读
    private Long skillTypeId;   // 只读,表示到技能类型的ID关联
    SkillLevel level;           // 读写
    private int duration;       // 读写

    // 包级私有权限
    Skill(Long tenantId, Long skillTypeId, LocalDateTime createdAt, Long createdBy) {
        super(createdAt, createdBy);
        this.tenantId = tenantId;
        this.skillTypeId = skillTypeId;
    }

    public Long getId() {
        return id;
    }

    public Long getSkillTypeId() {
        return skillTypeId;
    }

    public SkillLevel getLevel() {
        return level;
    }

    // 包级私有权限
    void setLevel(SkillLevel level) {
        this.level = level;
    }

    public int getDuration() {
        return duration;
    }

    // 包级私有权限
    void setDuration(int duration) {
        this.duration = duration;
    }
}

6、对聚合根的封装

        ① 返回不可变列表

Collections.unmodifiableList(experiences);

        ② 用聚合根创建和访问非根对象等 

                聚合根对非聚合根的封装

package chapter15.unjuanable.domain.orgmng.emp;
// imports ...

public class Emp extends AuditableEntity {
    // other fields ...
    
    private List<Skill> skills;              // 读写
    private List<WorkExperience> experiences;// 读写
    private List<String> postCodes;          // 读写,岗位代码

    // constructors and other getters and setters ...

    public Optional<Skill> getSkill(Long skillTypeId) {
        return skills.stream()
                .filter(s -> s.getSkillTypeId() == skillTypeId)
                .findAny();
    }
    
    public List<Skill> getSkills() {
        return Collections.unmodifiableList(skills);
    }
    
    void addSkill(Long skillTypeId, SkillLevel level
                  , int duration, Long userId) {
        Skill newSkill = new Skill(tenantId, skillTypeId
                  , LocalDateTime.now(), userId);
        newSkill.setLevel(level);
        newSkill.setDuration(duration);

        skills.add(newSkill);
    }

    // 对 experiences、postCodes 进行类似的处理 ...
    
}

7、不变规则的实现

① 如果规则的验证不需要访问数据库,那么首先应该考虑在领域对象里实现,而不是在领域服务里实现。

② 关于技能和工作经验的两条规则,必须从整个聚合层面才能验证,所以无法在 Skill 和 WorkExperience 两个类内部实现,只能在聚合根(Emp)里实现,这也是聚合存在的价值。

package chapter15.unjuanable.domain.orgmng.emp;
// imports ...

public class Emp extends AuditableEntity {
    // other fields ...
    private List<Skill> skills;
    private List<WorkExperience> experiences;

    // constructor and other operations ...

    public void addSkill(Long skillTypeId, SkillLevel level
                        , int duration, Long userId) {
        // 调用业务规则: 同一技能不能录入两次
        skillTypeShouldNotDuplicated(skillTypeId);

        Skill newSkill = new Skill(tenantId, skillTypeId, userId)
                .setLevel(level)
                .setDuration(duration);

        skills.add(newSkill);
    }

    private void skillTypeShouldNotDuplicated(Long newSkillTypeId) {
        if (skills.stream().anyMatch(
                    s -> s.getSkillTypeId() == newSkillTypeId)) {
            throw new BusinessException("同一技能不能录入两次!");
        }
    }



    public void addExperience(LocalDate startDate, LocalDate endDate, String company, Long userId) {
        // 调用业务规则: 工作经验的时间段不能重叠
        durationShouldNotOverlap(startDate, endDate);

        WorkExperience newExperience = new WorkExperience(
                tenantId
                , startDate
                , endDate
                , LocalDateTime.now()
                , userId)
                .setCompany(company);

        experiences.add(newExperience);
    }

    private void durationShouldNotOverlap(LocalDate startDate
                                        , LocalDate endDate) {
        if (experiences.stream().anyMatch(
                e -> overlap(e, startDate, endDate))) {
            throw new BusinessException("工作经验的时间段不能重叠!");
        }
    }

    private boolean overlap(WorkExperience experience
                    , LocalDate otherStart, LocalDate otherEnd) {
        LocalDate thisStart = experience.getStartDate();
        LocalDate thisEnd = experience.getEndDate();

        return otherStart.isBefore(thisEnd) 
                && otherEnd.isAfter(thisStart);
    }
}

③ 在创建聚合方面,我们采用了和上个迭代不同的另一种方式:Assembler。这种方式和工厂模式各有利弊,可以根据实际情况选择。

④ 在持久化方面,我们用仓库(EmpRepository)来把聚合保存到数据库,要点是,仓库是针对聚合整体的,而不是针对单独的表的。也就是说,聚合和它的仓库有一一对应关系。此外,为了对修改过的聚合进行持久化,我们为实体增加了“修改状态”(ChangingStatus)属性,下节课会利用这个属性完成整个持久化功能。

⑤ Repository(仓库) 和传统的 DAO(数据访问对象) 虽然都用来访问数据库,但有一个重要的区别——DAO 是针对单个表的,而 Repository 是针对整个聚合的。下面我们通过代码再来理解一下。

package chapter16.unjuanable.adapter.driven.persistence.orgmng;
// imports ...

@Repository
public class EmpRepositoryJdbc implements EmpRepository {

    final JdbcTemplate jdbc;
    // SimpleJdbcInsert 是 Spring JDBC 提供的插入数据表的机制
    final SimpleJdbcInsert empInsert;
    final SimpleJdbcInsert skillInsert;
    final SimpleJdbcInsert insertWorkExperience;
    final SimpleJdbcInsert empPostInsert;

    @Autowired
    public EmpRepositoryJdbc(JdbcTemplate jdbc) {
        this.jdbc = jdbc;
        this.empInsert = new SimpleJdbcInsert(jdbc)
                .withTableName("emp")
                .usingGeneratedKeyColumns("id");
       
         // 初始化其他几个 SimpleJdbcInsrt ...
    }

    @Override
    public void save(Emp emp) {
        insertEmp(emp);  // 插入 emp 表

        //插入 skill 表
        emp.getSkills().forEach(s -> 
                        insertSkill(s, emp.getId()));
        //插入 work_experience 表
        emp.getExperiences().forEach(e -> 
                        insertWorkExperience(e, emp.getId()));
        //插入 emp_post表
        emp.getEmpPosts().forEach(p -> 
                        insertEmpPost(p, emp.getId()));

    }

    private void insertEmp(Emp emp) {
        Map<String, Object> parms = Map.of(
                "tenant_id", emp.getTenantId()
                , "org_id", emp.getOrgId()
                , "num", emp.getNum()
                , "id_num", emp.getIdNum()
                , "name", emp.getName()
                , "gender", emp.getGender().code()
                , "dob", emp.getDob()
                , "status", emp.getStatus().code()
                , "created_at", emp.getCreatedAt()
                , "created_by", emp.getCreatedBy()
        );

        Number createdId = empInsert.executeAndReturnKey(parms);
        //通过反射为私有 id 属性赋值
        forceSet(emp, "id", createdId.longValue());
    }

    private void insertWorkExperience(WorkExperience experience, Long empId) {
        // 类似 insertEmp...
    }

    private void insertSkill(Skill skill, Long empId) {
        // 类似 insertEmp...

    }

    private void insertEmpPost(EmpPost empPost, Long empId) {
        // 类似 insertEmp...
    }
  
    // 其他方法 ...
    
}

⑥ 标记领域对象的修改状态

ChangingStatus.java

package chapter16.unjuanable.common.framework.domain;

public enum ChangingStatus {
    NEW,            // 新增
    UNCHANGED,      // 不变
    UPDATED,        // 更改
    DELETED         // 删除
}

AuditableEntity.java

package chapter16.unjuanable.common.framework.domain;
import static chapter16.unjuanable.common.framework.domain.ChangingStatus.*;

public abstract class AuditableEntity {
    protected ChangingStatus changingStatus = NEW;
    // 其他属性、构造器 ...

    public ChangingStatus getChangingStatus() {
        return changingStatus;
    }

    public void toUpdate() {
        this.changingStatus = UPDATED;
    }

    public void toDelete() {
        this.changingStatus = DELETED;
    }

    public void toUnChang() {
        this.changingStatus = UNCHANGED;
    }
    
    // 其他方法 ...
}

        “修改状态”的默认值是“NEW”,可以通过 toUpdate()、 toDelete() 和 toUnChange() 来改变。这样,程序中的应用服务、仓库等等就可以对实体的状态进行操作了。

8、聚合的修改

① 在修改之前,要把聚合从数据库里取出来。为了这个目的,仓库要把聚合的数据整体装入内存,并重建聚合。这里我们还用了一个技巧,在仓库包里建立了聚合根的一个子类,从而绕过校验规则,避免不必要的性能损耗。

② 要在领域层的聚合根里增加对技能、工作经验和岗位的更改和删除代码,并为这些对象设置合适的修改状态,从而把非聚合根对象的修改逻辑封装起来。

③ 在应用层把当前聚合与请求参数进行对比,确定对聚合里的各个对象应该进行增、删、改,还是保持不变。然后,调用聚合根来进行相应的操作。

④ 为了把聚合存入数据库,仓库要遍历聚合中的各个对象,根据对象的更改状态进行合适的数据库操作。

十四、实体和值对象的区别

1、从“同一性”来说,实体靠独立于其他属性的标识来确定“同一性”;而值对象靠所有属性值作为一个整体来确定“同一性”,没有脱离其他属性的单独的标识。

2、从“可变性”来说,实体是可变的;值对象是不可变的。值对象的不可变性,并不来自于外在的约束,而是来自于值对象的本质,也就是说,谈论值对象是否可变本身是没有意义的。实体和值对象在可变性上的区别,其实,又是从“同一性”推导出来。

3、实体之间的关系用关联来表达,而实体和值对象之间的关系用属性来表达

4、对于依附于实体的值对象,可以放在实体所属的聚合包里,而不依赖于实体的值对象,可放在公共包里。

十五、限定

1、假设有一个一对多的关联,如果表示“多”的一端的某一个属性被限定以后,可以变成一对一关联的话,那么就可以使用限定了。

2、“限定”在模型里的表示方法是用一个小方框,里面写上被限定的属性,然后放到关联里表示“1”的那一端。之后,原来的一对多,在形式上一般就可以变成一对一了。这里增加的小方框叫做“限定符”。

十六、泛化

1、概念

假定 C 是父类,A 和 B 是它的子类,那么对应到自然语言,可以有四种说法。

第一种,A 和 B 统称为 C,例如,甜粽子和咸粽子统称为粽子。

第二种,C 可以分成 A 和 B 两类,例如,粽子可以分成甜粽子和咸粽子两类。

第三种,一个 A 是一个 C,一个 B 也是一个 C,例如,一个甜粽子是一个粽子,一个咸粽子也是一个粽子。

第四种,A 和 B 具有共性,表示共性的概念称为 C,例如,甜粽子和咸粽子具有共性,表示共性的概念称为粽子。

这四种说法表面上不同,实际上表达了完全相同的含义,都可以用同样的泛化关系来表示。

2、使用(可以让用户选择不同权限、可以禁止用户选择不同权限)

3、泛化展示

4、泛化应用:

        特定企业定制的系统,SaaS 系统往往需要考虑更多的灵活性。

5、泛化使用选型:

        第一,假如只有特性值不同,那么用特性值为对象分类就可以了,不必使用泛化。

        第二,如果特性种类不同,那么很可能要采用泛化。

        第三,如果在业务规则、操作接口或操作实现方面有共性和个性,首先考虑在实现上是否可以使用策略模式,如果可以,那么在领域模型中就不必泛化,否则考虑泛化。

6、泛化示例:

        先从业务视角考虑,当我们抽象出泛化以后,实际上就在模型中增加了三条领域知识:

        第一,项目、子项目、普通工时项有一个共性,就是都能报工时,这个共性抽象成了“工时项”这个概念;

        第二,凡是工时项,必然和工时记录具有一对多的关系;

        第三,每条工时记录,必然与且仅与一条工时项关联。

7、泛化优点:

        ① 这个泛化也在一定程度上起到了简化模型的作用,因为原来项目、子项目、普通工时项各自和工时记录相关联,一共 3 条关联。现在,抽象并简化成了工时项和工时记录之间的 1 条关联。

        ② 再从技术视角看,我们使用泛化的话,可以把工时项直接映射成程序中的父类,在这个父类中处理和工时记录的关系,而不必在三个子类中分别处理,提高了可复用性,而且,将来如果再增加一种不同的工时项,只需增加一个子类就可以了,也符合程序的可扩展性。

8、领域模型中的几种关系

        ① 是实例和实例之间的关系。也可以说是对象和对象之间的关系。当我们谈关联和聚合的时候,说的就是实例之间的关系。比如说组织和员工之间具有一对多关联,实际上是说一个组织实例可以有多个员工实例。

        ② 是类和类之间的关系。泛化其实就是类和类之间的关系,而不是实例和实例之间的关系。当我们说圆形是图形的子类的时候,实际上是说,圆形这一类事物,是图形这一类事物的子集。

        ③ 是类和实例之间的关系。比如圆形这个类和某个具体的圆之间的关系。或者前面说的普通工时项和学习时间之间的关系。

9、模型中识别泛化的过程

        ① 一种是归纳法,也就是先识别出了一些类,然后发现它们之间有共性,于是抽象出父类。

        ② 另一种是演绎法,也就是先识别出了一个类,然后发现这个类又可以分成几种不同的情况,于是识别出不同的子类。

10、泛化建表

① 每个类一个表

② 每个子类一个表

11、设计主键的策略

① 共享主键策略:

        如果父类和各个子类在业务概念上,本质上就是一类事物,只是在某些方面有所差别,那么共享主键是比较合理的

② 不共享主键策略:

        如果在业务概念上不是同一类事物,只是由于某些个别方面有共性才造成的泛化关系,那么不共享主键比较合理。

12、sql使用乐观锁来处理并发

CorporateClient selectById(Long id) { 
    String sql = " select c.version" + ", c.addr_country" + ", c.addr_province" + ", c.addr_city" + ", c.addr_district" + ", c.addr_detail" + ", cc.name" + ", cc.tax_num" + ", cc.created_at" + ", cc.created_by" + ", cc.last_update_at" + ", cc.last_updated_by " + " from client as c" + " left join corporate_client as cc" + " on c.id = cc.id " + " where c.id = ? and c.client_type = ? ";
}

十七、限界上下文

1、划分限界上下文

2、上下文映射

建议为每个模型单独画一张图,基础信息管理上下文的模型图:

项目管理上下文的模型图

3、一个指向自身的关联优点:

        第一,它可以用于表示项目和子项目的父子关系。

        第二,就算不是项目的工时项,也可能有层次关系,现在的设计更有普适性了。

        第三,这个关联可以表达任意深度的层次关系,而不是像项目那样只能表达两层关系。

4、限界上下文内部概念保持一致,上下文之间的概念不必一致

5、微服务架构考量

        第一,收益大于成本。使用微服务的好处包括容易横向扩容、独立部署、避免系统腐化等等。代价是提高了运维的成本、远程调用增加了性能损耗以及维护最终一致性的复杂性等等。只有在收益大于成本的情况下,才值得使用微服务。

        第二,只有在团队有足够的运维技能和基础设施支持的时候,才能使用微服务。

        第三,只有团队具有一定的开发微服务的技能和经验时,才能使用微服务。

十八、微服务的设计方法

1、不同的可伸缩性要求。

        如果一个上下文里有些部分,需要随着使用情况,动态部署到更多的容器,比如说“双十一”促销的时候。而另外的部分性能要求比较稳定,不需要动态伸缩。那么,如果不同部分都混在一个微服务中,那么当扩展到更多容器的时候,成本就会比较高了。这时候,我们可以考虑根据可伸缩性的不同,划分成两个微服务。

2、不同的安全性要求。

        比如说,有些功能要接入互联网,有些部分在内网用,需要部署在防火墙的不同位置。这时候,也需要划分成不同的微服务。

3、技术异构性。

        比如说,有些部分需要用 Java 开发,有些部分需要用 node.js 开发,这时候,也要分成不同的微服务了。

十九、限界上下文间的集成

1、不同的集成策略

        工时管理”中的某个功能,需要获得“基础信息管理”中的员工数据     

         ① 数据同步策略:

        在“工时管理”服务对应的数据库里,建立员工表,但只包含工时管理需要的字段。然后,当“基础信息管理”中的员工信息发生了新增、修改或删除的时候,以某种方式把数据同步到“工时管理”数据库。这样,工时管理就可以通过访问本地数据库获得员工信息了。

        ② API 调用策略:

        在“工时管理”的数据库里不需要建立员工表,而是每次需要员工信息的时候,都调用“基础信息管理”提供的 API 来获取数据。

2、时管理数据库中有一个工时项表。“项目管理”在新增、修改和删除项目时,“工时管理”中的工时项表可能会发生相应的变化。我们以在“项目管理”中新增项目为例,来看看实现策略:

        

        ① 同步调用

        “项目管理”新增一个项目,“项目管理”服务就会调用“工时管理”服务中的一个“新增工时项”接口,“工时管理”服务就会在自己的工时项表里增加一条记录,然后把成功信息返回给调用方。“项目管理”服务会等待这个成功信息,收到以后,才会继续处理其他逻辑。

        ② 异步调用

        项目管理”新增项目以后,会向消息中间件发送一个“项目已增加”的事件,然后不用等待,继续进行其他处理。而“工时管理”服务中会订阅“项目已增加”事件。当监听到这个事件发生的时候,就会在自己的工时项表里增加一条记录。这种方式也叫做“事件驱动”架构。

二十、防腐层

        防腐层能够隔离两个限界上下文的变化,使两个上下文各自独立地演进。理论上,当单体架构拆分成微服务的时候,只需要修改防腐层就可以了。我们例子中的防腐层是在适配器中实现的。

二十一、战略设计和战术设计

1、战术设计,就是细粒度的建模,包括实体、值对象、关联、模块、聚合等等。

2、战略设计,解决的是当系统变得很大很复杂的时候,怎样从宏观上把握系统的总体结构,应对系统的规模和复杂性的问题。

二十二、CQRS(命令查询职责分离)

1、概念

        第一,命令(增、删、改)要走领域模型。

        第二,(查询)不走领域模型,直接用 SQL 和 DTO。

2、解决方案

        ① 代码结构分离

        代码首先分成了两个包,一个是 command processor(命令处理器),另一个是 query processor(查询处理器)。其中,命令处理器采用的就是之前基于领域模型的分层架构。 

        ② 数据库结构分离

        数据库里的表分成了两套——命令模型(command model)和查询模型(query model),分别由命令处理器和查询处理器访问。其中命令模型中的表是根据领域模型设计的,查询模型部分的表就是根据查询需求进行了反规范化设计。

        补充:查询模型图

        

      trace标识来源,员工工时记录里的信息,总能追溯回工时记录和员工,从而知道数据的来源。

        ③ 应用服务分离

              如果我们发现,使用命令的并发请求相对比较少,而使用查询的并发请求却很多,需要横向扩展才能满足性能和可用性要求,那么就可以考虑拆成两个微服务了。

        ④    数据库实例分离

        采用这种策略时,我们把数据库实例也分成了两个,分别用于命令和查询两种数据模型。

组合处理

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值