领域驱动落地实战

前言

最近接手了一个项目,历史悠久,技术债欠的比较多,每次迭代上线心惊胆战,本着边换轮子边前进的原则,对系统进行改造升级。同时由于团队里面新人较多,业务逻辑还需要学习,最好在改造的同时沉淀一些业务领域知识。DDD的思想刚好对目前的情况是一种比较好的解法,那么首先就从应用架构开始了。 

应用架构的目的

让团队成员能有一个统一的开发规范,降低沟通成本,提升效率和代码质量。让系统安全、稳定、快速迭代。

下面通过一个案例分析来引出我们的应用架构的设计。

一、案例分析

功能

线索公海的申领

功能代码段

@PostMapping("/publicsea/apply")
@Validated
public CommonResult apply(@RequestBody BasePcRequest<PublicSeaApplyRequest> request) {
    BaseAppRequest<PublicSeaApplyRequest> baseAppRequest = new BaseAppRequest<>();
    baseAppRequest.setData(request.getData());
    baseAppRequest.setUserInfoDetailVo(request.getUserInfoDetailVo());
    applyController.apply(baseAppRequest);
    ThreadCache.set(CommonConstants.CURRENT_THREAD_USER, request.getUserInfoDetailVo());
    ApplyResponse applyResponse = businessFlowUpdateService.apply(baseAppRequest);
    if (applyResponse.getKeys().size() > 0) {
        return new CommonResult<>(ResultCode.SUCCESS.getCode(), applyResponse.getMessage(), applyResponse.getKeys());
    }
    return new CommonResult<>(ResultCode.FAILED.getCode(), applyResponse.getMessage(), applyResponse.getKeys());
}
@Override
public ApplyResponse apply(BaseAppRequest<PublicSeaApplyRequest> request) {
  
    Date now = new Date();
    ApplyResponse applyResponse = new ApplyResponse();
  
    PublicSeaApplyRequest applyRequest = request.getData();
    //获取当前登录人信息
    UserInfoDetailVo userInfoDetailVo = request.getUserInfoDetailVo();
  
    //根据stage执行不同的逻辑
    //所选阶段不存在 抛出异常
    if (!ObjectEnum.isExist(applyRequest.getStage())) {
        throw new BusinessRunTimeException(BusinessRuntimeExceptionEnum.PHASE_NOT_EXIST.getCode(), BusinessRuntimeExceptionEnum.PHASE_NOT_EXIST.getMessage());
    }
  
    List<String> ids = applyRequest.getKeys();
  
    final int totalCount = applyRequest.getKeys().size();
  
    CheckPublicApplyBo checkPublicApplyBo = null;
    if (ObjectEnum.CLUE.getApiName().equals(applyRequest.getStage()) || ObjectEnum.CUSTOMER.getApiName().equals(applyRequest.getStage())) {
        checkPublicApplyBo = checkPublicSeaAuth(userInfoDetailVo, applyRequest.getStage(), applyRequest.getKeys());
        ids = checkPublicApplyBo.getKeys();
        if (CollectionUtils.isEmpty(ids)) {
            throw new BusinessParamCheckingException(BusinessParamCheckingExceptionEnum.PUBLIC_SEA_NOT_EXIST.getCode(), BusinessParamCheckingExceptionEnum.PUBLIC_SEA_NOT_EXIST.getMessage());
        }
    }
  
  
    //保有量判断
    HoldSize holdSize = resourceHoldUtil.getResourceHold(userInfoDetailVo.getPid(), applyRequest.getStage(), userInfoDetailVo.getUserWid(), ids.size());
    int size = holdSize.getHold();
    if (size <= 0) {
        //代表保有量已经满了
        applyResponse.setMessage("领取失败" + totalCount + "条[超出保有量上限,最多还能领取0条]");
        applyResponse.setKeys(new ArrayList<>());
        applyResponse.setReturnCode(BusinessRuntimeExceptionEnum.CAPACITY_IS_FULL.getCode());
        return applyResponse;
    }
    if (size < ids.size()) {
        applyResponse.setMessage("领取失败" + totalCount + "条[超出保有量上限,最多还能领取" + size + "条]");
        applyResponse.setKeys(new ArrayList<>());
        applyResponse.setReturnCode(BusinessRuntimeExceptionEnum.CAPACITY_IS_FULL.getCode());
        return applyResponse;
    }
    //校验保有量高级规则
    HoldValidateResultBo resultBo = resourceHoldUtil.validateHoldSeniorRuleByKeys(
            userInfoDetailVo.getPid(), applyRequest.getStage(), holdSize, ids);
    if (resultBo.isOverHold()) {
        applyResponse.setMessage("领取失败" + totalCount + "条" + resultBo.getOverHoldTips());
        applyResponse.setKeys(new ArrayList<>());
        applyResponse.setReturnCode(BusinessRuntimeExceptionEnum.CAPACITY_IS_FULL.getCode());
        return applyResponse;
    }
  
    String applyLimitMsg = "";
    if (null != checkPublicApplyBo) {
        List<ApplyLimitCheckBo> applyLimitCheckBos = new ArrayList<>();
        if (ObjectEnum.CLUE.getApiName().equals(applyRequest.getStage())) {
            for (TScrmClue clue : checkPublicApplyBo.getClues()) {
                if (clue.getDeadline().compareTo(CustomizeDateUtils.parseDateStr(SystemConstant.publicDeadline, CustomizeDateUtils.parsePatterns[1])) == 0) {
                    //只针对回收资源
                    continue;
                }
                ApplyLimitCheckBo applyLimitCheckBo = new ApplyLimitCheckBo();
                applyLimitCheckBo.setObjectKey(clue.getClueKey());
                applyLimitCheckBo.setFormerOwner(clue.getFormerOwner());
                applyLimitCheckBo.setOwner(userInfoDetailVo.getUserWid());
                applyLimitCheckBo.setPoolTime(clue.getPoolingTime());
                applyLimitCheckBo.setPublicseaId(clue.getPublicSeaId());
                applyLimitCheckBos.add(applyLimitCheckBo);
            }
        }
  
        if (ObjectEnum.CUSTOMER.getApiName().equals(applyRequest.getStage())) {
            for (TScrmCustomer customer : checkPublicApplyBo.getCustomers()) {
                if (customer.getDeadline().compareTo(CustomizeDateUtils.parseDateStr(SystemConstant.publicDeadline, CustomizeDateUtils.parsePatterns[1])) == 0) {
                    continue;
                }
                ApplyLimitCheckBo applyLimitCheckBo = new ApplyLimitCheckBo();
                applyLimitCheckBo.setPublicseaId(customer.getPublicSeaId());
                applyLimitCheckBo.setPoolTime(customer.getPoolingTime());
                applyLimitCheckBo.setOwner(userInfoDetailVo.getUserWid());
                applyLimitCheckBo.setFormerOwner(customer.getFormerOwner());
                applyLimitCheckBo.setObjectKey(customer.getCustomerKey());
                applyLimitCheckBos.add(applyLimitCheckBo);
            }
        }
  
  
        Map<String, String> map = tScrmPublicseaDropRuleService.batchCheckApplyLimit(userInfoDetailVo.getPid(), applyRequest.getStage(), applyLimitCheckBos);
  
        if (MapUtil.isNotEmpty(map)) {
            applyLimitMsg = map.values().iterator().next();
            ids.removeAll(map.keySet());
        }
  
        if (CollectionUtils.isEmpty(ids)) {
            applyResponse.setKeys(Collections.emptyList());
            applyResponse.setMessage(String.format(SystemConstant.OPERATION_FAIL, 0, totalCount, map.values().iterator().next()));
            return applyResponse;
        }
    }
  
  
    CommonBatchUpdateBo batchUpdateBo = new CommonBatchUpdateBo();
    FieldConditionBo conditionBo = new FieldConditionBo();
    List<FieldConditionBo> fieldConditionBos = new ArrayList<>();
  
    /*通过配置读取是否更新最近跟进人和最近跟进时间  线索和客户读取配*/
    if (ObjectEnum.CLUE.getApiName().equals(request.getData().getStage())) {
        ThreadCache.set(CommonConstants.FOLLOW_TYPE, BusinessFlowTypeEnum.CLUE_OPENSEACUSTOMER_RECEIVE);
    }
    if (ObjectEnum.CUSTOMER.getApiName().equals(request.getData().getStage())) {
        ThreadCache.set(CommonConstants.FOLLOW_TYPE, BusinessFlowTypeEnum.CUSTOMER_OPENSEACUSTOMER_RECEIVE);
    }
  
    List<String> columnNames = new ArrayList<>();
    List<Object> columnValues = new ArrayList<>();
  
    columnNames.add(FieldKeyEnum.OWNER.getFieldKey());
    columnNames.add(FieldKeyEnum.CLUE_CLAIM_TIME.getFieldKey());
    columnValues.add(userInfoDetailVo.getUserWid());
    columnValues.add(now);
  
    columnNames.add(FieldKeyEnum.LAST_FOLLOW_TIME.getFieldKey());
    columnNames.add(FieldKeyEnum.LAST_FOLLOW_USER_WID.getFieldKey());
    columnValues.add(now);
    columnValues.add(userInfoDetailVo.getUserWid());
  
    columnNames.add(FieldKeyEnum.UPDATE_USER_WID.getFieldKey());
    columnValues.add(userInfoDetailVo.getUserWid());
  
    if (applyRequest.getStage().equals(ObjectEnum.CLUE.getApiName())) {
        conditionBo.setApiName(FieldKeyEnum.CLUE_KEY.getFieldKey());
        columnNames.add(FieldKeyEnum.CLUE_FOLLOW_STATUS.getFieldKey());
        columnValues.add(CluefollowStatusEnum.FOLLOW_STATUS_NUCONTACT.getCode());
    } else if (applyRequest.getStage().equals(ObjectEnum.CUSTOMER.getApiName())) {
        conditionBo.setApiName(FieldKeyEnum.CUSTOMER_KEY.getFieldKey());
        columnNames.add(FieldKeyEnum.CUSTOMER_FOLLOW_STATUS.getFieldKey());
        columnValues.add(CustomerFollowStatusEnum.NO_FOLLOW.getCode());
        columnNames.add(FieldKeyEnum.CUSTOMER_GROUP.getFieldKey());
        columnValues.add(Lists.newArrayList());
    } else if (applyRequest.getStage().equals(ObjectEnum.NICHE.getApiName())) {
        throw new BusinessRunTimeException(BusinessRuntimeExceptionEnum.NOT_SUPPORTED.getCode(), BusinessRuntimeExceptionEnum.NOT_SUPPORTED.getMessage());
    } else {
        throw new BusinessRunTimeException(BusinessRuntimeExceptionEnum.NOT_SUPPORTED.getCode(), BusinessRuntimeExceptionEnum.NOT_SUPPORTED.getMessage());
    }
    conditionBo.setCompareEnum(CompareEnum.IN);
    conditionBo.setValueList(ids);
    fieldConditionBos.add(conditionBo);
  
    batchUpdateBo.setFieldConditionBo(fieldConditionBos);
    batchUpdateBo.setFieldUpdateBo(businessFlowCommon.setColumn(columnNames.toArray(new String[]{}), columnValues.toArray(new Object[]{})));
    List<SyncChangeOwnerBo> syncChangeOwnerBos = new ArrayList<>();
  
    StateChangeLogBo stateChangeLogBo = new StateChangeLogBo();
    stateChangeLogBo.setCreateUserWid(userInfoDetailVo.getUserWid());
    stateChangeLogBo.setPid(userInfoDetailVo.getPid());
    stateChangeLogBo.setOpContent("领取");
    stateChangeLogBo.setOpType(OpLogTypeEnum.CLAIM.getCode());
    stateChangeLogBo.setOpResult(SystemConstant.BYTE_YES);
    stateChangeLogBo.setStage(applyRequest.getStage());
    stateChangeLogBo.setOpTime(now);
    stateChangeLogBo.setOpUserNo(userInfoDetailVo.getUserWid());
  
    ids = businessFlowCommon.applyAndAssign(applyRequest.getStage(), userInfoDetailVo, batchUpdateBo, ids, stateChangeLogBo, 0L);
  
    if (CollectionUtils.isNotEmpty(ids)) {
        ids.forEach(id -> {
            if (applyRequest.getStage().equals(ObjectEnum.CLUE.getApiName())) {
  
                SyncChangeOwnerBo syncChangeOwnerBo = new SyncChangeOwnerBo();
  
                syncChangeOwnerBo.setClueKey(id);
                syncChangeOwnerBo.setOperationType(SyncOpTypeEnum.CLAIM.getCode());
                syncChangeOwnerBo.setPid(userInfoDetailVo.getPid());
                syncChangeOwnerBo.setOwner(userInfoDetailVo.getUserWid());
                syncChangeOwnerBos.add(syncChangeOwnerBo);
  
            }
        });
  
        if (applyRequest.getStage().equals(ObjectEnum.CUSTOMER.getApiName())) {
            //同步申领商机
            List<String> nicheKeys = businessFlowMapper.getNotWinNicheKeys(ids, userInfoDetailVo.getPid());
            if (!nicheKeys.isEmpty()) {
                CommonBatchUpdateBo commonBatchUpdateBo = new CommonBatchUpdateBo();
                FieldConditionBo fieldConditionBo = new FieldConditionBo();
                List<FieldConditionBo> conditionBos = new ArrayList<>();
  
                fieldConditionBo.setApiName(FieldKeyEnum.NICHE_KEY.getFieldKey());
                fieldConditionBo.setCompareEnum(CompareEnum.IN);
                fieldConditionBo.setValueList(nicheKeys);
                conditionBos.add(fieldConditionBo);
  
                String[] nicheStrs = {FieldKeyEnum.OWNER.getFieldKey(), FieldKeyEnum.LAST_UPDATE_USER_WID.getFieldKey(),
                        FieldKeyEnum.LAST_UPDATE_TIME.getFieldKey(), FieldKeyEnum.NICHE_CLAIM_TIME.getFieldKey(),
                        FieldKeyEnum.LAST_FOLLOW_TIME.getFieldKey(), FieldKeyEnum.LAST_FOLLOW_USER_WID.getFieldKey()};
                Object[] nicheO = {userInfoDetailVo.getUserWid(), userInfoDetailVo.getUserWid(), now, now, now, userInfoDetailVo.getUserWid()};
                commonBatchUpdateBo.setFieldConditionBo(conditionBos);
                commonBatchUpdateBo.setFieldUpdateBo(businessFlowCommon.setColumn(nicheStrs, nicheO));
                commonAdapterServiceImpl.commonBatchUpdate(ObjectEnum.NICHE, userInfoDetailVo.getPid(), commonBatchUpdateBo);
            }
        }
    }
  
    if (ids.size() < totalCount) {
        int failSize = totalCount - ids.size();
        if (StringUtils.isNotBlank(applyLimitMsg)) {
            applyResponse.setMessage(String.format(SystemConstant.OPERATION_FAIL, ids.size(), failSize, applyLimitMsg));
        } else {
            applyResponse.setMessage(String.format(SystemConstant.OPERATION_FAIL, ids.size(), failSize, "数据有变动,刷新后重试"));
        }
    } else {
        applyResponse.setMessage("领取成功" + ids.size() + "条");
    }
    applyResponse.setKeys(ids);
    return applyResponse;
}

