花了幾個月搞了套能力編排框架,再也不用深夜加班喫泡麪了

你能學到什麼

  • 工廠、策略,責任鏈等多種設計模式的實際運用
  • 項目中多個核心流程在開發層面統一風格語言
  • 瞭解能力編排框架帶來的能力複用和業務隔離
  • 顛覆傳統開發習慣方法固定入參帶來的方法擴展侷限

前言

當我們做一個新項目開始的時候,我們總會想項目成型之後希望他能夠有條不紊,每一個模塊的代碼風格統一,註釋詳盡,條理清晰。這樣方便不同的人去看邏輯實現時不會因爲開發者的習慣導致理解上的歧義。但是在組內多人協作開發時,儘管爲了這一目標我們會約定種種開發規範,但是隨着項目的推進,代碼量的增加,漸漸地就變的不可控起來,因爲在沒有被強制的約束下每個人的習慣是很難被改變的。儘管我們會做一些代碼評審來發現這些不符合事先的約定,但是又會因爲種種客觀原因,比如:工期,已經提測了,快要上線了,邏輯太過複雜了,不敢再去改,擔心會該出問題等等,導致只能就這樣。

對於一個項目來說,我們不可能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


   
   
   

後臺回覆 學習資料 領取學習視頻


如有收穫,點個在看,誠摯感謝


本文分享自微信公衆號 - 猿天地(cxytiandi)。
如有侵權,請聯繫 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閱讀的你也加入,一起分享。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章