迄今为止最完整的DDD实践
Posted 阿里技术
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了迄今为止最完整的DDD实践相关的知识,希望对你有一定的参考价值。
作者:章磊(章三) 阿里飞猪技术团队
一、为什么需要DDD
对于一个架构师来说,在软件开发中如何降低系统复杂度是一个永恒的挑战。
- 复杂系统设计: 系统多,业务逻辑复杂,概念不清晰,有什么合适的方法帮助我们理清楚边界,逻辑和概念
- 多团队协同: 边界不清晰,系统依赖复杂,语言不统一导致沟通和理解困难。有没有一种方式把业务和技术概念统一,大家用一种语言沟通。例如:航程是大家所理解的航程吗?
- 设计与实现一致性: PRD,详细设计和代码实现天差万别。有什么方法可以把业务需求快速转换为设计,同时还要保持设计与代码的一致性?
- 架构统一,可复用资产和扩展性: 当前取决于开发的同学具备很好的抽象能力和高编程的技能。有什么好的方法指导我们做抽象和实现。
二、DDD的价值
- 边界清晰的设计方法: 通过领域划分,识别哪些需求应该在哪些领域,不断拉齐团队对需求的认知,分而治之,控制规模。
- 统一语言: 团队在有边界的上下文中有意识地形成对事物进行统一的描述,形成统一的概念(模型)。
- 业务领域的知识沉淀: 通过反复论证和提炼模型,使得模型必须与业务的真实世界保持一致。促使知识(模型)可以很好地传递和维护。
- 面向业务建模: 领域模型与数据模型分离,业务复杂度和技术复杂度分离。
三、DDD架构
3.1 分层架构
- 用户接口层: 调用应用层完成具体用户请求。包含:controller,远程调用服务等
- 应用层App: 尽量简单,不包含业务规则,而只为了下一层中的领域对象做协调任务,分配工作,重点对领域层做编排完成复杂业务场景。包含:AppService,消息处理等
- 领域层Domain: 负责表达业务概念和业务逻辑,领域层是系统的核心。包含:模型,值对象,域服务,事件
- 基础层: 对所有上层提供技术能力,包括:数据操作,发送消息,消费消息,缓存等
- 调用关系: 用户接口层->应用层->领域层->基础层
- 依赖关系:用 户接口层->应用层->领域层->基础层
3.2 六边形架构
- 六边形架构: 系统通过适配器的方式与外部交互,将应用服务于领域服务封装在系统内部
- 分层架构: 它依然是分层架构,它核心改变的是依赖关系。
- 领域层依赖倒置: 领域层依赖基础层倒置成基础层依赖领域层,这个简单的变化使得领域层不依赖任务层,其他层都依赖领域层,使得领域层只表达业务逻辑且稳定。
3.3 调用链路
四、DDD的基本概念
4.1 领域模型
领域(战略):业务范围,范围就是边界。
子领域:领域可大可小,我们将一个领域进行拆解形成子领域,子领域还可以进行拆解。当一个领域太大的时候需要进行细化拆解。
模型(战术):基于某个业务领域识别出这个业务领域的聚合,聚合根,界限上下文,实体,值对象。
4.1.1 核心域
决定产品和公司核心竞争力的子域是核心域,它是业务成功的主要因素和公司的核心竞争力。直接对业务产生价值。
4.1.2 通用域
没有太多个性化的诉求,同时被多个子域使用的通用功能子域是通用域。例如,权限,登陆等等。间接对业务产生价值。
4.1.3 支撑域
支撑其他领域业务,具有企业特性,但不具有通用性。间接对业务产生价值。
4.1.4 为什么要划分核心域、通用域和支撑域
一个业务一定有他最重要的部分,在日常做业务判断和需求优先级判断的时候可以基于这个划分来做决策。例如:一个交易相关的需求和一个配置相关的需求排优先级,很明显交易是核心域,规则是支持域。同样我们认为是支撑域或者通用域的在其他公司可能是核心域,例如权限对于我们来说是通用域,但是对于专业做权限系统的公司,这个是核心域。
4.2 限界上下文(战略)
业务的边界的划分,这个边界可以是一个领域或者多个领域的集合。复杂业务需要多个域编排完成一个复杂业务流程。限界上下文可以作为微服务划分的方法。其本质还是高内聚低耦合,只是限界上下文只是站在更高的层面来进行划分。如何进行划分,我的方法是一个界限上下文必须支持一个完整的业务流程,保证这个业务流程所涉及的领域都在一个限界上下文中。
4.3 实体(ENTITY)
定义: 实体有唯一的标识,有生命周期且具有延续性。例如一个交易订单,从创建订单我们会给他一个订单编号并且是唯一的这就是实体唯一标识。同时订单实体会从创建,支付,发货等过程最终走到终态这就是实体的生命周期。订单实体在这个过程中属性发生了变化,但订单还是那个订单,不会因为属性的变化而变化,这就是实体的延续性。
实体的业务形态: 实体能够反映业务的真实形态,实体是从用例提取出来的。领域模型中的实体是多个属性、操作或行为的载体。
实体的代码形态: 我们要保证实体代码形态与业务形态的一致性。那么实体的代码应该也有属性和行为,也就是我们说的充血模型,但实际情况下我们使用的是贫血模型。贫血模型缺点是业务逻辑分散,更像数据库模型,充血模型能够反映业务,但过重依赖数据库操作,而且复杂场景下需要编排领域服务,会导致事务过长,影响性能。所以我们使用充血模型,但行为里面只涉及业务逻辑的内存操作。
实体的运行形态: 实体有唯一ID,当我们在流程中对实体属性进行修改,但ID不会变,实体还是那个实体。
实体的数据库形态: 实体在映射数据库模型时,一般是一对一,也有一对多的情况。
4.4 值对象(VALUEOBJECT)
定义:通过对象属性值来识别的对象,它将多个相关属性组合为一个概念整体。在 DDD 中用来描述领域的特定方面,并且是一个没有标识符的对象,叫作值对象。值对象没有唯一标识,没有生命周期,不可修改,当值对象发生改变时只能替换(例如String的实现)
值对象的业务形态: 值对象是描述实体的特征,大多数情况一个实体有很多属性,一般都是平铺,这些数据进行分类和聚合后能够表达一个业务含义,方便沟通而不关注细节。
值对象的代码形态: 实体的单一属性是值对象,例如:字符串,整型,枚举。多个属性的集合也是值对象,这个时候我们把这个集合设计为一个CLASS,但没有ID。例如商品实体下的航段就是一个值对象。航段是描述商品的特征,航段不需要ID,可以直接整体替换。商品为什么是一个实体,而不是描述订单特征,因为需要表达谁买了什么商品,所以我们需要知道哪一个商品,因此需要ID来标识唯一性。
我们看一下下面这段代码,person 这个实体有若干个单一属性的值对象,比如 Id、name 等属性;同时它也包含多个属性的值对象,比如地址 address。
值对象的运行形态: 值对象创建后就不允许修改了,只能用另外一个值对象来整体替换。当我们修改地址时,从页面传入一个新的地址对象替换调用person对象的地址即可。如果我们把address设计成实体,必然存在ID,那么我们需要从页面传入的地址对象的ID与person里面的地址对像的ID进行比较,如果相同就更新,如果不同先删除数据库在新增数据。
值对象的数据库形态: 有两种方式嵌入式和序列化大对象。
案例1:以属性嵌入的方式形成的人员实体对象,地址值对象直接以属性值嵌入人员实体中。
当我们只有一个地址的时候使用嵌入式比较好,如果多个地址必须有序列化大对象。同时可以支持搜索。
案例2:以序列化大对象的方式形成的人员实体对象,地址值对象被序列化成大对象 Json 串后,嵌入人员实体中。
支持多个地址存储,不支持搜索。
值对象的优势和局限:
1.简化数据库设计,提升数据库操作的性能(多表新增和修改,关联表查询)
2.虽然简化数据库设计,但是领域模型还是可以表达业务
3.序列化的方式会使搜索实现困难(通过搜索引擎可以解决)
4.5 聚合和聚合根
多个实体和值对象组成的我们叫聚合,聚合的内部一定的高内聚。这个聚合里面一定有一个实体是聚合根。
聚合与领域的关系:聚合也是范围的划分,领域也是范围的划分。领域与聚合可以是一对一,也可以是一对多的关系
聚合根的作用是保证内部的实体的一致性,对外只需要对聚合根进行操作。
4.6 限界上下文,域,聚合,实体,值对象的关系
领域包含限界上下文,限界上下文包含子域,子域包含聚合,聚合包含实体和值对象
4.7 事件风暴
参与者
除了领域专家,事件风暴的其他参与者可以是DDD专家、架构师、产品经理、项目经理、开发人员和测试人员等项目团队成员
事件风暴准备的材料
一面墙和一支笔。
事件风暴的关注点
在领域建模的过程中,我们需要重点关注这类业务的语言和行为。比如某些业务动作或行为(事件)是否会触发下一个业务动作,这个动作(事件)的输入和输出是什么?是谁(实体)发出的什么动作(命令),触发了这个动作(事件)…我们可以从这些暗藏的词汇中,分析出领域模型中的事件、命令和实体等领域对象。
实体执行命令产生事件。
业务场景的分析
通过业务场景和用例找出实体,命令,事件。
领域建模
领域建模时,我们会根据场景分析过程中产生的领域对象,比如命令、事件等之间关系,找出产生命令的实体,分析实体之间的依赖关系组成聚合,为聚合划定限界上下文,建立领域模型以及模型之间的依赖。领域模型利用限界上下文向上可以指导微服务设计,通过聚合向下可以指导聚合根、实体和值对象的设计。
五、如何建模
- 用例场景梳理:就是一句话需求,但我们需要把一些模糊的概念通过对话的方式逐步得到明确的需求,在加以提炼和抽象。
- 建模方法论:词法分析(找名词和动词),领域边界
- 模型验证
5.1 协同单自动化分单案例
5.1.1 领域建模
需求:我们需要把系统自动化失败转人工订单自动分配给小二,避免人工挑单和抢单,通过自动分配提升整体履约处理效率。
-
产品小A:把需求读了一遍.......。
-
开发小B:那就是将履约单分配给个小二对吧?
-
产品小A:不对,我们还需要根据一个规则自动分单,例如退票订单分给退票的小二
-
开发小B:恩,那我们可以做一个分单规则管理。例如:新增一个退票分单规则,在里面添加一批小二工号。履约单基于自身属性去匹配分单规则并找到一个规则,然后从分单规则里面选择一个小二工号,履约单写入小二工号即可。
-
产品小A:分单规则还需要有优先级,其中小二如果上班了才分配,如果下班了就不分配。
-
开发小B:优先级没有问题,在匹配分单规则方法里面按照优先级排序即可,不影响模型。而小二就不是简单一个工号维护在分单规则中,小二有状态了。
-
产品小A:分单规则里面添加小二操作太麻烦了,例如:每次新增一个规则都要去挑人,人也不一定记得住,实际客服在管理小二的时候是按照技能组管理的。
-
开发小B:恩,懂了,那就是通过新增一个技能组管理模块来管理小二。然后在通过分单规则来配置1个技能组即可。获取一个小二工号就在技能组里面了。
-
开发小B:总感觉不对,因为新增一个自动化分单需求,履约单就依赖了分单规则,履约单应该是一个独立的域,分单不是履约的能力,履约单实际只需要知道处理人是谁,至于怎么分配的他不太关心。应该由分单规则基于履约单属性找匹配一个规则,然后基于这个规则找到一个小二。履约单与分单逻辑解耦。
-
产品小A:分单要轮流分配或者能者多劳分配,小二之前处理过的订单和航司优先分配。
-
开发小B:获取小二的逻辑越来越复杂了,实际技能组才是找小二的核心,分单规则核心是通过履约单特征得到一个规则结果(技能组ID,分单策略,特征规则)。技能组基于分单规则的结果获得小二工号。
-
产品小A:还漏了一个信息,就是履约单会被多次分配的情况,每一个履约环节都可能转人工,客服需要知道履约单被处理多次的情况
-
开发小B:那用履约单无法表达了,我们需要新增一个概念叫协同单,协同单是为了协同履约单,通过协同推进履约单的进度。
-
产品小A:协同单概念很好,小二下班后,如果没有处理完,还可以转交给别人。
-
开发小B:恩,那只需要在协同单上增加行为即可
5.1.2 领域划分
沟通的过程就是推导和验证模型的过程,最后进行域的划分:
5.1.3 场景梳理
穷举所有场景,重新验证模型是否可以覆盖所有场景。
场景名称 | 锁 | 场景动作 | 域 | 域服务 | 事件 | 聚合根 | 方法 |
创建协同单 | 无 | 1、判断关联业务单是否非法 | 协同单 | 创建协同单 1、问题分类是否符合条件 (例如:商家用户发起自营->商家的协同单) 2、save | 协同单 | 创建协同单 | |
分配协同单 | 协同单ID | 分配协同单到人. 1、判断协同单状态(=待处理) 2、记录操作日志 3、save | 协同单 | 分配协同单 | 协同单 | 分配协同单 | |
受理协同单 | 协同单ID | 处理协同单 | 协同单 | 受理协同单 1.判断订单状态(=待处理/验收失败) 2.更改订单状态(待处理/验收失败->处理中) 3.记录操作日志 4.save | 协同单 | 受理协同单 | |
转交协同单 | 协同单ID | 转交协同单 | 协同单 | 转交协同单 1.判断订单状态.(=处理中、待处理)
2.更改协同人值对象(同一组织下的不同人,从坐席管理域中取) 3.记录操作日志 4.save | 协同单 | 转交协同单 | |
关闭协同单 | 协同单ID | 关闭协同单 | 协同单 | 关闭协同单 1.判断订单状态 (=处理中、待处理) 2.更改订单状态 (关闭) 3.记录操作日志 4.save | 协同单 | 关闭协同单 | |
处理协同单 | 协同单ID | 处理协同单 | 协同单 | 处理协同单 1.判断订单状态 (=处理中) 2.更改订单状态(处理中->待验收) 3.记录操作日志 4.save | 协同单 | 处理协同单 | |
驳回协同单 | 协同单ID | 驳回协同单 | 协同单 | 驳回协同单 1.判断订单状态 (=待验收) 2.更改订单状态(待验收->处理中) 3.记录操作日志 4.save | 协同单 | 驳回协同单 | |
完结协同单 | 协同单ID | 完结协同单 | 协同单 | 完结协同单 1.判断订单状态 (=待验收) 2.更改订单状态(待验收->已完结) 3.记录操作日志 4.save | 协同单 | 完结协同单 | |
拒绝协同单 | 协同单ID | 拒绝协同单 | 协同单 | 拒绝协同单 1.判断订单状态(=处理中、待处理) 2.更改订单状态(已拒绝) 3.记录操作日志 4.save | 协同单 | 拒绝协同单 | |
催单 | 协同单ID | 催单 | 协同单 | 催单 1.判断订单状态(=处理中、待处理) 2、修改催单值对象 3、记录操作日志 4、save | 协同单 | 催单 |
六、怎么写代码
6.1 DDD规范
每一层都定义了相应的接口主要目的是规范代码:
-
application:CRQS模式,ApplicationCmdService是command,ApplicationQueryService是query
-
service:是领域服务规范,其中定义了DomainService,应用系统需要继承它。
-
model:是聚合根,实体,值对象的规范。
- Aggregate和BaseAggregate:聚合根定义
- Entity和BaseEntity:实体定义
- Value和BaseValue:值对象定义
- Param和BaseParam:领域层参数定义,用作域服务,聚合根和实体的方法参数
- Lazy:描述聚合根属性是延迟加载属性,类似与hibernate。
- Field:实体属性,用来实现update-tracing
/**
* 实体属性,update-tracing
* @param <T>
*/
public final class Field<T> implements Changeable
private boolean changed = false;
private T value;
private Field(T value)
this.value = value;
public void setValue(T value)
if(!equalsValue(value))
this.changed = true;
this.value = value;
@Override
public boolean isChanged()
return changed;
public T getValue()
return value;
public boolean equalsValue(T value)
if(this.value == null && value == null)
return true;
if(this.value == null)
return false;
if(value == null)
return false;
return this.value.equals(value);
public static <T> Field<T> build(T value)
return new Field<T>(value);
-
repository
- Repository:仓库定义
- AggregateRepository:聚合根仓库,定义聚合根常用的存储和查询方法
-
event:事件处理
-
exception:定义了不同层用的异常
- AggregateException:聚合根里面抛的异常
- RepositoryException:基础层抛的异常
- EventProcessException:事件处理抛的
6.2 工程结构
6.2.1 application模块
- CRQS模式:commad和query分离。
- 重点做跨域的编排工作,无业务逻辑
6.2.2 domain模块
域服务,聚合根,值对象,领域参数,仓库定义
6.2.3 infrastructurre模块
所有技术代码在这一层。mybatis,redis,mq,job,opensearch代码都在这里实现,domain通过依赖倒置不依赖这些技术代码和JAR。
6.2.4 client模块
对外提供服务
6.2.5 model模块
内外都要用的共享对象
6.3 代码示例
6.3.1 application示例
public interface CaseAppFacade extends ApplicationCmdService
/**
* 接手协同单
* @param handleCaseDto
* @return
*/
ResultDO<Void> handle(HandleCaseDto handleCaseDto);
public class CaseAppImpl implements CaseAppFacade
@Resource
private CaseService caseService;//域服务
@Resource
CaseAssembler caseAssembler;//DTO转Param
@Override
public ResultDO<Void> handle(HandleCaseDto handleCaseDto)
try
ResultDO<Void> resultDO = caseService.handle(caseAssembler.from(handleCaseDto));
if (resultDO.isSuccess())
pushMsg(handleCaseDto.getId());
return ResultDO.buildSuccessResult(null);
return ResultDO.buildFailResult(resultDO.getMsg());
catch (Exception e)
return ResultDO.buildFailResult(e.getMessage());
- mapstruct:VO,DTO,PARAM,DO,PO转换非常方便,代码量大大减少。
- CaseAppImpl.handle调用域服务caseService.handle
6.3.2 domainService示例
public interface CaseService extends DomainService
/**
* 接手协同单
*
* @param handleParam
* @return
*/
ResultDO<Void> handle(HandleParam handleParam);
public class CaseServiceImpl implements CaseService
@Resource
private CoordinationRepository coordinationRepository;
@Override
public ResultDO<Void> handle(HandleParam handleParam)
SyncLock lock = null;
try
lock = coordinationRepository.syncLock(handleParam.getId().toString());
if (null == lock)
return ResultDO.buildFailResult("协同单handle加锁失败");
CaseAggregate caseAggregate = coordinationRepository.query(handleParam.getId());
caseAggregate.handle(handleParam.getFollowerValue());
coordinationRepository.save(caseAggregate);
return ResultDO.buildSuccessResult(null);
catch (RepositoryException | AggregateException e)
String msg = LOG.error4Tracer(OpLogConstant.traceId(handleParam.getId()), e, "协同单handle异常");
return ResultDO.buildFailResult(msg);
finally
if (null != lock)
coordinationRepository.unlock(lock);
- 领域层不依赖基础层的实现: coordinationRepository只是接口,在领域层定义好,由基础层依赖领域层实现这个接口
- 业务逻辑和技术解耦: 域服务这层通过调用coordinationRepository和聚合根将业务逻辑和技术解耦。
- 聚合根的方法无副作用: 聚合根的方法只对聚合根内部实体属性的改变,不做持久化动作,可反复测试。
- 模型与数据分离:
- 改变模型:caseAggregate.handle(handleParam.getFollowerValue());
- 改变数据:coordinationRepository.save(caseAggregate);事务是在save方法上
6.3.3 Aggregate,Entity示例
public class CaseAggregate extends BaseAggregate implements NoticeMsgBuilder
private final CaseEntity caseEntity;
public CaseAggregate(CaseEntity caseEntity)
this.caseEntity = caseEntity;
/**
* 接手协同单
* @param followerValue
* @return
*/
public void handle(FollowerValue followerValue) throws AggregateException
try
this.caseEntity.handle(followerValue);
catch (Exception e)
throw e;
public class CaseEntity extends BaseEntity
/**
* 创建时间
*/
private Field<Date> gmtCreate;
/**
* 修改时间
*/
private Field<Date> gmtModified;
/**
* 问题分类
*/
private Field<Long> caseType;
/**
* 是否需要支付
*/
private Field<Boolean> needPayFlag;
/**
* 是否需要自动验收通过协同单
*/
private Field<Integer> autoAcceptCoordinationFlag;
/**
* 发起协同人值对象
*/
private Field<CreatorValue> creatorValue;
/**
* 跟进人
*/
private Field<FollowerValue> followerValue;
/**
* 状态
*/
private Field<CaseStatusEnum> status;
/**
* 关联协同单id
*/
private Field<String> relatedCaseId;
/**
* 关联协同单类型
* @see 读配置 com.alitrip.agent.business.flight.common.model.dataobject.CoordinationCaseTypeDO
*/
private Field<String> relatedBizType;
/**
* 支付状态
*/
private Field<PayStatusEnum> payStatus;
省略....
public CaseFeatureValue getCaseFeatureValue()
return get(caseFeatureValue);
public Boolean isCaseFeatureValueChanged()
return caseFeatureValue.isChanged();
public void setCaseFeatureValue(CaseFeatureValue caseFeatureValue)
this.caseFeatureValue = set(this.caseFeatureValue, caseFeatureValue);
public Boolean isPayStatusChanged()
return payStatus.isChanged();
public Boolean isGmtCreateChanged()
return gmtCreate.isChanged();
public Boolean isGmtModifiedChanged()
return gmtModified.isChanged();
public Boolean isCaseTypeChanged()
return caseType.isChanged();
省略....
/**
* 接手
*/
public void handle(FollowerValue followerValue) throws AggregateException
if (isWaitProcess()||isAppointProcess())
this.setFollowerValue(followerValue);
this.setStatus(CaseStatusEnum.PROCESSING);
this.setGmtModified(new Date());
initCaseRecordValue(CaseActionNameEnum.HANDLE, null, followerValue);
else
throwStatusAggregateException();
省略....
- 充血模型VS贫血模型:
- 充血模型:表达能力强,代码高内聚,领域内封闭,聚合根内部结构对外不可见,通过聚合根的方法访问,适合复杂企业业务逻辑。
- 贫血模型:业务复杂之后,逻辑散落到大量方法中。
- 规范大于技巧:DDD架构可以避免引入一些其他概念,系统只有域,域服务,聚合根,实体,值对象,事件来构建系统。
聚合根的reconProcess的方法的业务逻辑被reconHandler和reconRiskHandler处理,必然这些handler要访问聚合根里面的实体的属性,那么逻辑就会散落。修改后:
没有引入其他概念,都是在聚合根里面组织实体完成具体业务逻辑,去掉了handler这种技术语言。
- 聚合根和实体定义的方法是具备单一原则,复用性原则与使用场景无关,例如:不能定义手工创建协调单和系统自动创建协同单,应该定义创建协同单。
- Update-tracing: handle方法修改属性后,然后调用 coordinationRepository.save(caseAggregate),我们只能全量属性更新。Update-tracing是监控实体的变更。 Entiy定义属性通过Field进行包装实现属性的变更状态记录,结合mapstruct转换PO实现Update-tracing。
修改了mapstruct生成转换代码的源码,修改后生成的代码:
当属性被改变后就转换到po中,这样就可以实现修改后的字段更新。修改后的mapstruct代码地址:git@gitlab.alibaba-inc.com:flight-agent/mapstruct.git
- idea的get和set方法自动生成: 由于使用field包装,需要自定义get和set生成代码
6.3.4 Repository示例
public interface CoordinationRepository extends Repository
/**
* 保存/更新
* @param aggregate
* @throws RepositoryException
*/
void save(CaseAggregate aggregate) throws RepositoryException;
@Repository
public class CoordinationRepositoryImpl implements CoordinationRepository
@Override
public void save(CaseAggregate aggregate) throws RepositoryException
try
//聚合根转PO,update-tracing技术
CasePO casePO = caseConverter.toCasePO(aggregate.getCase());
CasePO oldCasePO = null;
if (aggregate.getCase().isAppended())
casePOMapper.insert(casePO);
aggregate.getCase().setId(casePO.getId());
else
oldCasePO = casePOMapper.selectByPrimaryKey(casePO.getId());
casePOMapper.updateByPrimaryKeySelective(casePO);
// 发送协同单状态改变消息
if (CaseStatusEnum.FINISH.getCode().equals(casePO.getStatus())
|| CaseStatusEnum.WAIT_DISTRIBUTION.getCode().equals(casePO.getStatus())
|| CaseStatusEnum.PROCESSING.getCode().equals(casePO.getStatus())
|| CaseStatusEnum.APPOINT_PROCESS.getCode().equals(casePO.getStatus())
|| CaseStatusEnum.WAIT_PROCESS.getCode().equals(casePO.getStatus())
|| CaseStatusEnum.CLOSE.getCode().equals(casePO.getStatus())
|| CaseStatusEnum.REJECT.getCode().equals(casePO.getStatus())
|| CaseStatusEnum.PENDING_ACCEPTANCE.getCode().equals(casePO.getStatus()))
FollowerDto followerDto = new FollowerDto();
followerDto.setCurrentFollowerId(aggregate.getCase().getFollowerValue().getCurrentFollowerId());
followerDto.setCurrentFollowerGroupId(aggregate.getCase().getFollowerValue().getCurrentFollowerGroupId());
followerDto.setCurrentFollowerType(aggregate.getCase().getFollowerValue().getCurrentFollowerType());
followerDto.setCurrentFollowerName(aggregate.getCase().getFollowerValue().getCurrentFollowerName());
//拒绝和关闭都使用CLOSE
String tag = CaseStatusEnum.codeOf(casePO.getStatus()).name();
if(CaseStatusEnum.REJECT.name().equals(tag))
tag = CaseStatusEnum.CLOSE.name();
statusChangeProducer.send(CaseStatusChangeEvent.build()
.setId(casePO.getId())
.setFollowerDto(followerDto)
.setStatus(aggregate.getCase().getStatus().getCode())
.setCaseType(aggregate.getCase().getCaseType())
.setOldStatus(null != oldCasePO ? oldCasePO.getStatus() : null)
.setAppointTime(aggregate.getCase().getAppointTime()), (tag));
// 操作日志
if (CollectionUtils.isNotEmpty(aggregate.getCase().getCaseRecordValue()))
CaseRecordValue caseRecordValue = Lists.newArrayList(aggregate.getCase().getCaseRecordValue()).get(0);
caseRecordValue.setCaseId(casePO.getId());
recordPOMapper.insert(caseConverter.from(caseRecordValue));
catch (Exception e)
throw new RepositoryException("", e.getMessage(), e);
- CoordinationRepository接口定义在领域层
- CoordinationRepositoryImpl实现在基础层:数据库操作都是基于聚合根操作,保证聚合根里面的实体强一致性。
七、最后结束语
- 好的模型,可以沉淀组织资产,不好的模型,逐渐成为负债
- 功能才是表象,模型才是内在
- 建模过程是不断猜想与反驳的过程
- 演化观点是建模过程的基本心智模式
DDD 领域驱动设计落地实践:六步拆解 DDD
引言
相信通过前面几篇文章的介绍,大家对于 DDD 的相关理论以及实践的套路有了一定的理解,但是理解 DDD 理论和实践手段是一回事,能不能把这些理论知识实际应用到我们实际工作中又是另外一回事,因此本文通过实际的业务分析把之前文章中涉及的理论和手段全部带着大家走一遍,我想通过这种方式,让大家实际的感受下 DDD 落地过程中会遇到哪些问题以及我们应该怎样去解决这些问题。
项目需求信息
这里还是大家比较熟悉的电商场景来进行说明,我想这样大家比较好理解一点。在前段时间双十一,大家被各种购物优惠券的套路整的眼花缭乱,仿佛数学不好,都不配拿到最优惠的价格了。大家都在吐槽,就不能少点套路,买东西直接给我 5 折不就天下太平了吗?我想造成这种现象的原因大概就是中国电商行业的内卷吧,只有通过各种营销活动的堆积,才能让大家话更多的时间去浏览更过的商品,才能获得更好的留客以及交易。好了,跑题了,这些我们先不去关心。那我们今天就用这个折磨人的优惠券的流程作为设计实例来说明整个 DDD 的落地过程吧。优惠券的关键业务流程如下:
(1)当需要进行大促活动的时候,运营同学需要选定对应的商品,创建创建优惠券。
(2)运营同学需要创建营销活动,制定对应的营销活动规则,比如什么满减啊,跨店减啊类似这种折磨人脑细胞的规则,然后关联相应的优惠券,最后提交活动审批。审批通过后,进行营销活动发布。
(3)提交活动审批后,审批进行营销活动审批。
(4)用户在营销页面领取优惠券之后,下单购买商品之后,在付款的时候根据对应的优惠券进行付费金额计算并完成支付。
DDD 落地实践
项目背景信息我们大致了解之后,那么我们就要着手开始通过DDD来进行领域驱动设计的过程了。其实我们学习 DDD 理论以及方法不是最终的目的,而通过它实现实际的业务复杂度治理以及优化微服务设计才是真正的目的。
战略设计
在战略设计阶段,我们最主要的过程大致包括了业务场景分析、领域建模、划分边界上下文三个阶段。实际上战略设计师 DDD 过程中的核心步骤,
1、业务分析
在这个阶段我们所有做的就是进行全面的业务梳理,吧业务中涉及到的所有细节都梳理出来,为后续进行领域建模分析提供足够的、全面的业务输入。经常使用到的业务场景分析方法主要包括用例分析法、事件风暴法以及四色建模法。这里我们使用事件风暴进行业务场景的分析以及梳理。
(1)事前准备
在进行事件风暴之前我们需要进行一些准备,主要包括贴纸、笔以及讨论的会议室,会议室中最好不要有椅子,目的是想让大家都能够站立在一起、全神贯注的去进行业务讨论。
(2)邀请参会的人
会议的参与方主要包括业务、用户、PD、研发、测试、架构师等。
(3)业务讨论
首先确定我们今天需要讨论的业务是什么,目标是什么。像前文所说的那样,本次讨论的业务就是营销活动的优惠券业务,目标就是完成优惠券的业务梳理,确保没有业务方面的理解 gap,在团队中达成业务理解的的一致性。在这个过程中我们需要通过提问的方式来驱动交流。
a、分析业务中的事件,搞清楚事件发生的前因后果,什么意思呢?就是什么动作会导致当前时间的发生,当前这个事件发生后又会导致怎样的后果。这些我们都需要梳理清楚。还有一点需要注意, 我不但要关注正常的业务流程还要关注异常的业务流程。
b、寻找业务逻辑和业务规则,比如我们在提交活动前,需要确定这些优惠券适用哪些人、领取方式是怎样的以及生效事件是怎样的等等,这些都是我们在执行操作之前需要确定的业务规则。
如下图所示,我们将优惠券的业务流程进行了梳理,分别从操作人、事件、命令的方式来描述整个优惠券业务流转的过程。
注:在进行事件风暴过程中,所有的参与人都要全身投入整个过程,放下手机以及电脑,一起参与整个业务梳理过程,只有这样,事件风暴才可能有比较好的效果。
2、领域建模
在前面的事件风暴业务梳理中,我们已经把优惠券业务涉及到的参与者、动作以及事件等都进行了全面的梳理。那么接下来我们就要在此基础之上进行领域建模,这是整个 DDD 的核心。
(1)领域对象分析
如上面所示的事件风暴小黑板中的内容,我们需要在这些梳理出来的内容中找到对应的实体、值对象以及围绕这些的领域事件以及命令操作。根据分析,我们总整个业务过程中提取了优惠券、营销活动、活动审批单、活动规则、审批意见等实体以及值对象以及和这些领域对象相关的命令操作。
(2)构建业务聚合
完成领域对象分析之后,我们需要构建业务聚合。想要构建聚合,那么首先就要在实体中找到聚合根。我们先来回顾下聚合根的特点,聚合根一定是实体,那么它具有全局唯一的标识,另外它是具备生命周期的同时需要专门的模块来进行管理。根据这样的标准,在领域对象中我们发现优惠券、营销活动以及活动审批单是具备聚合根特征的,而营销规则、营销内容等是和营销活动紧密相关的,因此他们构成营销活动聚合关系。优惠券规则、优惠券类型等是和优惠券聚合根紧密相连的,所以他们构成优惠券聚合关系。同理活动审批单也会构成聚合关系。最终我们形成如下的聚合关系。
3、划分边界上下文
在上述步骤中,我们获得了整个业务流程中的所有聚合后,我们需要更具业务语义上下文将具体的聚合划分到对应的上下文中,因此我们可以把优惠券的业务分为优惠券、营销活动以及审批三个限界上下文。
战术设计
在战略设计阶段,我们通过事件风暴法对整体的业务进行了全部的梳理,同时构建了领域模型以及划分了边界下文。那么接下来我们就要将领域模型映射到工程结构以及代码中实现最终的实现落地。另外在这个阶段实际还有很多细节需要明确,那优惠券来说,它包含哪些属性,需要哪些领域服务,哪些需要设计为实体,哪些需要设计为值对象,这些都是需要在战术设计阶段明确下来。
1、微服务拆分
我们根据已经划分的边界上下文,我们可以拆分为优惠券服务、营销活动服务以及审批中心三个微服务,至于用户支付使用这块,还是由原先已存在支付服务来完成,只是在付款核算的时候需要使用到优惠券进行最后的金额计算。
2、领域分层
在领域分层方面,我们还是按照之前文章中所说的分层结构来进行,即 interfaces 层、biz 层、domain 层以及 instructure 层。每层代表的含义之前的文章中已经进行了详细的说明,大家可以翻看前面文章中的介绍,这里不再进行赘述了。
我们以优惠券为例,实际聚合中对象还需要进行进一步的细化。对于优惠券来说它实际上还有如下所示的值对象以及实体来组成实际的优惠券。同时在优惠券我们的梳理的领域服务还包括创建优惠券、查询优惠券以及修改优惠券状态,这些动作实际都应该在领域层通过领域服务的形式完成落地。而对应的 biz 层就相当于业务的编排组合,也就是实际的业务流程的串联。
3、代码结构
当我们把领域对象进行进一步的细化之后,同时把对应的领域服务敲定之后,我们可以把这些分析后的内容映射成工程分层后的代码了。如下图所示,即为优惠券的 domain 层的代码映射。
当然到这里并不意味着结束,其实在后续还有很多工作要做,比如详细设计、编写代码以及功能测试,特别实在详细设计阶段,我们还要涉及很多的细节问题的敲定,比如数据库表的设计、比如使用什么 MQ,用不用缓存,怎么保证缓存和数据库的数据一致性问题,分布式服务有没有分布式事务的问题,应该怎么解决?有没有服务幂等问题,应该怎么解决?这些都是需要在详细设计阶段进行确定的。因此 DDD 就像是框架,通过它把业务映射成为领域对象以及领域服务和领域事件,再把这些领域相关内容再读映射为实际的代码。使得我们的服务更加的逻辑清晰以及扩展性更强,但是分布式的技术实现细节,我们还是需要有对应的解决方案来进行解决。
总结
本文以电商行业的营销活动中的优惠券的发放和使用作为实际案例来阐述 DDD 领域驱动设计落地实践的过程,通过整个过程的梳理,为大家提炼了整个设计过程的精要,相信大家可以按照这样的思路在实际的工作中再结合各自的业务特征应该可以真正完成整个 DDD 的实践。万事开头难,相信只要大家能够亲自去参与或者主导一个 DDD 的落地实践过程,那么对于理解 DDD 这套架构设计方法论又会进入一个新的台阶。在后面的文章中再和大家聊聊落地 DDD 过程中可能会遇到的一些问题以及软件复杂度治理的问题。
真正的大师永远怀着一颗学徒的心
————————————————
以上是关于迄今为止最完整的DDD实践的主要内容,如果未能解决你的问题,请参考以下文章