大概流程图

图片

一眼无法看清所依赖的对象及服务。

存在的问题

一段业务代码里包含了参数校验、数据读取、业务计算、数据存储等等逻辑,这种代码样式是通称为“事物脚本”。会有以下几个问题:

可维护性差

例如 TScrmClue 直接映射的是数据库中的表结构,一旦表结构发生变更或者换存储方式,很多代码需要随之改变。

参数校验散落在各个角落,修改需要通盘考虑。

可扩展性差

事物脚本代码通常在写第一个需求时实现起来比较快,随着业务场景逐渐变多,可扩展性会越来越差。代码复用性低,copy 痕迹很常见。

if(客资对象 == 线索){
        公共逻辑
        if (动作== 申领){
                to do
        }
        .......
}
........
可测试性差

如上图,如果修改了公共逻辑,基本上需要对接口做覆盖测试,测试工作量大(N*M)。

小结

以上分析来看,在设计的时候违背了以下几个软件设计原则:

单一原则:单一性原则要求一个对象/类应该只有一个变更的原因。但是在这个流程里,代码可能会因为任意一个外部依赖或计算逻辑的改变而改变。

开闭原则:开放封闭原则指开放扩展,但是封闭修改。以上代码数据过滤逻辑可以封装在不可变代码里,如果需求增加,可以扩展其实现。

