分层设计
领域驱动设计(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
物流聚合的发货状态,其含有字段如下:
@Data
public class ExpressAggregate {
private ExpressNumber expressNumber;
// 状态
private ExpressStatus expressStatus;
...
}
基于贫血对象,判断该物流实体是否发货需要在service中调用ExpressAggregate
做判断:
ExpressAggregate expressAggregate = ...;
if (Objects.equals(express.status,...)){
// bisiness logic
...
}
而基于富血对象,我们可以将是否发货的逻辑内置与ExpressAggregate
中:
@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:
// 无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可不走领域层。
图片来源:Axon Framework : Architecture Overview