4.1.1 核心域
决定产品和公司核心竞争力的子域是核心域,它是业务成功的主要因素和公司的核心竞争力。直接对业务产生价值。
4.1.2 通用域
没有太多个性化的诉求,同时被多个子域使用的通用功能子域是通用域。例如,权限,登陆等等。间接对业务产生价值。
4.1.3 支撑域
支撑其他领域业务,具有企业特性,但不具有通用性。间接对业务产生价值。
4.1.4 为什么要划分核心域、通用域和支撑域
一个业务一定有他最重要的部分,在日常做业务判断和需求优先级判断的时候可以基于这个划分来做决策。例如:一个交易相关的需求和一个配置相关的需求排优先级,很明显交易是核心域,规则是支持域。同样我们认为是支撑域或者通用域的在其他公司可能是核心域,例如权限对于我们来说是通用域,但是对于专业做权限系统的公司,这个是核心域。
业务的边界的划分,这个边界可以是一个领域或者多个领域的集合。复杂业务需要多个域编排完成一个复杂业务流程。限界上下文可以作为微服务划分的方法。其本质还是高内聚低耦合,只是限界上下文只是站在更高的层面来进行划分。如何进行划分,我的方法是一个界限上下文必须支持一个完整的业务流程,保证这个业务流程所涉及的领域都在一个限界上下文中。
定义:实体有唯一的标识,有生命周期且具有延续性。例如一个交易订单,从创建订单我们会给他一个订单编号并且是唯一的这就是实体唯一标识。同时订单实体会从创建,支付,发货等过程最终走到终态这就是实体的生命周期。订单实体在这个过程中属性发生了变化,但订单还是那个订单,不会因为属性的变化而变化,这就是实体的延续性。
实体的业务形态:实体能够反映业务的真实形态,实体是从用例提取出来的。领域模型中的实体是多个属性、操作或行为的载体。
实体的代码形态:我们要保证实体代码形态与业务形态的一致性。那么实体的代码应该也有属性和行为,也就是我们说的充血模型,但实际情况下我们使用的是贫血模型。贫血模型缺点是业务逻辑分散,更像数据库模型,充血模型能够反映业务,但过重依赖数据库操作,而且复杂场景下需要编排领域服务,会导致事务过长,影响性能。所以我们使用充血模型,但行为里面只涉及业务逻辑的内存操作。
实体的运行形态:实体有唯一ID,当我们在流程中对实体属性进行修改,但ID不会变,实体还是那个实体。
实体的数据库形态:实体在映射数据库模型时,一般是一对一,也有一对多的情况。
定义:通过对象属性值来识别的对象,它将多个相关属性组合为一个概念整体。在 DDD 中用来描述领域的特定方面,并且是一个没有标识符的对象,叫作值对象。值对象没有唯一标识,没有生命周期,不可修改,当值对象发生改变时只能替换(例如String的实现)。
值对象的业务形态:值对象是描述实体的特征,大多数情况一个实体有很多属性,一般都是平铺,这些数据进行分类和聚合后能够表达一个业务含义,方便沟通而不关注细节。
值对象的代码形态:实体的单一属性是值对象,例如:字符串,整型,枚举。多个属性的集合也是值对象,这个时候我们把这个集合设计为一个CLASS,但没有ID。例如商品实体下的航段就是一个值对象。航段是描述商品的特征,航段不需要ID,可以直接整体替换。商品为什么是一个实体,而不是描述订单特征,因为需要表达谁买了什么商品,所以我们需要知道哪一个商品,因此需要ID来标识唯一性。
我们看一下下面这段代码,person这个实体有若干个单一属性的值对象,比如Id、name等属性;同时它也包含多个属性的值对象,比如地址address。
值对象的运行形态:值对象创建后就不允许修改了,只能用另外一个值对象来整体替换。当我们修改地址时,从页面传入一个新的地址对象替换调用person对象的地址即可。如果我们把address设计成实体,必然存在ID,那么我们需要从页面传入的地址对象的ID与person里面的地址对像的ID进行比较,如果相同就更新,如果不同先删除数据库在新增数据。
值对象的数据库形态:有两种方式嵌入式和序列化大对象。
案例1:以属性嵌入的方式形成的人员实体对象,地址值对象直接以属性值嵌入人员实体中。
当我们只有一个地址的时候使用嵌入式比较好,如果多个地址必须有序列化大对象,同时可以支持搜索。
案例2:以序列化大对象的方式形成的人员实体对象,地址值对象被序列化成大对象Json串后,嵌入人员实体中。
支持多个地址存储,不支持搜索。
值对象的优势和局限:
多个实体和值对象组成的我们叫聚合,聚合的内部一定的高内聚。这个聚合里面一定有一个实体是聚合根。
聚合与领域的关系:聚合也是范围的划分,领域也是范围的划分。领域与聚合可以是一对一,也可以是一对多的关系。
聚合根的作用是保证内部的实体的一致性,对外只需要对聚合根进行操作。
领域包含限界上下文,限界上下文包含子域,子域包含聚合,聚合包含实体和值对象。
参与者
除了领域专家,事件风暴的其他参与者可以是DDD专家、架构师、产品经理、项目经理、开发人员和测试人员等项目团队成员。
事件风暴准备的材料
一面墙和一支笔。
事件风暴的关注点
在领域建模的过程中,我们需要重点关注这类业务的语言和行为。比如某些业务动作或行为(事件)是否会触发下一个业务动作,这个动作(事件)的输入和输出是什么?是谁(实体)发出的什么动作(命令),触发了这个动作(事件)…我们可以从这些暗藏的词汇中,分析出领域模型中的事件、命令和实体等领域对象。
实体执行命令产生事件。
业务场景的分析
通过业务场景和用例找出实体,命令,事件。
领域建模
领域建模时,我们会根据场景分析过程中产生的领域对象,比如命令、事件等之间关系,找出产生命令的实体,分析实体之间的依赖关系组成聚合,为聚合划定限界上下文,建立领域模型以及模型之间的依赖。领域模型利用限界上下文向上可以指导微服务设计,通过聚合向下可以指导聚合根、实体和值对象的设计。
5.1.1 领域建模
需求:我们需要把系统自动化失败转人工订单自动分配给小二,避免人工挑单和抢单,通过自动分配提升整体履约处理效率。
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.记录操作日志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 | 协同单 | 催单 |
每一层都定义了相应的接口主要目的是规范代码:
/**
* 实体属性,update-tracing
* @param
*/
public final class Field 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 Field build(T value){
return new Field(value);
}
}
6.2.1 application模块
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 handle(HandleCaseDto handleCaseDto);
}
public class CaseAppImpl implements CaseAppFacade {
@Resource
private CaseService caseService;//域服务
@Resource
CaseAssembler caseAssembler;//DTO转Param
@Override
public ResultDO handle(HandleCaseDto handleCaseDto) {
try {
ResultDO 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());
}
}
}
6.3.2 domainService示例
public interface CaseService extends DomainService {
/**
* 接手协同单
*
* @param handleParam
* @return
*/
ResultDO handle(HandleParam handleParam);
}
public class CaseServiceImpl implements CaseService {
@Resource
private CoordinationRepository coordinationRepository;
@Override
public ResultDO 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);
}
}
}
}
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 gmtCreate;
/**
* 修改时间
*/
private Field gmtModified;
/**
* 问题分类
*/
private Field caseType;
/**
* 是否需要支付
*/
private Field needPayFlag;
/**
* 是否需要自动验收通过协同单
*/
private Field autoAcceptCoordinationFlag;
/**
* 发起协同人值对象
*/
private Field creatorValue;
/**
* 跟进人
*/
private Field followerValue;
/**
* 状态
*/
private Field status;
/**
* 关联协同单id
*/
private Field relatedCaseId;
/**
* 关联协同单类型
* @see 读配置 com.alitrip.agent.business.flight.common.model.dataobject.CoordinationCaseTypeDO
*/
private Field relatedBizType;
/**
* 支付状态
*/
private Field 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();
}
}
省略....
}
聚合根的reconProcess的方法的业务逻辑被reconHandler和reconRiskHandler处理,必然这些handler要访问聚合根里面的实体的属性,那么逻辑就会散落。修改后:
没有引入其他概念,都是在聚合根里面组织实体完成具体业务逻辑,去掉了handler这种技术语言。
修改了mapstruct生成转换代码的源码,修改后生成的代码:
if(caseEntity.isAppended() || caseEntity.isCaseTypeChanged()){
casePO.setCaseType( caseEntity.getCaseType() );
}
当属性被改变后就转换到po中,这样就可以实现修改后的字段更新。
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);
}
}
}
作者|章磊
点击立即免费试用云产品 开启云上实践之旅!
原文链接
本文为阿里云原创内容,未经允许不得转载