二、重构方案

结合我们现在的业务特点及技术条件。利用DDD的思想来梳理业务逻辑,进行重构。首先要说明的是:DDD 不是一套框架,而是一种架构思想,在代码落地方面,是一种代码组织方式。

步骤:

1、抽取DP

DP概念:Domain Primitive,特定领域里,拥有精准定义的、可自我验证的、拥有行为的 Value Object。

使用DP的收益:代码遵循了 DRY(一个规则实现一次) 原则和单一性原则。

收集DP行为,替换数据校验和无状态逻辑。

把上下文对入参校验的逻辑进行抽取:

@Data
public class ApplyParam {
 
    PublicSeaApplyRequest applyRequest;
 
    UserInfoDetailVo userInfoDetailVo;
 
    public ApplyParam(PublicSeaApplyRequest applyRequest,UserInfoDetailVo userInfoDetailVo){
        //所选阶段不存在 抛出异常
        if (!ObjectEnum.isExist(applyRequest.getStage())) {
            throw new BusinessRunTimeException(BusinessRuntimeExceptionEnum.PHASE_NOT_EXIST.getCode(), BusinessRuntimeExceptionEnum.PHASE_NOT_EXIST.getMessage());
        }
        List<String> ids = applyRequest.getKeys();
        if (CollectionUtils.isEmpty(ids)) {
            throw new BusinessParamCheckingException(BusinessParamCheckingExceptionEnum.PUBLIC_SEA_NOT_EXIST.getCode(), BusinessParamCheckingExceptionEnum.PUBLIC_SEA_NOT_EXIST.getMessage());
        }
        this.applyRequest =applyRequest;
        this.userInfoDetailVo = userInfoDetailVo;
 
    }
}

