写在前面的话:项目运用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标识来源,员工工时记录里的信息,总能追溯回工时记录和员工,从而知道数据的来源。
③ 应用服务分离
如果我们发现,使用命令的并发请求相对比较少,而使用查询的并发请求却很多,需要横向扩展才能满足性能和可用性要求,那么就可以考虑拆成两个微服务了。
④ 数据库实例分离
采用这种策略时,我们把数据库实例也分成了两个,分别用于命令和查询两种数据模型。
组合处理