迄今为止最完整的DDD实践
阿里妹导读
对于一个架构师来说,在软件开发中如何降低系统复杂度是一个永恒的挑战。
一、为什么需要DDD
- 复杂系统设计:系统多,业务逻辑复杂,概念不清晰,有什么合适的方法帮助我们理清楚边界,逻辑和概念?
- 多团队协同:边界不清晰,系统依赖复杂,语言不统一导致沟通和理解困难。有没有一种方式把业务和技术概念统一,大家用一种语言沟通。例如:航程是大家所理解的航程吗?
- 设计与实现一致性:PRD,详细设计和代码实现天差万别。有什么方法可以把业务需求快速转换为设计,同时还要保持设计与代码的一致性?
架构统一,可复用资产和扩展性:当前取决于开发的同学具备很好的抽象能力和高编程的技能。有什么好的方法指导我们做抽象和实现。
二、DDD的价值
- 边界清晰的设计方法:通过领域划分,识别哪些需求应该在哪些领域,不断拉齐团队对需求的认知,分而治之,控制规模。
- 统一语言:团队在有边界的上下文中有意识地形成对事物进行统一的描述,形成统一的概念(模型)。
- 业务领域的知识沉淀:通过反复论证和提炼模型,使得模型必须与业务的真实世界保持一致。促使知识(模型)可以很好地传递和维护。
面向业务建模:领域模型与数据模型分离,业务复杂度和技术复杂度分离。
3.1 分层架构
- 用户接口层:调用应用层完成具体用户请求。包含:controller,远程调用服务等
- 应用层App:尽量简单,不包含业务规则,而只为了下一层中的领域对象做协调任务,分配工作,重点对领域层做编排完成复杂业务场景。包含:AppService,消息处理等
- 领域层Domain:负责表达业务概念和业务逻辑,领域层是系统的核心。包含:模型,值对象,域服务,事件
- 基础层:对所有上层提供技术能力,包括:数据操作,发送消息,消费消息,缓存等
- 调用关系:用户接口层->应用层->领域层->基础层
依赖关系:用户接口层->应用层->领域层->基础层
3.2 六边形架构
- 六边形架构:系统通过适配器的方式与外部交互,将应用服务于领域服务封装在系统内部
- 分层架构:它依然是分层架构,它核心改变的是依赖关系。
领域层依赖倒置:领域层依赖基础层倒置成基础层依赖领域层,这个简单的变化使得领域层不依赖任务层,其他层都依赖领域层,使得领域层只表达业务逻辑且稳定。
3.3 调用链路
4.1 领域模型
- 领域(战略):业务范围,范围就是边界。
- 子领域:领域可大可小,我们将一个领域进行拆解形成子领域,子领域还可以进行拆解。当一个领域太大的时候需要进行细化拆解。
模型(战术):基于某个业务领域识别出这个业务领域的聚合,聚合根,界限上下文,实体,值对象。
4.2 限界上下文(战略)
4.3 实体(ENTITY)
4.4 值对象(VALUEOBJECT)
- 简化数据库设计,提升数据库操作的性能(多表新增和修改,关联表查询)。
- 虽然简化数据库设计,但是领域模型还是可以表达业务。
序列化的方式会使搜索实现困难(通过搜索引擎可以解决)。
4.5 聚合和聚合根
4.6 限界上下文,域,聚合,实体,值对象的关系
4.7 事件风暴
五、如何建模
- 用例场景梳理:就是一句话需求,但我们需要把一些模糊的概念通过对话的方式逐步得到明确的需求,在加以提炼和抽象。
- 建模方法论:词法分析(找名词和动词),领域边界
模型验证
5.1 协同单自动化分单案例
- 产品小A:把需求读了一遍.......。
- 开发小B:那就是将履约单分配给个小二对吧?
- 产品小A:不对,我们还需要根据一个规则自动分单,例如退票订单分给退票的小二
开发小B:恩,那我们可以做一个分单规则管理。例如:新增一个退票分单规则,在里面添加一批小二工号。履约单基于自身属性去匹配分单规则并找到一个规则,然后从分单规则里面选择一个小二工号,履约单写入小二工号即可。
- 产品小A:分单规则还需要有优先级,其中小二如果上班了才分配,如果下班了就不分配。
开发小B:优先级没有问题,在匹配分单规则方法里面按照优先级排序即可,不影响模型。而小二就不是简单一个工号维护在分单规则中,小二有状态了。
- 产品小A:分单规则里面添加小二操作太麻烦了,例如:每次新增一个规则都要去挑人,人也不一定记得住,实际客服在管理小二的时候是按照技能组管理的。
开发小B:恩,懂了,那就是通过新增一个技能组管理模块来管理小二。然后在通过分单规则来配置1个技能组即可。获取一个小二工号就在技能组里面了。
开发小B:总感觉不对,因为新增一个自动化分单需求,履约单就依赖了分单规则,履约单应该是一个独立的域,分单不是履约的能力,履约单实际只需要知道处理人是谁,至于怎么分配的他不太关心。应该由分单规则基于履约单属性找匹配一个规则,然后基于这个规则找到一个小二。履约单与分单逻辑解耦。
- 产品小A:分单要轮流分配或者能者多劳分配,小二之前处理过的订单和航司优先分配。
开发小B:获取小二的逻辑越来越复杂了,实际技能组才是找小二的核心,分单规则核心是通过履约单特征得到一个规则结果(技能组ID,分单策略,特征规则)。技能组基于分单规则的结果获得小二工号。
- 产品小A:还漏了一个信息,就是履约单会被多次分配的情况,每一个履约环节都可能转人工,客服需要知道履约单被处理多次的情况
开发小B:那用履约单无法表达了,我们需要新增一个概念叫协同单,协同单是为了协同履约单,通过协同推进履约单的进度。
- 产品小A:协同单概念很好,小二下班后,如果没有处理完,还可以转交给别人。
开发小B:恩,那只需要在协同单上增加行为即可。
左滑查看
场景名称 | 锁 | 场景动作 | 域 | 域服务 | 聚合根 | 方法 |
创建协同单 | 无 | 1、判断关联业务单是否非法 | 协同单 | 创建协同单 1、问题分类是否符合条件(例如:商家用户发起自营->商家的协同单) 2、save | 协同单 | 创建协同单 |
分配协同单 | 协同单ID | 分配协同单到人. 1、判断协同单状态(=待处理) 2、记录操作日志 3、save | 协同单 | 分配协同单 | 协同单 | 分配协同单 |
受理协同单 | 协同单ID | 处理协同单 | 协同单 | 受理协同单 1.判断订单状态(=待处理/验收失败) 2.更改订单状态(待处理/验收失败->处理中) 3.记录操作日志 4.save | 协同单 | 受理协同单 |
转交协同单 | 协同单ID | 转交协同单 | 协同单 | 转交协同单 1.判断订单状态.(=处理中、待处理) 2.校验转交的人是否在正确的组织下面 3.更改协同人值对象(同一组织下的不同人,从坐席管理域中取) 4.记录操作日志 5.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>
*/
publicfinalclassField<T> implementsChangeable{
privateboolean changed = false;
private T value;
privateField(T value){
this.value = value;
}
publicvoidsetValue(T value){
if(!equalsValue(value)){
this.changed = true;
}
this.value = value;
}
publicbooleanisChanged(){
return changed;
}
public T getValue(){
return value;
}
publicbooleanequalsValue(T value){
if(this.value == null && value == null){
returntrue;
}
if(this.value == null){
returnfalse;
}
if(value == null){
returnfalse;
}
returnthis.value.equals(value);
}
publicstatic <T> Field<T> build(T value){
returnnew Field<T>(value);
}
}
repository Repository:仓库定义 AggregateRepository:聚合根仓库,定义聚合根常用的存储和查询方法 event:事件处理 exception:定义了不同层用的异常 AggregateException:聚合根里面抛的异常 RepositoryException:基础层抛的异常 EventProcessException:事件处理抛的
6.2 工程结构
- CRQS模式:commad和query分离。
重点做跨域的编排工作,无业务逻辑。
域服务,聚合根,值对象,领域参数,仓库定义
6.3 代码示例
publicinterfaceCaseAppFacadeextendsApplicationCmdService{
/**
* 接手协同单
* @param handleCaseDto
* @return
*/
ResultDO<Void> handle(HandleCaseDto handleCaseDto);
}
publicclassCaseAppImplimplementsCaseAppFacade{
private CaseService caseService;//域服务
CaseAssembler caseAssembler;//DTO转Param
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。
publicinterfaceCaseServiceextendsDomainService{
/**
* 接手协同单
*
* @param handleParam
* @return
*/
ResultDO<Void> handle(HandleParam handleParam);
}
publicclassCaseServiceImplimplementsCaseService{
private CoordinationRepository coordinationRepository;
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方法上。
publicclass CaseAggregate extends BaseAggregate implements NoticeMsgBuilder {
private final CaseEntity caseEntity;
public CaseAggregate(CaseEntity caseEntity) {
this.caseEntity = caseEntity;
}
/**
* 接手协同单
* @param followerValue
* @return
*/
publicvoid handle(FollowerValue followerValue) throws AggregateException {
try {
this.caseEntity.handle(followerValue);
} catch (Exception e) {
throw e;
}
}
}
publicclass 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() {
returnget(caseFeatureValue);
}
publicBoolean isCaseFeatureValueChanged() {
return caseFeatureValue.isChanged();
}
publicvoid setCaseFeatureValue(CaseFeatureValue caseFeatureValue) {
this.caseFeatureValue = set(this.caseFeatureValue, caseFeatureValue);
}
publicBoolean isPayStatusChanged() {
return payStatus.isChanged();
}
publicBoolean isGmtCreateChanged() {
return gmtCreate.isChanged();
}
publicBoolean isGmtModifiedChanged() {
return gmtModified.isChanged();
}
publicBoolean isCaseTypeChanged() {
return caseType.isChanged();
}
省略....
/**
* 接手
*/
publicvoid handle(FollowerValue followerValue) throws AggregateException {
if (isWaitProcess()||isAppointProcess()) {
this.setFollowerValue(followerValue);
this.setStatus(CaseStatusEnum.PROCESSING);
this.setGmtModified(newDate());
initCaseRecordValue(CaseActionNameEnum.HANDLE, null, followerValue);
} else {
throwStatusAggregateException();
}
}
省略....
}
- 充血模型VS贫血模型:
- 充血模型:表达能力强,代码高内聚,领域内封闭,聚合根内部结构对外不可见,通过聚合根的方法访问,适合复杂企业业务逻辑。
- 贫血模型:业务复杂之后,逻辑散落到大量方法中。
规范大于技巧:DDD架构可以避免引入一些其他概念,系统只有域,域服务,聚合根,实体,值对象,事件来构建系统。
- 聚合根和实体定义的方法是具备单一原则,复用性原则与使用场景无关,例如:不能定义手工创建协调单和系统自动创建协同单,应该定义创建协同单。
Update-tracing:handle方法修改属性后,然后调用 coordinationRepository.save(caseAggregate),我们只能全量属性更新。Update-tracing是监控实体的变更。Entiy定义属性通过Field进行包装实现属性的变更状态记录,结合mapstruct转换PO实现Update-tracing。
if(caseEntity.isAppended() || caseEntity.isCaseTypeChanged()){
casePO.setCaseType( caseEntity.getCaseType() );
}
当属性被改变后就转换到po中,这样就可以实现修改后的字段更新。
idea的get和set方法自动生成:由于使用field包装,需要自定义get和set生成代码。
publicinterfaceCoordinationRepositoryextendsRepository {
/**
* 保存/更新
* @param aggregate
* @throws RepositoryException
*/
voidsave(CaseAggregate aggregate) throws RepositoryException;
}
@Repository
publicclassCoordinationRepositoryImplimplementsCoordinationRepository {
@Override
publicvoidsave(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) {
thrownew RepositoryException("", e.getMessage(), e);
}
}
}
CoordinationRepository接口定义在领域层。 - CoordinationRepositoryImpl实现在基础层:数据库操作都是基于聚合根操作,保证聚合根里面的实体强一致性。
最后结束语
- 好的模型,可以沉淀组织资产,不好的模型,逐渐成为负债。
- 功能才是表象,模型才是内在。
- 建模过程是不断猜想与反驳的过程。
演化观点是建模过程的基本心智模式。
- 阿里云开发者社区,千万开发者的选择阿里云开发者社区,百万精品技术内容、千节免费系统课程、丰富的体验场景、活跃的社群活动、行业专家分享交流,欢迎点击【阅读原文】加入我们。
关键词
方法
对象
代码
模型
值对象
最新评论
推荐文章
作者最新文章
你可能感兴趣的文章
Copyright Disclaimer: The copyright of contents (including texts, images, videos and audios) posted above belong to the User who shared or the third-party website which the User shared from. If you found your copyright have been infringed, please send a DMCA takedown notice to [email protected]. For more detail of the source, please click on the button "Read Original Post" below. For other communications, please send to [email protected].
版权声明:以上内容为用户推荐收藏至CareerEngine平台,其内容(含文字、图片、视频、音频等)及知识版权均属用户或用户转发自的第三方网站,如涉嫌侵权,请通知[email protected]进行信息删除。如需查看信息来源,请点击“查看原文”。如需洽谈其它事宜,请联系[email protected]。
版权声明:以上内容为用户推荐收藏至CareerEngine平台,其内容(含文字、图片、视频、音频等)及知识版权均属用户或用户转发自的第三方网站,如涉嫌侵权,请通知[email protected]进行信息删除。如需查看信息来源,请点击“查看原文”。如需洽谈其它事宜,请联系[email protected]。