对公海校验逻辑进行抽取:

@Data
public class BatchCheckApplyLimit {
 
    List<String> keys;
    String applyLimitMsg;
    HashMap<String, String> map;
 
    public BatchCheckApplyLimit(List<String> ids, UserInfoDetailVo userInfoDetailVo, List<ClueEntity> list, List<TScrmPublicseaDropRuleDTO> publicSeaDropRule, ObjectStatusDTO dto){
        HashMap<String, String> mapresult = new HashMap<>();
        if(CollectionUtils.isEmpty(list)){
             this.map = mapresult;
        }else {
            List<ApplyLimitCheckBo> applyLimitCheckBos = new ArrayList<>();
            for (ClueEntity clue : list) {
                ApplyLimitCheckBo applyLimitCheckBo = new ApplyLimitCheckBo();
                applyLimitCheckBo.setObjectKey(clue.getClueKey());
                applyLimitCheckBo.setFormerOwner(clue.getFormerOwner());
                applyLimitCheckBo.setOwner(userInfoDetailVo.getUserWid());
                applyLimitCheckBo.setPoolTime(clue.getPoolingTime());
                applyLimitCheckBo.setPublicseaId(clue.getPublicSeaId());
                applyLimitCheckBos.add(applyLimitCheckBo);
            }
 
            Map<Long, TScrmPublicseaDropRuleDTO> dropRuleMap = publicSeaDropRule.stream().collect(Collectors.toMap(TScrmPublicseaDropRuleDTO::getId, TScrmPublicseaDropRule -> TScrmPublicseaDropRule, (k1, k2) -> k2));
            String anybodyApplyDaysToast = "同一"+dto.getTranslateName()+"%d天内不能被任何人领取";
            String formerOwnerApplyDaysToast = "同一"+dto.getTranslateName()+"%d天内不能被前所属人领取";
            applyLimitCheckBos.stream().forEach(bo -> {
                TScrmPublicseaDropRuleDTO scrmPublicseaDropRule = dropRuleMap.get(bo.getPublicseaId());
                Integer anybodyApplyDays = scrmPublicseaDropRule.getAnybodyApplyDays();
                if(anybodyApplyDays !=0){
                    if(DateUtil.compare(DateUtil.offsetDay(bo.getPoolTime(),anybodyApplyDays),new Date()) > 0){
                        map.put(bo.getObjectKey(),String.format(anybodyApplyDaysToast,anybodyApplyDays));
                        return;
                    }
                }
                Integer formerOwnerApplyDays = scrmPublicseaDropRule.getFormerOwnerApplyDays();
                if(formerOwnerApplyDays !=0){
                    if(DateUtil.compare(DateUtil.offsetDay(bo.getPoolTime(),formerOwnerApplyDays),new Date()) > 0 && Objects.equals(bo.getOwner(),bo.getFormerOwner())){
                        map.put(bo.getObjectKey(),String.format(formerOwnerApplyDaysToast,formerOwnerApplyDays));
                        return;
                    }
                }
            });
            this.map = mapresult;
            this.applyLimitMsg = map.values().iterator().next();
            ids.removeAll(map.keySet());
            this.keys = ids;
 
        }
    }
}

