领域驱动设计精粹(下)

谈谈领域驱动设计的落地

前文提到了事件风暴产出的领域模型是概念模型,到实际落地还有些距离,而落地的结果也是各不相同,我觉得说落地,要先回顾一下领域驱动设计的两个作用。

  1. 通过战略设计拆分子域,指导微服务拆分。
  2. 通过事件风暴建立领域概念模型,指导代码设计。

也就是说领域驱动设计产出的结果是指导性的,并不是一个直接可落地的结果。落地的方案则是要通过架构设计和框架选择上来进行。架构是为了控制软件复杂性而做,就好像『一千个读者心中有一千个哈姆雷特』,不同人做架构不尽相同。下面说说我的落地方式。

架构演进

我们最初接触和使用的分层架构是三层的,三层架构解决了程序内部代码调用复杂和职责不清的问题,在 DDD 分层架构中的关于对象和服务被重新归类到不同分层中,确定了层与层之间的职责边界。DDD 提出了四层架构,其中最主要的变化是提出领域层的概念,需要领域专家对于业务知识的精准把握之上,根据领域设计方法建立领域模型,把变动较少的领域模型放入领域层,而多变的业务场景代码放入应用层。如下图对应三层到四层的演进过程。

分层架构的一个重要原则是每层只能与位于其下方的层发生耦合,可以简单分为以下两种:

  • 严格分层架构,某层只能与位于其直接下方的层发生耦合。
  • 松散分层架构,允许某层与它的任意下方层发生耦合。

这两种分层架构的耦合方式是各有利弊,在网络上对于他们也是各有各的见解。结合实际情况在开发中,更倾向于采用松散分层架构,但是要禁止用户接口层直接访问基础设施层,防止一些潜在的安全问题。

子域划分

基于现有三层架构,在其中增加 domain 包的形式增加领域服务层。不同的子域通过包来划分如下:

1
2
3
package noogel.xyz.domain.deal;  // 交易子域
package noogel.xyz.domain.quote; // 算价子域
package noogel.xyz.domain.promotion; // 促销子域

同一个领域服务下面再按照领域对象、领域服务、领域资源库、防腐层等方式组织。

1
2
3
4
package noogel.xyz.domain.xxx.repository;  // 资源库接口定义
package noogel.xyz.domain.xxx.entity; // 领域对象
package noogel.xyz.domain.xxx.facade; // 防腐层
package noogel.xyz.domain.xxx.service; // 领域服务

领域对象

领域驱动解决的一个问题就是对象的贫血问题。通过如下促销领域对象来说明,对于当前购买商品组合能否满足购买规则的检查逻辑不是放在服务层或者工具类中,而是由领域对象提供方法支持。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@Getter
@ToString
@...
public class PromotionDo {
/**
* 业务幂等
*/
private String bizNo;
// 省略字段...
private Long beginTime;
private Long endTime;
private String desc;

/**
* 计算生效数据
* @param items
* @return
*/
public List<PromotionDo> calculateValid(List<ItemDo> items) {
switch (rule.getKind()) {
// ...
}
List<PromotionDo> promoDataList = new ArrayList<>();
// do sth ...
return promoDataList;
}
}

资源库(依赖倒置)

资源库对外的整体访问由 Repository 提供,它聚合了各个资源库的数据信息,同时也承担了资源存储的逻辑。我们将资源库的接口定义放在领域层,而具体实现放在基础设施层。

1
2
3
4
package noogel.xyz.domain.xxx.repository;  // 资源库接口定义
package noogel.xyz.infrastructure.repository; // 资源库实现
package noogel.xyz.infrastructure.rpc; // RPC 服务
package noogel.xyz.infrastructure.dao; // 数据库访问对象

资源库接口定义,提供必要的入参,并且以领域对象的形式作为结果返回。至于组织返回的领域对象,交由具体实现类来实现,可以通过调用数据库、缓存系统、RPC 接口等形式来组织生成领域对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public interface PromotionRepository {
/**
* 保存 xx
* @param data 领域对象
* @return 唯一 key
*/
String create(PromotionDo data);

/**
* 批量更新状态
* @param key
* @param state
* @return
*/
boolean batchUpdateState(List<String> key, PromoState state);

/**
* 批量查询
* @param promoIds
* @return
*/
Map<String, PromotionDo> batchGetOnlineById(List<Long> ids);
}

防腐层

用来消除外部上下文结构差异的作用,也叫适配层。比如在算价上下文中需要调用促销上下文数据,不同的促销数据源提供了不同的接口和数据,这时就需要引入防腐层来屏蔽差异,防止外部上下文侵入领域内部影响代码模型。首先定义需要的数据接口规范。

