花了几个月搞了套能力编排框架,再也不用深夜加班吃泡面了

猿天地

共 78141字,需浏览 157分钟

 · 2021-04-09

你能学到什么

  • 工厂、策略,责任链等多种设计模式的实际运用
  • 项目中多个核心流程在开发层面统一风格语言
  • 了解能力编排框架带来的能力复用和业务隔离
  • 颠覆传统开发习惯方法固定入参带来的方法扩展局限

前言

当我们做一个新项目开始的时候,我们总会想项目成型之后希望他能够有条不紊,每一个模块的代码风格统一,注释详尽,条理清晰。这样方便不同的人去看逻辑实现时不会因为开发者的习惯导致理解上的歧义。但是在组内多人协作开发时,尽管为了这一目标我们会约定种种开发规范,但是随着项目的推进,代码量的增加,渐渐地就变的不可控起来,因为在没有被强制的约束下每个人的习惯是很难被改变的。尽管我们会做一些代码评审来发现这些不符合事先的约定,但是又会因为种种客观原因,比如:工期,已经提测了,快要上线了,逻辑太过复杂了,不敢再去改,担心会该出问题等等,导致只能就这样。

对于一个项目来说,我们不可能100%来保证所有功能模块,每一处细节都能够统一风格,但是好在符合28原则,一个项目中的核心流程只占20%,在不同人开发时控制好这部分的绝对风格就能够保证80%下不出问题,以及新同学熟悉项目时能够快速掌握整个项目80%的价值。所以开发定义一套对核心流程强制约束的开发框架是有必要,有意义的。

当我们编写去实现一段复杂的流程逻辑时,我们会遇到哪些问题?

  • 因为交互太多,链路太长导致流程主体逻辑不清晰
  • 我们对逻辑进行拆分,提取出多个方法拼装简化入口主函数,这样多半都是定制方法,复用性很低
  • 我们对业务场景进行分类归纳,通过工厂+模版来优化代码结构,把差异放在子类处理,做到了一定的业务隔离,但是如果修改模版有可能牵一发动全身。
  • 大量的方法聚集在service里,难免还会有一些相似的,甚至重复的代码(可能只是一个字段之差),导致想使用方法的人难以快速找到,就会出现不如自己重新写一个,恶性循环。
  • 一个方法可能干多件事,但是有些业务可能只需要这个方法的部分逻辑,重够原方法吧担心出问题,复制出来改一改把重复代码又让人很忧伤

业务背景介绍:

假如我们的系统现在接入了一个业务:应付主体公司给供应商创建对账单的需求,最终创建对账单前需要经过一些列的检查,校验,判断,加锁等等处理,下面是一个简易流程图,也看得出来这个业务的流程也是比较长的。

我们可以回顾一下一般情况我们会怎么写?

常规写法:

/**
 * @author zhanghang
 * @version V1.0
 * @ClassName
 * @Description
 * @date 2021/3/22 15:28
 */
public class AccountStatementService {

    public Boolean createAccountStatement(AccountStatementPo po) {
        // 入参必填字段校验
        // 登录用户校验
        // 应付主体及供应商有效性校验
        // 获取应付主体对账单预览
        // 检查对账单账扣明细类型的合法性
        // 检查对账单类型和对账单商品明细类型的一致性
        // 校验对账单预览明细里的币种、税率的一致性
        // 校验对账单预览明细里的合法性
        // 对要创建对账单的明细数据进行加锁
        // 校验所有明细状态的有效性
        // 校验账扣明细的剩余应收金额是否足够使用
        // 更新商品明细状态
        // 冻结账扣明细的冻结金额和扣减剩余应收金额
        // 更新对账单预览状态为无效
        // 创建对账单以及对账单明细
        // 保存对账单操作流水以及明细操作流水
    }
}

tips: 这种就是最常规的写法,这是一种面向过程的编程,但我们写java要保持面向对象的思想,因此你说这种方式好吗?肯定不好,好一点的同学可能还会把每一步流程抽象成一个个方法然后在主方法入口处拼接,这样视觉看起来比较清晰,差一点的可能直接就在主方法里撸起袖子就干了,这样就会又臭又长,让不熟悉的人看起来十分着急。如果业务再出现点变化,那么各种if/else频繁出现,就保证不了原有流程不出bug了。

通过模版模式+工厂模式重构一把:

/**
 * @author zhanghang
 * @version V1.0
 * @ClassName
 * @Description
 * @date 2021/3/22 15:42
 */
@Slf4j
public abstract class AbstractCreateAccountStatementService {

    @Autowired
    private RedissonClient redissonClient;

    /**
     * 获取对账单类型
     * @return
     */
    protected abstract AccountStatementEnum getAccountStatementType();
    
    /**
     * 获取联锁Key
     * @param po
     * @return
     */
    protected abstract List<RLock> getKeyList(AccountStatementPo po);

    /**
     * 前置校验
     * @param po
     * @return
     */
    protected abstract Boolean doPreCheck(AccountStatementPo po);

    /**
     * 后置校验,加锁后校验
     * @param po
     * @return
     */
    protected abstract Boolean doCheckWithLock(AccountStatementPo po);

    /**
     * 创建对账单
     * @param po
     * @return
     */
    protected abstract Boolean doCreateAccountStatement(AccountStatementPo po);

    /**
     * 初始化创建对账单工厂
     */
    @PostConstruct
    protected void init() {
        AccountStatementFactory.add(getAccountStatementType(), this);
    }

    /**
     * 创建对账单
     * @param po
     * @return
     */
    public Boolean createAccountStatement(AccountStatementPo po) {
        Boolean checkResult = doPreCheck(po);
        if (!checkResult) {
            log.error("创建对账单前置校验失败!");
            return Boolean.FALSE;
        }
        try {
            // 获取联锁key
            List<RLock> lockList = getKeyList(po);
            // 获取联锁
            RedissonMultiLock lock = new RedissonMultiLock(lockList.toArray(new RLock[lockList.size()]));
            if(!lock.tryLock(po.getWaitTime(), po.getLeaseTime(), po.getUnit()) {
                log.error("获取redisson分布式联锁能力超时");
                return Boolean.FALSE;
            }
            checkResult = doCheckWithLock(po);
            if (!checkResult) {
                log.error("创建对账单后置校验失败!");
                return Boolean.FALSE;
            }
            return doCreateAccountStatement(po);
        } catch (Exception e) {
            log.error("获取redisson分布式联锁能力失败:{}", e.getMessage());
            log.error(e.getMessage(), e);
            return Boolean.FALSE;
        }
    }

}

/**
 * @author zhanghang
 * @version V1.0
 * @ClassName
 * @Description 创建X类型对账单实现类
 * @date 2021/3/22 15:28
 */
public class CreateXAccountStatementService extends AbstractCreateAccountStatementService{


    @Override
    protected AccountStatementEnum getAccountStatementType() {
        return AccountStatementEnum.X;
    }