如果有新的校验逻辑,那么只需要新增一个构造函数,不需要修改老逻辑,降低测试复杂度。

和传统代码里的校验方式对比

在传统Java架构里有几个办法能够去解决一部分问题,常见的如BeanValidation注解或ValidationUtils类,但这几个传统的方法同样有问题:

BeanValidation

通常只能解决简单的校验逻辑,复杂的校验逻辑一样要写代码实现定制校验器

在添加了新校验逻辑时,同样会出现在某些地方忘记添加一个注解的情况,DRY原则还是会被违背

Contacts createWithBeanValidation(

  @NotNull @NotBlank String name,

  @NotNull @Pattern(regexp = "^0?[1-9]{2,3}-?\\d{8}$") String phone,

  @NotNull String address

);

当大量的校验逻辑集中在一个类里之后,违背了单一性原则,最后也会导致代码混乱和不可维护ValidationUtils类:

业务异常和校验异常还是会混杂。

**抽取静态工具类 xxxUtils 类:**项目里充斥着大量的静态工具类,业务代码散在多个文件当中时,很难找到或者梳理核心的业务逻辑。

2、抽象数据库存储层

目前大量代码直接调用的mapper,对数据库强依赖。抽象出一个数据存储层来降低依赖。不关心数据存储层具体使用什么数据库。