1
2
3
4
5
6
7
8
9
10
public interface PromotionFacade {

/**
* 计算促销数据
*
* @param ctx
* @return
*/
List<PromotionData> calculatePromotion(PromotionContext ctx);
}

实现类来用处理外部数据的差异,按照接口要求封装数据,简化模型的复杂性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class Promotion1Facade implements PromotionFacade {

@Override
public List<PromotionData> calculatePromotion(PromotionContext ctx) {
PromotionData promoData = PromotionData.of(...);
return Collections.singletonList(promoData);
}
}


public class Promotion2Facade implements PromotionFacade {

@Autowired
private RpcService rpcService;

@Override
public List<PromotionData> calculatePromotion(PromotionContext ctx) {
PromotionData data = new PromotionData();
// do sth ...
return data;
}
}

上下文集成

对于上下文集成的手段可以通过 RPC 服务、HTTP 服务、MQ 消息订阅。

领域服务

上面我们讲述了各个要素对于资源和行为的封装,业务逻辑的实现代码应该尽量放在聚合根边界内。但是总会遇到不适合放在聚合根上的业务逻辑,而此时领域服务就需要承载编排组合领域对象、资源库和防腐接口等一系列要素,提供对其它上下文的交互接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public interface PromotionService {
/**
* 创建促销
*
* @param item
* @return
*/
String createPromotion(CreatePromotionDto item);

/**
* 批量更新状态
*
* @param req
* @return
*/
boolean batchUpdatePromotion(BatchUpdatePromotionReqDto req);

/**
* 计算有效的促销
*
* @param req
* @return
*/
List<PromoResultDto> calculateValidPromotion(CalculateValidPromotionReqDto req);
}

落地延伸

DDD 的设计概念很多,学习成本比较高,于是我们组织了《实现领域驱动设计》的读书分享会,通过共读分享交流理解的方式,让大家对于 DDD 的设计方法和概念有了比较统一的认知。同时发现在做设计分享时,组内的认知比较一致,而对外的理解成本则会比较高。

不论我们怎样称呼应用层和领域层,但是四层架构的优势已经显而易见,对于电商交易这样一类相对复杂的系统而言。DDD 教会我们怎么拆分领域,如何沉淀领域模型,而如何组织领域服务提供业务功能上是匮乏的,下面是基于系统问题和业界资料总结的一个抽象框架,描述的是如何组合核心能力与业务场景,并提供一个配置化的灵活系统。

能力单元

提供基础能力的独立单元,只单纯依赖下游数据提供能力,职责比较单一,对应领域驱动设计的领域服务。

场景单元

通过编排不同能力单元,形成一个预定义的执行流程,叫做场景单元。场景单元有以下关键要素:

  1. 执行节点:执行节点负责转换出入参并调用能力单元或场景单元,返回结果给下一个节点。
  2. 条件控制:根据执行节点结果进行简单逻辑判断选择不同的执行路径。
  3. 干预策略:干预策略是场景的扩展点,通过预留的扩展点可以干预执行流程。

所以一个场景单元的实际处理通路由条件控制和干预策略决定。

策略配置服务

  1. 提供静态或动态的策略配置给场景单元使用。
  2. 基于节点维度的简单风控策略支持,比如限流、熔断等。

框架图

核心能力封装数据和行为,职责要单一且通用,对外提供完善的接口供场景调用,核心能力内部是高内聚的,能力外不能与其它能力模块发生直接耦合,只能通过场景进行间接耦合,要保证核心能力的职责单一性。

能力模型是指对于复杂场景进行归类和抽象得出的一个模型,可以用来解决某一类通用问题。能力模型既可以是由订单系统内部提供的,也可能是由外部系统通过 RPC 形式提供的一整套能力接口包装而得。

内部事件,由于能力之间不允许直接耦合,所以内部事件不允许在能力模块内部发送,只能由场景中进行控制发送,并且能力内部不允许直接监听,而应该把监听事件作为场景的一种入口,实现场景之间的依赖调用。

场景单元偏流程数据编排,需要组织和协调资源的代码被定义为流程。场景单元与策略服务耦合更重,通过策略服务控制场景流程图的走向,以此来实现系统配置化。

参考

《复杂软件设计之道:领域驱动设计全面解析与实战》 - 彭晨阳
《实现领域驱动设计》 - 沃恩·弗农
《解构领域驱动设计》 - 张逸
《DDD实战课》 - 极客时间

文章

https://insights.thoughtworks.cn/backend-development-ddd/
https://zhuanlan.zhihu.com/p/383427771
https://cloud.tencent.com/developer/article/1549817