Post

基于DDD的编码实践

分层设计

领域驱动设计(Domain-driven design, DDD) 作为一种复杂软件系统的应对方案,在设计和编码提供了一种新的解决方式,即领域驱动,要求程序员在设计和编码时从领域专家的角度 出发来实现架构/代码,做到代码即业务。同时利用各种方式拆解复杂模块,常用的方式有拆分子域、构建富血对象。

设计时,需要建立统一语言,确保领域中的业务概念处于同一个限界上下文,比如在一套电商系统中,用户买了一个东西,对应后台有一个订单,此时订单指代 订单域的一项数据,当该订单需要发货时,在物流域中也会接受订单域输入并产生发货订单,此时,物流域的订单和订单域的订单就不处于一个限界上下文。建立统一语言有助于 后续的产品和研发之间的高效沟通,打破代码和业务的语义鸿沟。领域模型的设计方法有 用例分析、事件风暴,领域模型需要提取出核心功能,并保证一定的扩展性,往往该过程是最重要也是最困难的。

进入编码阶段,构建聚合、聚合根、实体、值对象。虽然领域层与业务逻辑强关联,但是为了技术实现,在设计时也会有一些妥协,如,聚合不宜设计的过大,聚合的设计需要考虑 实体之间的一致性要求,同时有一些事务、锁的使用在某些时候会侵入领域层(并非不能这样,实践中往往在实现时会借鉴DDD的思想,但不会全套照搬);除此之外,结合事件驱动的方式, 可以让领域层代码保留一定的扩展性,实现上可以参考文章SpringEvent扩展性利器; 领域层作为核心不应该依赖具体实现,借鉴六边形架构,领域层中定义了仓储协议(Repository接口),业务逻辑只需要从仓储接口中获取数据,至于实现领域层并不关系,而具体的实现由其他模块如infrastructure层来实现; 同时,在实际处理输入时(http,rpc,job…)通常涉及与其他域的交互,DDD中通过构建防腐层来应对外部变化。

最终得到的代码分层结构如下图,Maven archetype代码参见:ddd-spring-web-maven-archetype

编码tips

构建富血实体

经典的MVC架构基于贫血对象构建,贫血对象只作为data class,其业务含义丢失,通过构建富血对象将业务实体的 逻辑内聚,不在分散在各个service中,一是业务含义清晰,二是能够单点控制。

比如,判断ExpressAggregate物流聚合的发货状态,其含有字段如下:

1
2
3
4
5
6
7
8
9
@Data
public class ExpressAggregate {
    
    private ExpressNumber expressNumber;
    // 状态
    private ExpressStatus expressStatus;
    ...
}

基于贫血对象,判断该物流实体是否发货需要在service中调用ExpressAggregate做判断:

1
2
3
4
5
ExpressAggregate expressAggregate = ...;
if (Objects.equals(express.status,...)){
  // bisiness logic
  ...
}

而基于富血对象,我们可以将是否发货的逻辑内置与ExpressAggregate中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Data
public class ExpressAggregate {

    private ExpressNumber expressNumber;
    private ExpressStatus expressStatus;
    ...

    /**
     * 判断是否发货
     * @return
     */
    public boolean hasSent() {
        return Objects.equals(this.expressStatus, ExpressStatus.SENT)
                   || Objects.equals(this.expressStatus, ExpressStatus.RECEIVED)
                   || Objects.equals(this.expressStatus, ExpressStatus.RETURN);
    }
}

这样,调用方直接使用 expressAggregate.hasSent() 即可知道结果,避免了判断逻辑散落各处。

值对象不可变

使用值对象表示无唯一标识(id)含义的实体,其各项属性相等即视为同一个值对象,因此值对象不可变。在实现层面, 值对象不应有setter:

1
2
3
4
5
6
7
8
9
// 无setter
@Getter
@AllArgsConstructor(staticName = "of")
public class ExpressNumber {
    private String expressNumber;
}

// usage
ExpressNumber expressNum = ExpressNumber.of("abc123");

相比于直接使用String expressNumber , 在业务代码中使用ExpressNumber具有更强的业务含义,且作为方法入参时不易与其他String类型参数弄混。

层间对象转换

不同层的对象不应混用,层间调用应使用转换器转换,转换工作由谁来做?谁有转换需求谁来做。

CQRS

CQRS(Command Query Responsibility Segregation) 将输入分为 Command 和 Query, Command作为变更系统状态的输入由领域层(聚合根)处理,而Query可不走领域层。

比如,db使用分库分表时,跨库的分页、排序、关联查询性能差,而将数据导入到ES中应对复杂查询可以减少查询开销。

图片来源:Axon Framework : Architecture Overview

This post is licensed under CC BY 4.0 by the author.