图片

这里会引入几个模型概念:

Entity、Data Object (DO)和Data Transfer Object (DTO):

DO:DO的字段类型和名称应该和数据库物理表格的字段类型和名称一一对应;

Entity:实体对象映射应用中的业务模型,它的字段和方法应该和业务语言保持一致,和持久化无关。一般来说 Entity 中的字段应该比 DO 里面要多,或者有DO 的嵌套关系(聚合根);Entity 不需要序列化和持久化,仅仅存在内存中。

DTO(传输对象):主要作为Application层的入参和出参,比如CQRS里的Command、Query、Event,以及Request、Response等都属于DTO的范畴。DTO的价值在于适配不同的业务场景的入参和出参。

关于复杂的实体Entity,举个例子:

图片

Entity、DO和DTO 的相互转换:

图片

通过抽象出一个Assembler/Converter对象,我们能把复杂的转化逻辑都收敛到一个对象中。对象映射关系变化时,只需要改这个对象就可以了。

注意:

抽象的repository接口 操作的 只能是 实体对象。

从使用复杂度角度来看,区分了DO、Entity、DTO带来了代码量的膨胀。但是在实际复杂业务场景下,通过功能来区分模型带来的价值是功能性的单一和可测试、可预期,最终反而是逻辑复杂性的降低(所以简单的业务并不适合DDD)。

3、防腐层(ACL)

这个案例是以线索为例,和线索不相关的服务都可以称为三方服务。在当前系统需要依赖其他系统的时候,可能会依赖对方的数据结构,API,如果对方接口或者数据结构发生变更,会导致当前系统被“腐蚀”,这个时候我们需要加一层防腐层来隔离外部依赖,这种模式叫ACL(Anti-Corruption Layer).

图片

ACL 不仅仅是做了一层封装,还能提供一些其他功能:

  • 适配器:在外部返回数据不符合内部规范时,可以通过适配器模式将数据进行转化。

  • 缓存:对外部系统调用频繁且数据变更不频繁的数据(比如系统字段)

  • 兜底:外部系统不稳定时返回兜底值,提高系统的稳定性。

  • 易于开发测试:在和外部系统进行联调开发时,外部系统没有准备好的情况下可以进行mock 实现。

  • 开关:某些场景定制化可以通过开关在ACL 实现。

4、封装业务逻辑

用Entity 封装对象的有状态的行为

@ApiModelProperty(value = "资源流入ID")
private String sourceId;
//认领
public void apply(UserInfoDetailVo userInfoDetailVo){
    Date now = new Date();
    this.owner = userInfoDetailVo.getUserWid();
    this.claimTime = now;
    this.lastFollowTime = now;
    this.lastFollowUserWid = userInfoDetailVo.getUserWid();
    this.lastUpdateUserWid = userInfoDetailVo.getUserWid();
 
    this.followStatus = CluefollowStatusEnum.FOLLOW_STATUS_NUCONTACT.getCode();
}
 
