二. DDD和微服务编码实战
从具体的编码讲起,理解如何结合DDD和微服务开发应用程序应对复杂性,提升系统的扩展性.
1. 应用架构
业务逻辑处于中心位置,技术细节位于边缘,通过依赖倒置实现业务不依赖于技术;
另外,业务层要“厚”,技术层要“薄”;
-
六边形架构,又称端口/适配器架构:
每一种外部系统都有一个适配器与之对应,外界通过应用层API与内部交互
- 整洁架构:
2. 分层和依赖倒置
3. 贫血领域模型和富领域模型
3.1 贫血领域模型示例
主要的逻辑都在service里,处理业务逻辑和技术问题的代码混在一起,这种service被称为事务脚本.
3.2 富领域模型
Service作为一个用例的入口,对外提供服务,把业务逻辑委托给领域对象,它自己负责把领域对象的行为串联起来,并一些处理技术相关的问题,从而完成一个完整的用例.
3.3 富领域模型的好处
分开对待本质复杂度和偶然复杂度,核心业务逻辑被封装在领域对象里,内聚,容易保持一致性,且容易维护和扩展。
此外,容易测试,且代码和测试都可以作为文档。
3.4 富领域模型代码示例
Amount:
@Embeddable
public class Amount {
Integer amount;
protected Amount(){}
public Amount(Integer amount) {
if(amount == null || amount.intValue() <= 0)
throw new IllegalArgumentException("amount should not be null or less than 1.");
this.amount = amount;
}
public Integer getAmount() {
return amount;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Amount amount1 = (Amount) o;
return amount.equals(amount1.amount);
}
@Override
public int hashCode() {
return Objects.hash(amount);
}
@Override
public String toString() {
return "Amount{" +
"amount=" + amount +
'}';
}
public Amount add(Amount amount) {
return new Amount(this.amount + amount.getAmount());
}
public Amount subtract(Amount amount) {
return new Amount(this.amount - amount.getAmount());
}
}
Account:
@Entity
public class Account {
@Id
@GeneratedValue(strategy= GenerationType.AUTO)
private Long id;
@Embedded
@AttributeOverrides({
@AttributeOverride(name = "amount", column = @Column(name = "balance"))
})
private Amount balance;
@Embedded
@AttributeOverrides({
@AttributeOverride(name = "id", column = @Column(name = "owner_id"))
})
private Owner owner;
// https://thoughts-on-java.org/hibernate-tips-how-to-avoid-hibernates-multiplebagfetchexception/
@OneToMany(mappedBy = "account", fetch = FetchType.EAGER,
cascade = CascadeType.ALL)
private Set<RechargeRecord> rechargeRecords = new HashSet<>();
@OneToMany(mappedBy = "account", fetch = FetchType.EAGER,
cascade = CascadeType.ALL)
private Set<ConsumeRecord> consumeRecords = new HashSet<>();
@CreatedDate
@Temporal(TIMESTAMP)
protected Date created;
@Version
private Integer version;
protected Account(){}
public Account(Owner owner, Amount balance) {
this.owner = owner;
this.balance = balance;
}
public void recharge(Amount amount, Source source) {
Optional<RechargeRecord> exists = this.rechargeRecords.stream().filter(r -> r.getSource().equals(source)).findFirst();
if(exists.isPresent()) {
throw new RechargeDuplicateException(this, source);
}
this.balance = this.balance.add(amount);
RechargeRecord record = new RechargeRecord(this, amount, source);
this.rechargeRecords.add(record);
}
public void consume(Amount amount, Source source) {
this.balance = this.balance.subtract(amount);
ConsumeRecord record = new ConsumeRecord(this, amount, source);
this.consumeRecords.add(record);
}
public Long getId() {
return id;
}
public Owner getOwner() {
return owner;
}
public Amount getBalance() {
return balance;
}
public Date getCreated() {
return created;
}
public Integer getVersion() {
return version;
}
public Set<RechargeRecord> getRechargeRecords() {
return Collections.unmodifiableSet(rechargeRecords);
}
public Set<ConsumeRecord> getConsumeRecords() {
return Collections.unmodifiableSet(consumeRecords);
}
@Override
public String toString() {
return "Account{" +
"id=" + id +
", owner='" + owner + '\'' +
", balance=" + balance +
", created=" + created +
", version=" + version +
'}';
}
//仅为了测试
protected void addRechargeRecord(RechargeRecord record) {
this.rechargeRecords.add(record);
}
}
3.5 富领域模型的问题
富领域对象需要:
-
足够大的内存
-
并发访问不多
这在单机软件里很多场景是可行的,但在企业应用软件里,尤其是互联网软件里,是不太可能的
3.6 通过聚合在贫富间取得平衡
聚合是⼀组相关领域模型的集合,是用来封装业务的不变性。同时强迫⼤家尽可能的简化领域模型之间的关联关系。在贫富之间寻找平衡。聚合的主要原则包括:
-
聚合是一致性边界,聚合根负责执行业务规则,改变边界内的任一对象的状态都不能违反整个聚合的所有业务规则;
-
聚合根有全局标识,聚合边界内的其他实体只有局部标识,聚合边界外的对象,只能持有聚合根的标识,不能引用聚合根对象,也不能持有聚合内部对象或标识
-
聚合具有整体的生命周期,删除聚合(根),聚合内的所有对象都需要删除
-
只有聚合根能从持久化系统内查询得到,边界内的对象只能从聚合根导航访问
3.7 使用聚合对案例重构
开发团队把情况和业务专家做了说明,并进行了深入地探讨,最后大家一致觉得“充值” 和 “消费” 应该是两种“申请”,而不“记录”,记录这个概念太偏系统实现了,而不是领域知识。从生活中也能感受到,充值申请和账户余额的增加也不是非要有强一致性的业务规则。
这个模型的好处是聚合更小了,聚合根之间没有直接的对象引用,而是通过ID来引用,这可以解决性能和并发问题。
3.8 保障聚合根原则不被破坏
可以使用ArchUnit来检查聚合根间是否有相互引用;另外,jQAssistant 也提供了
DDD代码检查的插件:101.jqassistant.org/dddplugin/
3.9 重构的启示
要想得到正确的领域模型:
-
业务视角很重要
-
业务专家的高度参与很重要
-
开发团队的高度参与很重要
-
共同协作很重要
-
迭代很重要
-
统一语言很重要
DDD通过协作迭代式探索模型,形成统一语言.
统一语言是对业务知识的集中和提炼。“所以DDD首先不是关于技术的,而是关于讨论、聆听、理解、发现的”。这个过程不是由某些业务人员输出业务语言那么简单,开发团队的积极参与和业务人员的积极参与同样重要。首先,业务知识分散在不同的业务人员那里;其次,开发团队的提问也可能暴露业务团队不知道的地方;最后,统一语言也不只是业务语言的汇总,它对开发人员和软件实现也是友好的,它最终反应的模型要在“在分析和设计方面都要有良好的效果”;统一语言是建立领域模型的基础,也是领域模型的直接反映,领域模型是统一语言的骨干,统一语言的变化也是模型中的变化。