    @Override
    protected Boolean doPreCheck(AccountStatementPo po) {
        // 入参必填字段校验
        // 登录用户校验
        // 应付主体及供应商有效性校验
        // 获取应付主体对账单预览
        // 检查对账单账扣明细类型的合法性
        // 检查对账单类型和对账单商品明细类型的一致性
        // 校验对账单预览明细里的币种、税率的一致性
        // 校验对账单预览明细里的合法性
        return null;
    }
    
    @Override
    protected abstract List<RLock> getKeyList(AccountStatementPo po){
        return null;
    }

    @Override
    protected Boolean doCheckWithLock(AccountStatementPo po) {
        // 校验所有明细状态的有效性
        // 校验账扣明细的剩余应收金额是否足够使用
        return null;
    }

    @Override
    protected Boolean doCreateAccountStatement(AccountStatementPo po) {
        // 更新商品明细状态
        // 冻结账扣明细的冻结金额和扣减剩余应收金额
        // 更新对账单预览状态为无效
        // 创建对账单以及对账单明细
        // 保存对账单操作流水以及明细操作流水
        return null;
    }

}

tips:可以看得出来通过工厂+模版重构后的代码结构清晰了很多,从创建对账单主函数来看,其实就是经过4个步骤:

  • 前置校验
  • 加锁处理
  • 后置校验
  • 创建对账单

经过这样拆分后,每一步要做那些事去对应的子类里面看相对来说就会清晰很多。如果前置校验在某个子类里校验的比较多,还可以做成责任链模式来优化代码,这里就不详述了。

除此之外,通过子类实现的方法也把差异化处理放到子类来做,这样就不会再主流程里出现各种if/else,做到了一定成的业务隔离,在一定程度上降低了修改主流程if/else导致原来其他流程出现bug。

如果有一些公共的校验,更新,保存等方法还是可以上浮到抽象父类,保证子类的复用能力。

总的来说,通过设计模式重构代码逻辑看起来更加清晰,也保证了一定程度的隔离性和复用性,也体现了面向对象的思想。但是还是存在一些隐患的:

  • 比如说模版方法发生改变,必然会影响到响应的子类,抽象父类的公用方法发生调整,因为父类的方法去复用是有一些前提的,因此也会影响到子类,这时候业务的隔离性也难以保证。

  • 相对于其他的一些业务来说假如想使用创建对账单这边类的部分方法或部分逻辑时,会受制于访问修饰符或者方法带来的限制从而影响了复用性。

  • 写模版,写方法,写抽象也受制于开发本身的功力,抽象能力参差不齐。没法保证创建对账单的风格和创建别的单据或者操作别的单据的流程风格做到统一。

微信搜索 猿天地 后台回复 学习资料 领取学习视频


通过代码流程编排的约束行框架来继续重构:

/**
 * @author zhanghang
 * @version V1.0
 * @ClassName
 * @Description
 * @date 2021/1/18 9:07
 */
@Component
public class CreateDefaultAccountStatementCommand extends AbstractCommand<CreateDefaultAccountStatementContext, AccountStatementBillDto> {

    @Autowired
    private QuerySysConfigAbility querySysConfigAbility;

    @Autowired
    private QueryCustomerBaseAbility queryCustomerBaseAbility;

    @Autowired
    private QueryUserNeedsDraftAbility queryUserNeedsDraftAbility;

    @Autowired
    private QueryBaseNeedsAbility queryBaseNeedsAbility;

    @Autowired
    private CheckGoodsNeedsBizSecondTagAllAccordanceAbility checkGoodsNeedsBizSecondTagAllAccordanceAbility;

    @Autowired
    private CheckAccountDeductNeedsAccountedTypeAllAccordanceAbility checkAccountDeductNeedsAccountedTypeAllAccordanceAbility;

    @Autowired
    private RedissonMultiLockAbility redissonMultiLockAbility;

    @Autowired
    private CheckBaseNeedsStatusAllAccordanceAbility checkBaseNeedsStatusAllAccordanceAbility;

    @Autowired
    private CheckAccountDeductNeedsStatusCanCheckingAbility checkAccountDeductNeedsStatusCanCheckingAbility;

    @Autowired
    private CheckAccountDeductNeedsRemainReceivableMoneyEnoughAbility checkAccountDeductNeedsRemainReceivableMoneyEnoughAbility;

    @Autowired
    private UpdateBaseNeedsStatusAbility updateBaseNeedsStatusAbility;

    @Autowired
    private QueryAccountDeductNeedsAbility queryAccountDeductNeedsAbility;

    @Autowired
    private FrozenAccountDeductRemainReceivableMoneyAbility frozenAccountDeductRemainReceivableMoneyAbility;

    @Autowired
    private UpdateUserNeedsDraftStatusAbility updateUserNeedsDraftStatusAbility;

    @Autowired
    private SaveAccountStatementBillAbility saveAccountStatementBillAbility;

    @Autowired
    private JobCreateAbility jobCreateAbility;

    @Override
    protected String getCommandFailLogPre() {
        return "创建默认对账单失败";
    }

    @Override
    public List<AbstractAbility> initAbilityList() {
        List<AbstractAbility> list = new ArrayList<>();
        // 检查应付主体有效性能力
        list.add(querySysConfigAbility);
        // 检查供应商有效性能力
        list.add(queryCustomerBaseAbility);
        // 获取用户创建预览应付需求草稿能力
        list.add(queryUserNeedsDraftAbility);
        // 获取用户创建预览应付草稿的应付需求基础信息,校验币种及税率
        list.add(queryBaseNeedsAbility);
        // 检查对账单类型与商品应付需求业务二级标签是否一致
        list.add(checkGoodsNeedsBizSecondTagAllAccordanceAbility);
        // 校验账扣应付需求的到账方式是否都是抵货款
        list.add(checkAccountDeductNeedsAccountedTypeAllAccordanceAbility);
        // 获取分布式锁加锁能力
        list.add(redissonMultiLockAbility);
        // 校验商品应付需求的状态是否都是需求状态能力
        list.add(checkBaseNeedsStatusAllAccordanceAbility);
        // 校验账扣应付需求的状态是否可以生成对账单
        list.add(checkAccountDeductNeedsStatusCanCheckingAbility);
        // 校验账扣应付需求的剩余应付金额是否足够使用能力
        list.add(checkAccountDeductNeedsRemainReceivableMoneyEnoughAbility);
        // 更新商品应付需求状态能力
        list.add(updateBaseNeedsStatusAbility);
        // 查询账扣应付需求明细能力
        list.add(queryAccountDeductNeedsAbility);
        // 抵扣账扣应付需求金额能力
        list.add(frozenAccountDeductRemainReceivableMoneyAbility);
        // 更新用户创建预览应付需求草稿为无效能力
        list.add(updateUserNeedsDraftStatusAbility);
        // 创建对账单能力
        list.add(saveAccountStatementBillAbility);
        // 异步保存应付需求操作流水
        list.add(jobCreateAbility);
        return list;
    }
}
/**
 * @author zhanghang
 * @version V1.0
 * @ClassName
 * @Description 抽象命令类
 * @date 2021/1/17 17:37
 */
@Slf4j
public abstract class AbstractCommand<T extends BaseContext<R>, R> {