//放弃
public void giveup(){
    //to do
}
 
//变更所属人
 
public void change_owner(){
    // to do
}

通过domain service 封装多对象的行为逻辑

public interface ClueService {
    List<ClueEntity> apply(UserInfoDetailVo userInfoDetailVo, List<String> ids, List<ClueEntity> clueEntities);
}
@Service
public class ClueServiceImpl implements ClueService {
    @Override
    public List<ClueEntity> apply(UserInfoDetailVo userInfoDetailVo, List<String> ids, List<ClueEntity> clueEntities) {
        List<ClueEntity> result = new ArrayList<>();
        clueEntities.forEach(e->{
            if (ids.contains(e.getClueKey())){
                e.apply(userInfoDetailVo);
                result.add(e);
            }
        });
        return result;
    }
}

5、重构分析

修改后的代码

@ApiOperation("公海申领")
@PostMapping("/publicsea/apply")
@Validated
public CommonResult apply(@RequestBody BasePcRequest<PublicSeaApplyRequest> request) {
    // 可根据stage 根据策略
    CommonResult CommonResult = new CommonResult();
    ApplyParamDP applyParam =  new ApplyParamDP(request.getData(),request.getUserInfoDetailVo());
    ApplyResponse applyResponse = applyService.apply(applyParam);
    CommonResult.setErrcode(ResultCode.SUCCESS.getCode());
    CommonResult.setErrmsg(applyResponse.getMessage());
    CommonResult.setData(applyResponse);
    return CommonResult;
}
@Override
public ApplyResponse apply(ApplyParamDP param) {
    ApplyResponse applyResponse = new ApplyResponse();
    //权限校验
    checkAuth(param);
 
    //数据获取
    List<ClueEntity> clueEntities = clueRepository.queryPublicSeaAuth(new CluePidPublicParam(param.getUserInfoDetailVo().getPid(), param.getApplyRequest().getKeys()));
    Map<Long, Boolean> seaIdCheck = publicSeaService.checkUserExistPublicSea(new CheckUserExistPublicSeaParam(param.getUserInfoDetailVo().getPid(), param.getUserInfoDetailVo().getUserWid(), param.getApplyRequest().getStage(), clueEntities));
    CheckPublicApplyBo checkPublicApplyBo = new CheckPublicApplyBo(param.getApplyRequest().getKeys(),clueEntities,seaIdCheck);
 
    //保有量数据校验
    ApplyResponse applyRes= chekResourceHold(param, applyResponse, checkPublicApplyBo);
    if (applyRes != null) return applyRes;
 
    //公海规则校验
    BatchCheckApplyLimitDP limit = getBatchCheckApplyLimit(param, clueEntities, seaIdCheck, checkPublicApplyBo);
    ApplyResponse responseCheckApplyList = applyResponse.checkApplyList(checkPublicApplyBo.getKeys(), checkPublicApplyBo.getTotalCount(),limit.getApplyLimitMsg());
    if (responseCheckApplyList!=null) { return responseCheckApplyList;}
 
    //执行认领动作
    List<ClueEntity> applyentitys = clueService.apply(param.getUserInfoDetailVo(), checkPublicApplyBo.getKeys(), clueEntities);
    clueRepository.batchSaveUpdate(applyentitys);
    clueRepository.batchSaveUpdateES(applyentitys);
 
    // 异步计算掉保时间(可发消息)
    publicSeaService.updateDeadLineDate();
 
    //构建写跟进日志(可发消息)
    StateChangeLogBo applylog = new ClueApplyStateChangeLogBo(param.getUserInfoDetailVo(),param.getApplyRequest().getStage(),new Date());
    stateChangeLogService.savelog(applylog);
    return applyResponse.buildSuccessResponse(checkPublicApplyBo.getKeys(), checkPublicApplyBo.getTotalCount(),limit.getApplyLimitMsg());
}

代码从200多行 简化到30多行

流程图

图片

重新编排后的分层

图片

通过对外部依赖的抽象和内部逻辑的封装重构,应用整体的依赖关系变了:

1、最底层不再是数据库,而是Entity、DP 和Domain Service。这些对象不依赖任何外部服务和框架,而是纯内存中的数据和操作。这些对象我们打包在  领域层。领域层没有任何外部依赖关系。

