其他分享
首页 > 其他分享> > 领域驱动设计落地经验

领域驱动设计落地经验

作者:互联网

从事件风暴建模学到什么

 

在这里我说一下电商中比较核心的一个流程。在京东购物我们会选择很多需要的商品添加到购物车,在双十一的时候会凑单满减,然后从购物车选中下单。现在我们要设计的部分是用户在选择多件商品时自动给用户使用上最优的多种促销活动,在用户下单的时候能够计算好用户应该付多少钱,每件商品分别应付和优惠多少钱。后面的表达我会用算价来代表这个流程。

领域知识的构成

在领域驱动设计中很强调领域专家这角色,与团队人员共同协作完成任务。而往往团队人员就拥有领域专家所拥有的部分知识,从而承担领域专家的职责,那么剩下的领域知识就需要靠团队人员借助外援来填补,方式包括但不限于以下三种方式:

  1. 通过网络渠道(论文、文章、书籍)获得。
  2. 请教身边有相关经验的朋友。
  3. 通过竞品分析获得。

当我们团队获得该领域下主要的领域知识后,需要结合实际需求进行战略设计和战术设计,就可以通过事件风暴建模方法进行领域建模。

本来是想着拿实际的例子来讲一遍事件风暴建模的过程,现在想想与其照本宣科的讲知识,不如写写经验和感悟来的实在。

事件风暴 VS 传统开发

事件风暴建模的标准流程可以很轻松地找到,这里不再赘述。主要说下从传统软件开发模型到领域驱动设计的领域建模,发生了什么变化。

传统模式:产品需求->需求分析->详细设计->ER模型->UML 设计
DDD 模式:事件风暴->产品愿景->场景分析->领域建模->微服务拆分与设计。

在传统模式下的产出的是可直接落地的设计结果,但是缺乏顶层设计,对于后期的变更维护难以高效支撑。而 DDD 的关注点更多的是顶层设计和概念模型,概念模型并不是可直接落地的结果,这样的优势便是在后期的扩展和变更中更容易。

子域拆分的关键经验

关于如何拆分子域,看了很多的内容后得到的一句话:『凭经验』,这个就让人很糊涂,我如何知道我拆分的是否准确。

当我带着问题去找书查资料,收获还是比较快的,有一段话驱散了一部分迷雾:『领域的边界划分不断演绎,只要发现复杂性凝聚的地方,就划定为有界上下文,割裂它与其他系统的关系,并派出精兵强将专门对付。』它给了我两个点醒:

  1. 领域的边界是不断演绎的。
  2. 领域内部是高内聚的,领域间是低耦合的。

从这两点出发,可以通过以下两点执行:

  1. 和领域专家沟通现在,并预判一下未来。
  2. 分析领域内头部公司的策略。

领域建模的关键经验

假定产品愿景是可行并且可执行的。在场景分析和领域建模的过程,有个通用的范式。

  1. 提取业务中的动词名词识别为领域概念。
  2. 通过业务中的定语对领域概念进行归纳抽象
  3. 对确定的领域概念进行关系确认

由此我们可以得出领域分析模型,这是一个比较抽象的模型,此时还无法落地。从复杂性角度来看领域建模控制的是业务复杂性。

复杂性问题控制方式

在之前的文章中也提到过三点:

  1. 抽象
  2. 分治
  3. 领域知识

现在反过来看,提炼领域概念是抽象,子域拆分是分治,而要做到这两点的正需要的是领域知识。领域驱动设计不仅告诉了我们『道』,也告诉了我们『术』。

该内容为作者(知一)原创,首发在个人博客 https://noogel.xyz 和微信公众号『知一杂谈』,欢迎 关注、点赞、留言~

谈谈领域驱动设计的落地

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

  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 形式提供的一整套能力接口包装而得。

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

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

该内容为作者(知一)原创,首发在个人博客 https://noogel.xyz 和微信公众号『知一杂谈』,欢迎 关注、点赞、留言~

参考

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

文章

后端开发实践系列——领域驱动设计(DDD)编码实践 - Thoughtworks洞见
01|领域驱动设计到底在讲什么? - 知乎
构建领域驱动设计知识体系 - 云+社区 - 腾讯云

标签:场景,return,落地,xyz,领域,经验,设计,驱动,noogel
来源: https://blog.csdn.net/noogel/article/details/122591684