    /**
     * 获取命令执行失败日志前缀
     * @return
     */
    protected abstract String getCommandFailLogPre();

    /**
     * 初始化构造命令执行能力集合
     * @return
     */
    protected abstract List<AbstractAbility> initAbilityList();

    /**
     * 执行组件逻辑
     * @author zhanghang
     * @date 2021/1/17 17:37
     * @param context
     * @return
     */
    public R execute(T context) {
        try {
            return (R) FinanceSpringBeanUtil.getBean(this.getClass()).doExecute(context);
        } catch (BizException e) {
            String errorMsg = this.getCommandFailLogPre() + ":" + e.getStatusText();
            log.error(errorMsg);
            log.error("debugData:{}", e.getDebugData());
            log.error(e.getStatusText(), e);
            throw e;
        } catch (Exception e) {
            log.error(this.getCommandFailLogPre() + ":{}", e.getMessage());
            log.error(e.getMessage(), e);
            throw new RuntimeException(this.getCommandFailLogPre());
        } finally {
            context.doFinally();
        }
    }

    @Transactional(rollbackFor = Exception.class)
    public R doExecute(T context) {
        for (AbstractAbility ability : this.initAbilityList()) {
            ability.doHandle(context);
        }
        return context.getResult();
    }

}

/**
 * @author zhanghang
 * @version V1.0
 * @ClassName
 * @Description
 * @date 2021/1/17 17:32
 */
@Data
@Builder
@Slf4j
public class CreateDefaultAccountStatementContext extends BaseContext<AccountStatementBillDto>
        implements QuerySysConfigRequestParam, QueryCustomerBaseRequestParam,
        QueryUserNeedsDraftRequestParam,
        QueryBaseNeedsRequestParam, CheckGoodsNeedsBizSecondTagAllAccordanceRequestParam,
        CheckAccountDeductNeedsAccountedTypeAllAccordanceRequestParam, RedissonMultiLockRequestParam,
        CheckBaseNeedsStatusAllAccordanceRequestParam, CheckAccountDeductNeedsStatusCanCheckingRequestParam,
        CheckAccountDeductNeedsRemainReceivableMoneyEnoughRequestParam,
        UpdateBaseNeedsStatusRequestParam, FrozenAccountDeductRemainReceivableMoneyRequestParam,
        QueryAccountDeductNeedsRequestParam, UpdateUserNeedsDraftStatusRequestParam,
        SaveAccountStatementBillRequestParam, JobCreateAbilityRequestParam
         {

    /**
     * 锁实例
     */
    private RedissonMultiLock lock;

    /**
     * 客户ID
     */
    private Long customerId;

    /**
     * 应付主体ID
     */
    private Long subjectId;

    /**
     * 对账单类型
     */
    private Integer statementType;

    /**
     * 康众备注
     */
    private String subjectRemark;

    /**
     * 供应商(寄销和非寄销)对账单金额字段计算器管理员
     */
    private SupplierMoneyCalculaterManager supplierMoneyCalculaterManager;

    /**
     * 应付主体信息
     */
    private SystemConfigDo subjectConfigInfo;

    /**
     * 供应商基础信息
     */
    private CustomerBaseDto customerBaseDto;

    /**
     * 草稿类型
     */
    private DraftTypeEnum draftTypeEnum;

    /**
     * 创建预览基础应付需求ID:本次对账金额map
     */
    private Map<Long, BigDecimal> needsBaseId2StatementMoneyMap;

    /**
     * 用户应付需求创建预览集合
     */
    private List<UserNeedsDraftDo> needsCreatePreviewDraftList;

    /**
     * 创建预览基础应付需求基础数据需求单号集合
     */
    private List<String> baseNeedsNeedsNoList;

    /**
     * 创建预览商品基础应付需求集合
     */
    private List<NeedsBaseDo> goodsNeedsBaseList;

    /**
     * 创建预览账扣基础应付需求集合
     */
    private List<NeedsBaseDo> accountDeductNeedsBaseList;

    /**
     * 创建预览账扣基础应付需求集合
     */
    private List<NeedsAccountDeductDo> accountDeductNeedsInfoList;

    /**
     * 要保存的对账单的对象
     */
    private AccountStatementBillPo accountStatementBillPo;

  

    @Override
    public void doFinally() {
        if (lock != null) {
            lock.unlock();
        }
    }

    /**
     * 业务编码
     * @return bizCode
     */
    @Override
    public  String getBizCode() {
        return null;
    }

    /**
     * 单据编码
     * @return billCode
     */
    @Override
    public  String getBillCode() {
        return null;
    }

    /**
     * 类型
     * @return sceneType
     */
    @Override
    public String getSceneType() {
        return null;
    }

    /**
     * 检查应付主体有效性能力入参
     * 根据应付主体ID查询
     * @return
     */
    @Override
    public QuerySysConfigPo getQuerySysConfigRequestParam() {
       
    }

    /**
     * 检查应付主体有效性能力入参
     * 根据应付主体ID查询
     * @return
     */
    @Override
    public void setQuerySysConfigResponseParam(List<SystemConfigDo> responseParam){
        
    }

    /**
     * 查询供应商基本信息能力入参
     * @return
     */
    @Override
    public CustomerBaseQueryPo getCustomerBaseQueryParam() {
        
    }

    /**
     * 查询供应商基本信息能力反参
     * @return
     */
    @Override
    public void setCustomerBaseQueryResponseParam(List<CustomerBaseDto> responseParam) {
        
    }

    /**
     * 获取用户创建预览应付需求能力入参
     * 根据草稿类型+主体ID+客户ID+登陆人工号查询应付需求草稿
     * @return
     */
    @Override
    public QueryUserNeedsDraftPo getQueryUserNeedsDraftRequestParam() {
        
    }

    /**
     * 获取用户创建预览应付需求能力返参
     * 返回的应付需求草稿提取应付需求ID:本次对账金额回填到needsBaseId2StatementMoneyMap(应付需求中:本次对账金额map)
     * @return
     */
    @Override
    public void setQueryUserNeedsDraftResponseParam(List<UserNeedsDraftDo> responseParam) {
       
    }

    /**
     * 查询应付需求基础信息能力入参
     * 根据应付需求ID查询
     * @return
     */
    @Override
    public QueryBaseNeedsPo getBaseNeedsRequestParam() {
        
    }

    /**
     * 查询应付需求基础信息能力返参
     * 回填加联锁用的needsBaseNeedsNoList(应付需求编号集合)
     * 回填goodsNeedsBaseList(商品应付需求基础数据集合)
     * 回填accountDeductNeedsBaseList(账扣应付需求基础数据集合)
     * @param responseParam
     */
    @Override
    public void setQueryBaseNeedsResponseParam(List<NeedsBaseDo> responseParam) {
        
    }

    /**
     * 检查要创建对账单的商品应付需求的业务二级tag是否和对账单类型一致入参
     * @return
     */
    @Override
    public CheckGoodsNeedsBizSecondTagAllAccordancePo getCheckGoodsNeedsBizSecondTagAllAccordanceRequestParam() {
        
    }

    /**
     * 检查要创建对账单的商品应付需求的业务二级tag是否和对账单类型一致返参
     * @param responseParam
     */
    @Override
    public void setCheckGoodsNeedsBizSecondTagAllAccordanceResponseParam(Boolean responseParam){
        
    }

    /**
     * 获取redisson分布式联锁能力入参
     * @return
     */
    @Override
    public RedissonMultiLockPo getRedissonMultiLockRequestParam() {
        
    }

    /**
     * 获取redisson分布式联锁能力返参
     * @param responseParam
     */
    @Override
    public void setRedissonMultiLockResponseParam(RedissonMultiLock responseParam) {
        
    }

    @Override
    public Boolean jumpCheckAccountDeductNeedsAccountedTypeAllAccordanceAbility() {
        
    }

    /**
     * 检查要创建对账单的账扣应付需求的到账方式是否都是抵货款能力
     * @return
     */
    @Override
    public CheckAccountDeductNeedsAccountedTypeAllAccordancePo getCheckAccountDeductNeedsAccountedTypeAllAccordanceRequestParam() {
        
    }

    /**
     * 检查要创建对账单的商品应付需求的业务二级tag是否和对账单类型一致返参
     * @param responseParam
     */
    @Override
    public void setCheckAccountDeductNeedsAccountedTypeAllAccordanceResponseParam(Boolean responseParam){
        
    }

    /**
     * 检查要创建对账单的商品应付需求状态是否都是需求状态能力入参
     * @return
     */
    @Override
    public CheckBaseNeedsStatusAllAccordancePo getCheckBaseNeedsStatusAllAccordanceRequestParam() {
        
    }

    /**
     * 检查要创建对账单的商品应付需求状态是否都是需求状态能力返参
     * @param responseParam
     */
    @Override
    public void setCheckBaseNeedsStatusAllAccordanceResponseParam(Boolean responseParam){
        
    }


    /**
     * 判断是否要检查要创建对账单的账扣应付明细状态是否可以参与对账单生成能力
     * @return
     */
    @Override
    public Boolean jumpCheckAccountDeductNeedsStatusCanCheckingAbility(){
        
    }

    /**
     * 检查要创建对账单的账扣应付明细状态是否可以参与对账单生成能力入参
     * @return
     */
    @Override
    public CheckAccountDeductNeedsStatusCanCheckingPo getCheckAccountDeductNeedsStatusCanCheckingRequestParam() {
        
    }

    /**
     * 检查要创建对账单的账扣应付明细状态是否可以参与对账单生成能力反参
     * @param responseParam
     */
    @Override
    public void setCheckAccountDeductNeedsStatusCanCheckingResponseParam(Boolean responseParam){
        
    }

    /**
     * 判断是否需要跳过检查账扣应付需求的剩余应付金额是否足够使用能力
     * @return
     */
    @Override
    public Boolean jumpCheckAccountDeductNeedsRemainReceivableMoneyEnoughAbility() {
        
    }

    /**
     * 检查账扣应付需求的剩余应付金额是否足够使用能力
     * @return
     */
    @Override
    public CheckAccountDeductNeedsRemainReceivableMoneyEnoughPo getCheckAccountDeductNeedsRemainReceivableMoneyEnoughRequestParam() {
        
    }

    /**
     * 检查账扣应付需求的剩余应付金额是否足够使用能力反参
     * @param responseParam
     */
    @Override
    public void setCheckAccountDeductNeedsRemainReceivableMoneyEnoughResponseParam(Boolean responseParam){
        
    }

    /**
     * 更新应付需求状态能力入参
     * @return
     */
    @Override
    public UpdateBaseNeedsStatusPo getUpdateBaseNeedsStatusRequestParam() {
    
    }

    @Override
    public void setUpdateBaseNeedsStatusResponseParam(Boolean responseParam){
        
    }

    /**
     * 判断是否需要跳过查询账扣应付需求能力能力
     * @return
     */
    @Override
    public Boolean jumpQueryAccountDeductNeedsAbility() {
        
    }

    /**
     * 查询账扣应付需求能力能力入参
     * @return
     */
    @Override
    public QueryAccountDeductNeedsPo getAccountDeductNeedsRequestParam() {
        
    }

    /**
     * 查询账扣应付需求能力能力反参
     * @param responseParam
     */
    @Override
    public void setQueryAccountDeductNeedsResponseParam(List<NeedsAccountDeductDo> responseParam) {
        
    }

    /**
     * 判断是否需要跳过冻结账扣应付需求的剩余应收金额能力
     * @return
     */
    @Override
    public Boolean jumpFrozenAccountDeductRemainReceivableMoneyAbility() {
        
    }

    /**
     * 冻结账扣应付需求的剩余应收金额能力入参
     * @return
     */
    @Override
    public FrozenAccountDeductRemainReceivableMoneyPo getFrozenAccountDeductRemainReceivableMoneyRequestParam() {
    
    }

    /**
     * 冻结账扣应付需求的剩余应收金额能力反参
     * @param responseParam
     */
    @Override
    public void setFrozenAccountDeductRemainReceivableMoneyResponseParam(Boolean responseParam){
       
    }

    /**
     * 修改用户应付需求草稿的状态为无效能力入参
     * @return
     */
    @Override
    public UpdateUserNeedsDraftStatusPo getUpdateUserNeedsDraftStatusRequestParam() {
        
    }

    /**
     * 修改用户应付需求草稿的状态为无效能力反参
     * @param responseParam
     */
    @Override
    public void setUpdateUserNeedsDraftStatusResponseParam(Integer responseParam){
        
    }

    /**
     * 保存对账单能力入参
     * @return 要保存的对账单对象
     */
    @Override
    public AccountStatementBillDefaultSavePo getSaveAccountStatementBillRequestParam() {
        
    }

    /**
     * 保存对账单能力返参
     * @param responseParam 已保存对账单的对账单编号
     */
    @Override
    public void setSaveAccountStatementBillResponseParam(AccountStatementBillDto responseParam) {
        
    }

    @Override
    public JobCreateAbilityPo getJobCreateAbilityRequestParam() {
        
    }

    @Override
    public void setJobCreateAbilityResult(boolean responseParam) {
        
    }
}

tips:通过工厂+模版+责任链模式约束流程的执行,每个子类将每一步操作细化到原子级别,也就是一个原子能力类只干一件事(保证复用)。

然后不同的子类(称之为Command(命令))把要做的事情在命令类的方法里实现(也就是初始化到责任链执行集合中)。因此你在命令类中只能看到一个方法,就是当前子类要执行流程所经历的所有原子能力。

这时候有同学发问了,我具体逻辑写在哪呢?如果把复杂流程拆解抽象到原子能力的话,每个不同的业务,不同的流程使用原子能力的入参是不一样的,你怎么保证原子能力的复用性?好,关键的地方就来了。我们命令类执行流程过程中(就是执行命令类配置原子能力类),实际上是有一个上下文context的概念,这个context会绑定具体每一个命令类,命令类的context里会实现所有能力的入参,反参接口,在context里去做能力入参是哪些,能力反参我们命令又该怎么处理。这样相当于我们把当前命令的所有业务逻辑操作内聚到了命令绑定的context里,做到了天然的业务隔离。为什么天然呢?因为不同流程或不同业务他会创建另一个命令类以及另一个context。这时候又有同学发问了,加入我某个业务创建单据下面有两个类型,他们流程大致都是相似的,只有某某校验不一样,那这样我在搞一个新的命令搞一个新的context岂不是太重了?当然不用,你可以在命令的context的实现能力反参方法里做if/else判断,这样就可以兼容小范围的差异。

说说缺点吧:

  • 我们把流程精细化拆分分成一个个能力,然后有命令自由组装,这样类膨胀厉害,这个没办法,为了保证复用只能如此,其实设计模式重构不也一样类膨胀吗?只不过没有这么膨胀而已。
  • 从执行命令的抽象父类看得出来,流程一开始就是开启事务了,没办法把事务的粒度做到最细,事务空着这块会造成长事务的情况。当然也有办法解决,比如对能力类做进一步封装,做成能力组,在能力组里按照命令业务自由组装需要开启事务的能力,这样再把能力组配在命令类里,这样在原本类膨胀的基础上又膨胀出来新的类,而且这种能力组都是和具体业务相关,几乎不存在复用的可能,因此并不推荐这种做法,所以带来的问题就是对接口时效要求不高的流程可以做。
  • 分布式锁和事务是一样的问题,你可以控制加锁的时机,但是没办法控制解锁的时机,因为解锁是放在流程全部执行完毕的结束之后来统一处理的。在流程执行过程中没办法知道每一个命令在走到那一步可以解锁。当然流程没法统一做,如果命令想做的话也是可以在命令测context里取单独解锁的。
  • 说实话写完一个流程挺累的,因为要写一大堆的能力和实现一大堆的接口。context虽然内聚了所有命令内部的逻辑,但是和配置的能力成正比,一般一个能力要实现2~3个接口,所以context也会很长。

框架介绍

框架设计灵感:

起源于去年架构组开始推的流程引擎编排这个由头,借鉴采购组正在使用的淘系流程引擎编排插件,想着编写一个简单上手,易用的,针对于长流程业务场景下依然能够保证清晰的业务逻辑和天然业务隔离,统一的编码风格的约束框架。

适用场景:

业务流程较长,交互较多。最好是异步接口或定时任务发起的,即使是同步接口也要是那种对接口时效性要求不高的接口

业务身份:

一个业务场景下的的全局唯一标识,能够与它平级业务进行隔离的标识信息。

例如:

