领域驱动设计(二)

673 阅读5分钟

二. DDD和微服务编码实战

​ 从具体的编码讲起,理解如何结合DDD和微服务开发应用程序应对复杂性,提升系统的扩展性.

1. 应用架构

架构整洁之道

业务逻辑处于中心位置,技术细节位于边缘,通过依赖倒置实现业务不依赖于技术;

另外,业务层要“厚”,技术层要“薄”;

  1. 六边形架构,又称端口/适配器架构:

    每一种外部系统都有一个适配器与之对应,外界通过应用层API与内部交互

2_1.六边形架构.png

  1. 整洁架构:

2_2.整洁架构.jpg

2. 分层和依赖倒置

2_3.分层和依赖倒置.png

3. 贫血领域模型和富领域模型

2_4.贫血领域模型和富领域模型.png

3.1 贫血领域模型示例

主要的逻辑都在service里,处理业务逻辑和技术问题的代码混在一起,这种service被称为事务脚本.

2_5.积分账户充值的例子.png

3.2 富领域模型

Service作为一个用例的入口,对外提供服务,把业务逻辑委托给领域对象,它自己负责把领域对象的行为串联起来,并一些处理技术相关的问题,从而完成一个完整的用例.

2_6.积分账户充值的例子2.png

2_7.富领域模型.png

3.3 富领域模型的好处

分开对待本质复杂度和偶然复杂度,核心业务逻辑被封装在领域对象里,内聚,容易保持一致性,且容易维护和扩展。

此外,容易测试,且代码和测试都可以作为文档。

3.4 富领域模型代码示例

富领域模型代码示例github地址

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首先不是关于技术的,而是关于讨论、聆听、理解、发现的”。这个过程不是由某些业务人员输出业务语言那么简单,开发团队的积极参与和业务人员的积极参与同样重要。首先,业务知识分散在不同的业务人员那里;其次,开发团队的提问也可能暴露业务团队不知道的地方;最后,统一语言也不只是业务语言的汇总,它对开发人员和软件实现也是友好的,它最终反应的模型要在“在分析和设计方面都要有良好的效果”;统一语言是建立领域模型的基础,也是领域模型的直接反映,领域模型是统一语言的骨干,统一语言的变化也是模型中的变化。