2、然后是负责组件编排的Application Service,但是这些服务仅仅依赖了一些抽象出来的ACL类和Repository类,而其具体实现类是通过依赖注入注进来的。Application Service、Repository、ACL等我们统称为 应用层。应用层 依赖 领域层,但不依赖具体实现。

3、最后是ACL,Repository等的具体实现,这些实现通常依赖外部具体的技术实现和框架,所以统称为 基础设施层 。Web框架里的对象如Controller之类的在目前这种实现方式下也属于基础设施层。

现在我们很容易看出认领的逻辑流程和外部依赖。如果现在来实现这个业务需求,我们可以:

  1. 先写Domain层的业务逻辑;

  2. 然后对外部依赖抽象接口进行编写;

  3. 再写Application层的组件编排;

  4. 最后才写每个外部依赖的具体实现;

像填空题一样就把代码写完了,**这种架构思路和代码组织结构就叫做Domain-Driven Design(领域驱动设计)。所以DDD不是一个特殊的架构设计,而是一种架构思想,**在代码落地方面,是一种代码组织方式。

总的来说DDD就是从以数据库为中心过度到以领域模型为中心,将侧重点从效率改变为维护。从长远的角度看,以领域模型为中心的设计更加清晰,也是一种更忠实于领域抽象的实现,因而可维护性更高。

三、代码组织结构

Java中我们可以通过POM Module和POM依赖来处理相互的关系,避免下层代码依赖到上层实现的情况。

图片

types 模块

可以保存对外暴露的DP,因为DP 是无状态的逻辑,可以对外暴露,可以包含在对外的API 接口中,不依赖任何类库,可以单独打包。纯 POJO。

图片

Domain 模块

Domain 模块是核心业务逻辑的集中地,包含有状态的Entity、领域服务Domain Service、以及各种外部依赖的接口类:如Repository、ACL、中间件等。Domain模块仅依赖Types模块。纯 POJO。

图片

Application模块

Application模块依赖Domain模块,对业务逻辑进行编排。纯POJO。

图片Infrastructure模块

Infrastructure模块依赖Domain模块,Infrastructure模块包含了Persistence、ES、repositoryimpl等模块。其中持久化模块要依赖具体的ORM类库,比如MyBatis。依赖其他的服务的具体实现,converter中包括Entity到DO的转化类。

图片

接口模块

Web模块包含Controller等相关代码和启动类,interface 模块包括dubbo 接口相关代码。

可测试性的提高

1、Types,Domain模块都属于无外部依赖的纯POJO,基本上都可以100%的被单元测试覆盖。

2、Application模块的代码依赖外部抽象类,如果通过测试框架去Mock所有外部依赖,但仍然可以100%被单元测试。

3、Infrastructure的每个模块的代码相对独立,接口数量比较少,相对比较容易写单测。而且模块的变动不会很频繁,属于一劳永逸。

4、Web模块有两种测试方法:通过Spring的MockMVC测试,或者通过HttpClient调用接口测试。但是在测试时最好把Controller依赖的服务类都Mock掉。如果把Controller的逻辑都后置到Application Service中时,Controller的逻辑变得极为简单,很容易100%覆盖。

代码的变化速度

传统的的架构中,代码的从上到下的演进速度是一样的,改个需求可能从接口到业务逻辑到数据持久层都要修改。那么DDD的代码变化速度是不一样的。

Domain层属于核心业务逻辑,属于经常被修改的地方。

Application层属于业务用例的编排。业务用例一般都是描述比较大方向的需求,接口相对稳定,特别是对外的接口一般不会频繁变更。

Infrastructure层属于最低频变更的。

所以在DDD架构中,能明显看出越外层的代码越稳定,越内层的代码演进越快,真正体现了领域“驱动”的核心思想。

总结

通过DP 收集和封装无状态的计算逻辑,通过实体来梳理业务对象所具备的能力和动作,通过防腐层抽象外部依赖。DDD的架构能带来以下收益

  • 代码结构清晰

  • 高可维护性

  • 高可扩展性

  • 高可测试性

DDD不是一个特殊的架构设计,而是一种架构思想,是一种代码组织方式。

在效率和可维护性方面做平衡,在目前这种代码迁移的过程中,可以通过减少entity 的聚合 和原来逻辑保持一致性的方式 来平衡 效率和可维护性!

猜你喜欢

转载自juejin.im/post/7112026592573915172