  • 生成订单:前置仓过来的业务,旗舰店过来的业务,C端过来的业务等等 支付成功消费:支付宝支付成功回调,微信支付成功回调,翼支付支付回调等等
  • 生成客户应收单:康众挂账信用支付,赊呗信用支付,采付通信用支付等等

能力:

一个执行的最小单元。他就是一个类,这个类只干一件事,很单纯,不会因为业务逻辑的判断而做不同的事。能力一旦发布,本身不可能修改,最能废弃。

例如:

  • 保存客户应收单能力。
  • 查询客户应收单能力。
  • 更新客户应收单状态能力、更新客户应收单剩余应收金额能力。

扩展点:

相对于能力类的扩展,进入能力后需要根据不同业务属性做进一步的处理,一种业务属性伴随一个扩展点,然后把扩展点挂载到对应的能力上,当流程执行到当前能力时会发现当前流程需要执行扩展点,然后找到对应的扩展点执行逻辑。

例如:

保存客户应收单:

  • 采付通信用支付业务场景保存好客户应收单后还需要尝试开启账期
  • 康众挂账信用支付业务场景保存好客户应收单后还有其他后续操作

生成订单:

  • 旗舰店业务场景保存好订单后需要发MQ广播出去
  • 前置仓业务场景保存好订单后需要更新某些报表数据

支付成功消费:

  • 支付宝支付成功消息消费成功更新好支付状态后需要做一些操作
  • 微信支付成功消息消费成功更新好支付状态后需要做一些操作

tips:

扩展点其实也不一定是要依附于能力,如果同一个业务操作,因为不同业务场景(身份)导致流程里校验,构造,执行的逻辑相似度不高时,完全可以另起一个流程,然后把扩展点升级为能力。如果流程中逻辑处理大致都相同,只是最后保存数据成功后有些不同的业务操作,那么久没必要建两个流程。这里和模版模式是一样的,比方说保存数据是倒数第二不,然后在后面加一个doAfter()的抽象方法让子类去实现想要做的事。

即使能力挂了扩展点也不一定会执行,因为前面说了扩展点是和具体的业务场景(身份)绑定的,如果执行的流程中没有约定业务场景(身份)或者约定的业务身份未找到对应能力的扩展点也就仅仅只会执行能力。

看看代码

1.首先我们为能力定义一个注解保证我们所写的能力能够被spring装载。

/**
 * 能力注解,标示为能力,能力会自动被spring加载
 * @author zhanghang
 * @date 2021-01-16 20:42
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Component
public @interface Ability {
    @AliasFor(
            annotation = Component.class
    )
    String value() default "";
}

2.然后在定义一个能力接口来约定我们的能力必要哪些方法。

/**
 * @ClassName
 * @Description 能力基础接口
 * @Author zhanghang
 * @Date 2021-01-16 20:43
 */
public interface IAbility<T extends IParam, R> {

    /**
     * 能力名称
     *
     * @return
     */
    String getName();

    /**
     * 能力描述
     *
     * @return
     */
    String getDescription();

    /**
     * 执行能力
     * @param param
     * @return
     */
    R execute(T param);

}

3.接下来我们看一下能力抽象类部分是怎么处理能力和扩展点的

/**
 * @ClassName
 * @Description 抽象能力类
 * @Author zhanghang
 * @Date 2021-01-16 20:53
 */
@Slf4j
public abstract class AbstractAbility<T extends IParam, R> implements IAbility<T, R> {

    /**
     * 抽象能力实现
     * 执行规则:
     * 有些流程执行能力时候会因为业务身份的缘故不走能力本身的逻辑
     * 而是通过业务身份找到对应的扩展点,执行扩展点的逻辑。
     * 扩展点相当于重写能力执行逻辑。根据特定业务身份执行特定的业务逻辑
     * @param param
     */
    public R doHandle(T param) {
        if (param == null) {
            throw new BizException(CommonStateCode.PARAMETER_LACK, "执行能力context参数缺失", null);
        }
        IExtPoint<T, R> extPoint = getMatchExtPointList(param);
        log.info("能力:{}, 匹配到了 {} 扩展点", this.getName(), extPoint);
        if (extPoint == null) {
            return this.execute(param);
        } else {
            return extPoint.execute(param, this);
        }
    }

    /**
     * 能力扩展点匹配-交给实际能力进行实现
     * @param param 能力执行参数
     * @return 当前能力匹配点所有扩展点
     */
    private IExtPoint<T, R> getMatchExtPointList(T param) {
        return ExtPointFactory.getByProcessId(this.getClass().getSimpleName(), param);
    }

}

tips:从抽象父类的模版方法看得出来首先会根据入参去寻找是否有匹配的能力扩展点,如果匹配到则执行扩展点的逻辑。有同学可能会有疑问,我如果想要先执行能力的逻辑在执行扩展点,那你这边直接执行扩展点了能力怎么办呢?请大家注意,在执行扩展点时其实已经把能入当作入参传到扩展点里面,根据具体扩展点的执行逻辑来判断是否需要执行一遍能力。

4.下面我们来看看扩展点相关的内容,首先我们为扩展点定义一个注解保证我们所写的能力能够被spring装载。

/**
 * 能力扩展点注解
 * 扩展点必定和业务强关联,也就是说先有特定业务场景才产生了扩展点,否则能力就能满足。
 * @author zhanghang
 * @date 2021-01-17 11:56
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Component
public @interface ExtPoint {

    @AliasFor(
            annotation = Component.class
    )
    String value() default "";

    /**
     * 绑定的能力
     * @return Class
     */
    Class<?> ability();

    /**
     * 业务编码
     * @return
     */
    String bizCode() default "";

    /**
     * 单据编码
     * @return
     */
    String billCode() default "";

    /**
     * 场景类型
     * @return
     */
    String sceneType() default "";

}

tips:这边看的出来和能力注解基本一致,多了一个绑定归属能力类的属性

微信搜索 猿天地 后台回复 学习资料 领取学习视频


5.接下来为扩展点定义接口来约定扩展点的必要方法。

/**
 * @ClassName IExtPoint
 * @Description 能力扩展点
 * @Author zhanghang
 * @Date 2020-07-27 11:36
 */
public interface IExtPoint<T extends IParam, R> {
    /**
     * 能力扩展点名称
     *
     * @return
     */
    String getName();

    /**
     * 能力扩展点描述
     *
     * @return
     */
    String getDescription();

    /**
     * 扩展点执行
     * @param param
     * @param ability
     * @return
     */
    R execute(T param, AbstractAbility<T, R> ability);

}

6.我们在看一下扩展点的实体类和如何初始化到工厂里

/**
 * @ClassName ExtPointWrapper
 * @Description
 * @Author zhanghang
 * @Date 2020-07-27 15:43
 */
@Data
@ToString
@NoArgsConstructor
@AllArgsConstructor
public class ExtPointWrapper implements Serializable {
    /**
     * 扩展点类名
     */
    private String extensionClass;
    /**
     * 业务编码
     */
    private String bizCode;
    /**
     * 单据编码
     */
    private String billCode;
    /**
     * 场景类型
     */
    private String sceneType;

}

/**
 * @ClassName
 * @Description 抽象扩展点类
 * @Author zhanghang
 * @Date 2021-01-17 14:53
 */
@Slf4j
public abstract class AbstractExtPoint<T extends IParam, R> implements IExtPoint<T, R> {

    @PostConstruct
    protected void init(){
        ExtPoint extPoint = this.getClass().getDeclaredAnnotation(ExtPoint.class);
        if (extPoint == null) {
            log.info("扩展点类名为:{}未能够初始化路由,请关注", this.getClass().getName());
            return;
        }
        //添加扩展点路由信息
        ExtPointFactory.add(extPoint);
    }
}

ips:这边所有我们写的扩展点类都需要继承这个AbstractExtPoint扩展点父类,这样扩展点被spring实例化时候就会自动注册到工厂里。

7.接上文在框架选择走能力还是扩展点时,我们的扩展点已经加载到了工厂里,这边看一下怎么查到到正确的扩展点。

/**
 * @author zhanghang
 * @version V1.0
 * @ClassName
 * @Description
 * @date 2021/1/17
 */
@Slf4j
public class ExtPointFactory {

    /**
     * 能力扩展点容器
     * [{能力:[扩展点]}]
     */
    private static final Map<String, Set<ExtPointWrapper>> ABILITY_EXTPOINT_CONTEXT = new ConcurrentHashMap<>(128);

    public static void add(ExtPoint extPoint) {
        String abilityName = extPoint.ability().getSimpleName();
        String extpointName = extPoint.value();
        if (StringUtils.isAnyBlank(abilityName, extpointName)){
            throw new RuntimeException("初始化扩展点失败,请关注扩展点注解参数!");
        }
        Set<ExtPointWrapper> extPointWrapperSet = getAllExtPointByAbility(abilityName);
        if (CollectionUtils.isEmpty(extPointWrapperSet)) {
            extPointWrapperSet = new HashSet<>(16);
        }
        extPointWrapperSet.add(new ExtPointWrapper(extpointName, extPoint.bizCode(), extPoint.billCode(), extPoint.sceneType()));
        ABILITY_EXTPOINT_CONTEXT.put(abilityName, extPointWrapperSet);
    }

    public static Set<ExtPointWrapper> getAllExtPointByAbility(String abilityName) {
        return ABILITY_EXTPOINT_CONTEXT.get(abilityName);
    }

    public static IExtPoint getByProcessId(String abilityName, IParam param) {
        Set<ExtPointWrapper> extPointWrapperSet = getAllExtPointByAbility(abilityName);
        if (CollectionUtils.isEmpty(extPointWrapperSet)) {
            return null;
        }
        // 业务编码[一级]
        String bizCode = param.getBizCode();
        // 单据编码[二级]
        String billCode = param.getBillCode();
        // 场景类型[三级]
        String sceneType = param.getSceneType();
        // 无需走扩展点
        if (StringUtils.isAllBlank(bizCode, billCode, sceneType)) {
            return null;
        }
        ExtPointWrapper wrapper1 = selectExtPointWrapper(extPointWrapperSet, bizCode);
        ExtPointWrapper wrapper2 = selectExtPointWrapper(extPointWrapperSet, bizCode, billCode);
        ExtPointWrapper wrapper3 = selectExtPointWrapper(extPointWrapperSet, bizCode, billCode, sceneType);
        if (wrapper3 != null) {
            return FinanceSpringBeanUtil.getBean(wrapper3.getExtensionClass(), IExtPoint.class);
        }
        if (wrapper2 != null) {
            return FinanceSpringBeanUtil.getBean(wrapper2.getExtensionClass(), IExtPoint.class);
        }
        if (wrapper1 != null) {
            return FinanceSpringBeanUtil.getBean(wrapper1.getExtensionClass(), IExtPoint.class);
        }
        return null;
    }

    /**
     * 查找一级扩展点
     * @param extPointWrapperSet
     * @param bizCode
     * @return
     */
    private static ExtPointWrapper selectExtPointWrapper(Set<ExtPointWrapper> extPointWrapperSet, String bizCode) {
        if (StringUtils.isBlank(bizCode)) {
            return null;
        }
        for (ExtPointWrapper wrapper : extPointWrapperSet) {
            if (Objects.equals(wrapper.getBizCode(), bizCode)){
                return wrapper;
            }
        }
        return null;
    }

    /**
     * 查找一级+二级扩展点
     * @param extPointWrapperSet
     * @param bizCode
     * @param billCode
     * @return
     */
    private static ExtPointWrapper selectExtPointWrapper(Set<ExtPointWrapper> extPointWrapperSet, String bizCode, String billCode) {
        if (StringUtils.isAnyBlank(bizCode, billCode)) {
            return null;
        }
        for (ExtPointWrapper wrapper : extPointWrapperSet) {
            if (Objects.equals(wrapper.getBizCode(), bizCode)
                    && Objects.equals(wrapper.getBillCode(), billCode)){
                return wrapper;
            }
        }
        return null;
    }

    /**
     * 查找一级+二级+三级扩展点
     * @param extPointWrapperSet
     * @param bizCode
     * @param billCode
     * @param sceneType
     * @return
     */
    private static ExtPointWrapper selectExtPointWrapper(Set<ExtPointWrapper> extPointWrapperSet, String bizCode, String billCode, String sceneType) {
        if (StringUtils.isAnyBlank(bizCode, billCode, sceneType)) {
            return null;
        }
        for (ExtPointWrapper wrapper : extPointWrapperSet) {
            if (Objects.equals(wrapper.getBizCode(), bizCode)
                    && Objects.equals(wrapper.getBillCode(), billCode)
                    && Objects.equals(wrapper.getSceneType(), sceneType)){
                return wrapper;
            }
        }
        return null;
    }

}

tips:上面提到过业务身份的概念,但通常在业务比较复杂时,单一字段并不足以满足标识当前流程的业务身份,可能需要多个字段组合,这边提供了三个字段(一般来说3个也够了,再多的话逻辑会比较混乱,建议另起一个新的流程实现)。比方说:

  • 具体业务。组成一个简单的业务身份
  • 具体业务+单据。组成一个比较复杂的业务身份
  • 具体业务+单据+场景。组成一个最较复杂的业务身份
  • 然后需要注意的是,组成业务场景的3个字段是有一定上下级关系的,必须有上级业务身份字段才能有下级业务身份字段。这个比较好理解,必须有父菜单才能有子菜单。
  • 这个说白了就是业务身份就是key,扩展点就是value组成的一个键值对,绑定在具体的能力上。

8.我们来实际的看一个完整的能力类怎么写的。

/**
 * @author zhanghang
 * @version V1.0
 * @ClassName
 * @Description
 * @date 2021/1/21 16:10
 */
@Slf4j
@Ability
public class FrozenAccountDeductRemainReceivableMoneyAbility extends AbstractAbility<FrozenAccountDeductRemainReceivableMoneyRequestParam, Boolean> {

    @Autowired
    private NeedsAccountDeductMapper needsAccountDeductMapper;

    @Override
    public String getName() {
        return "冻结账扣应付需求的剩余应收金额能力";
    }

    @Override
    public String getDescription() {
        return "冻结账扣应付需求的剩余应收金额能力, 乐观锁保证";
    }

    @Override
    public Boolean execute(FrozenAccountDeductRemainReceivableMoneyRequestParam param) {
        if (param.jumpFrozenAccountDeductRemainReceivableMoneyAbility()) {
            log.info("FrozenAccountDeductRemainReceivableMoneyAbility已被跳过...");
            return Boolean.TRUE;
        }
        FrozenAccountDeductRemainReceivableMoneyPo po = param.getFrozenAccountDeductRemainReceivableMoneyRequestParam();
        if (po == null || CollectionUtils.isEmpty(po.getFrozenDetailList())) {
            log.error("冻结账扣应付需求的剩余应收金额能力参数缺失:{}", JSONObject.toJSONString(po));
            param.setFrozenAccountDeductRemainReceivableMoneyResponseParam(Boolean.FALSE);
            throw new BizException(CommonStateCode.PARAMETER_LACK, "冻结账扣应付需求的剩余应收金额能力参数缺失", JSONObject.toJSONString(po));
        }
        for (FrozenAccountDeductRemainReceivableMoneyPo.FrozenDetail frozenDetail : po.getFrozenDetailList()) {
            if (frozenDetail.getId() == null || frozenDetail.getToFrozenMoney() == null
                    || frozenDetail.getFromFrozenMoney() == null || frozenDetail.getFromRemainReceivableMoney() == null
                    || StringUtils.isAnyBlank(frozenDetail.getUpdatePerson(), frozenDetail.getUpdatePersonName())) {
                log.error("冻结账扣应付需求的剩余应收金额能力参数缺失:{}", JSONObject.toJSONString(po));
                param.setFrozenAccountDeductRemainReceivableMoneyResponseParam(Boolean.FALSE);
                throw new BizException(CommonStateCode.PARAMETER_LACK, "冻结账扣应付需求的剩余应收金额能力参数缺失", JSONObject.toJSONString(po));
            }
        }
        Date now = new Date();
        List<UpdateWrapper<NeedsAccountDeductDo>> updateWrapperList = po.getFrozenDetailList().stream()
                .map(e -> {
                    UpdateWrapper<NeedsAccountDeductDo> wrapper = new UpdateWrapper<>();
                    wrapper.lambda()
                            .setSql("remain_receivable_money = remain_receivable_money - " + e.getToFrozenMoney() + ", frozen_money = frozen_money + " + e.getToFrozenMoney())
                            .set(NeedsAccountDeductDo :: getUpdateTime, now)
                            .set(NeedsAccountDeductDo :: getUpdatePerson, e.getUpdatePerson())
                            .set(NeedsAccountDeductDo :: getUpdatePersonName, e.getUpdatePersonName())
                            .eq(NeedsAccountDeductDo :: getId, e.getId())
                            .eq(NeedsAccountDeductDo :: getRemainReceivableMoney, e.getFromRemainReceivableMoney())
                            .eq(NeedsAccountDeductDo :: getFrozenMoney, e.getFromFrozenMoney());
                    return wrapper;
                })
                .collect(Collectors.toList());
        for (UpdateWrapper<NeedsAccountDeductDo> wrapper : updateWrapperList) {
            int count = needsAccountDeductMapper.update(null, wrapper);
            if (count != 1) {
                log.error("冻结账扣应付需求的剩余应收金额失败:{}", JSONObject.toJSONString(wrapper));
                return Boolean.FALSE;
            }
        }
        param.setFrozenAccountDeductRemainReceivableMoneyResponseParam(Boolean.TRUE);
        return Boolean.TRUE;
    }
}

/**
 * @author zhanghang
 * @version V1.0
 * @ClassName
 * @Description
 * @date 2021/1/21 16:11
 */
public interface UnFrozenAccountDeductFrozenMoneyRequestParam extends IParam {

    /**
     * 跳过当前能力或扩展点
     * @return
     */
    default Boolean jumpUnFrozenAccountDeductFrozenMoneyAbility(){return Boolean.FALSE;}

    /**
     * 获取能力或扩展点执行逻辑入参
     * @return
     */
    UnFrozenAccountDeductFrozenMoneyPo getUnFrozenAccountDeductFrozenMoneyRequestParam();

    /**
     * 设置能力或扩展点执行逻辑返参到上下文context
     * @param responseParam
     */
    default void setUnFrozenAccountDeductFrozenMoneyResponseParam(Boolean responseParam){}
}

/**
 * @author zhanghang
 * @version V1.0
 * @ClassName
 * @Description
 * @date 2021/1/21 16:13
 */
@Data
@Builder
public class UnFrozenAccountDeductFrozenMoneyPo {

    /**
     * 账扣应付需求解冻明细
     */
    private List<UnFrozenDetail> unFrozenDetailList;

    @Data
    @Builder
    public static class UnFrozenDetail {
        /**
         * 账扣应付需求主键ID
         */
        private Long id;

        /**
         * 要冻结的金额
         */
        private BigDecimal toUnFrozenMoney;

        /**
         * 冻结前剩余应收金额
         */
        private BigDecimal fromRemainReceivableMoney;

        /**
         * 冻结前冻结金额
         */
        private BigDecimal fromFrozenMoney;

        /**
         * 更新人
         */
        private String updatePerson;

        /**
         * 更新人
         */
        private String updatePersonName;
    }

}

tips:

• 这是一个冻结某个业务单据的金额能力,因为我们是做成能力给业务层复用的,因此我们不知道调用者到底是但条调用还是批量调用,因此一般来说能力都需要设计成批量的。

• 还有就是更新的能力我们从严谨的角度来说需要靠乐观锁做保证,因为能力本身和调用者之间是解耦的,你不知道调用者会不会加锁失败或未加锁在并发时导致数据错误。

• 批量的能力是事务的,要成功都成功,有一条失败就是全部失败,失败后由调用者在context里抛出异常并且在框架执行的父类处回滚。[这一点需要培训重点关注]

• 能力不能仅仅用于代码流程编排,再简单的查询,修改业务只调用1,2个能力时是不需要搞这么复杂的,所以能力需要有返回值方便调用者直接使用能力,使用流程编排的在能力返回前手动把返回值设置到反参抽象方法里,由调用者的context来实现即可

• 能力本身解耦业务,因此这里我建议能力为了保证正常执行需要对参数做一些必要的验证。

• 如果在执行代码编排时有些特殊业务条件下是需要跳过某个能力有些不用调过,因此有些能力视情况而定给一个默认不跳过的方法,需要跳过的话由调用者实现。

对于能力来说我认为大致分为三大类:

  • 查询类能力:入参接口的PO是QueryWrapper[基于mybatisplus],基于别的ORM框架的话就是把对象的sql传入,查询能力只做sql的执行的返回,而不是给定具体入参条件,从而保证灵活性,否则有新的条件就会在PO加新的字段,这样会修改能力,违背我们对能力修改关闭的原则
  • 普通更新类能力:比方说更新表的一些不重要的字段,入参接口的PO是UpdateWrapper[基于mybatisplus],道理同上
  • 特定重要字段更新类能力:这一点是容易有争议的一点,因为有些同学会觉得使用普通更新类能力就可以做,没必要去写特定字段更新,这样很麻烦,其实我认为不是这样的,对于一些状态,金额等等重要字段在更新时需要严格把控,这个把控是需要在能力内部实现的,PO的入参也是由能力决定,相对来说不如1,2类能力灵活,但是重要的字段必须严谨的对待,不能偷懒使用第2类能力。
  • 检查校验能力:这个能力我觉得其实没有必要出现,因为他是基于查询能力的,而且多数检查校验不具有复用性,因此我认为在context实现反参方法时候做校验检查就可以了,而且还能放缓类的膨胀。这也是我没把他列在三大类能力之一的原因,但是由于这个考虑是随着编码深入慢慢体会到的,一开始没有想到,因此我的项目中是有检查校验能力的,后期会渐渐废弃掉。

9.最后我们来看一下调用方是怎么调用我们已经编排的好的命令类

/**
 1. @author zhanghang
 2. @version V1.0
 3. @ClassName
 4. @Description
 5. @date 2021/1/17 16:17
 */
@Slf4j
@Service(version = "1.0.0")
public class AccountStatementFacadeImpl implements AccountStatementFacade {

    @Autowired
    private CreateDefaultAccountStatementCommand createDefaultAccountStatementCommand;
 
    /**
     * 创建默认方式的对账单
     * 目前适用于创建寄销/非寄销对账单
     * @param context
     * @return
     */
    private Result<AccountStatementBillDto> createDefaultAccountStatement(CreateDefaultAccountStatementContext context) {
        Result<Boolean> checkResult = new AccountStatementCreateChecker.CreateDefaultAccountStatementChecker().doCheck(po);
        if (checkResult.isFailed()) {
            return Results.newFailedResult(checkResult.getStateCode(), checkResult.getStatusText(), checkResult.getAppMsg());
        }
        CreateDefaultAccountStatementContext context = CreateDefaultAccountStatementContext.builder()
                .customerId(Long.valueOf(po.getCustomerId()))
                .subjectId(Long.valueOf(po.getSubjectId()))
                .statementType(Integer.valueOf(po.getStatementType()))
                .draftTypeEnum(DraftTypeEnum.NON_CONSIGNMENT_CREATE_PREVIEW)
                .subjectRemark(po.getSubjectRemark())
                .supplierMoneyCalculaterManager(supplierMoneyCalculaterManager)
                .build();
        try {
            // 创建对账单命令
            AccountStatementBillDto accountStatementBillDto = createDefaultAccountStatementCommand.execute(context);
            return Results.newSuccessResult(accountStatementBillDto);
        } catch (Exception e) {
            log.error("创建默认对账单失败:{}", e.getMessage());
            log.error(e.getMessage(), e);
            return Results.newFailedResult(CommonStateCode.FAILED, e.getMessage(), e.getMessage());
        }
    }
}

ips:在真正调用之前我们肯定是需要根据入参来初始化好流程执行所必须的context。

写在最后

多人合作开发时候我们都希望整个项目的编码风格都想一个人写的一样,这样在阅读别人的代码就像阅读自己的代码一样,减少理解上的歧义。但是实际情况是不可能的。

如果项目里所有的编码都是严格使用这种约束行代码流程编排来做的话,对于一些简单的功能又太过臃肿。所以我们需要看情况来甄别长流程,复杂流程,核心流程使用这种方式编码,虽不能100%把控编码风格,但至少一个项目最重要的部分我们可以把控得住。

相比于以前的面向过程式编程,这套编码风格更加的面向对象了。以前为了实现一个功能,走完一个流程,多多少少会经历多个service的各种方法拼接,无法把一个完整流程的所有逻辑内聚到一个类里,多个不同的业务流程共同使用同一个service同一个方法,因为业务的特性自然就会出现各种if/else。时间久了你也不知道那个if是哪个流程业务的特殊处理,完全违背的高内聚,低耦合,随着业务的变化,需求的变更,功能的迭代,bug会层出不穷。反观再看这套编码风格,调用者入口处通过命令天然隔离其他业务,所有业务逻辑高度内聚到当前流程的context,再也不用担心会被别的业务污染。能力抽象到原子级别,一个能力只干一件事,高度复用,节省时间,提高效率,少加班美滋滋。

原文链接:blog.csdn.net/eumenides_/article/details/115183577

后台回复 学习资料 领取学习视频


如有收获,点个在看,诚挚感谢


浏览 185
点赞
评论
收藏
分享

手机扫一扫分享

举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

举报