天天看點

使用政策模式消除冗長的if-else|記一次smart-auto重構總結

作者:閃念基因
使用政策模式消除冗長的if-else|記一次smart-auto重構總結

導讀

作者針對smart-auto接口測試相關的核心代碼進行了一次重構,使代碼變得更清晰和可維護。

一、背景

smart-auto工具經過多年的疊代和好幾代測試同學的開發,現在功能已經非常強大,支援各種HSF接口調用傳回值的對比和斷言能力,每日跑的測試件已經占到了淘寶買菜所有自動化測試件的55%以上。但是随着功能的不斷增加,代碼也越來越來龐大,之前單一的功能也變得複雜,代碼的可維護性也在不停的降低。是以針對smart-auto接口測試相關的核心代碼進行了一次重構,使代碼變得更清晰和可維護。

二、現狀分析

可以看下優化之前接口測試相關的核心代碼,可以由下面簡單的表述下這段代碼意思:

1.工具中針對,Hsf接口校驗共有四種方式HsfCheck1、HsfCheck2、HsfCheck3、HsfCheck4;

2.所有的接口校驗方式都包含在一個Handler中,且不同的方式之間全部通過各種複雜的if-else分支來判斷;

3.整體代碼大概有200多行;

public CheckOutputModel doHandle(CheckCaseModel caseParam, BuildTestsuiteModel checkRecordModel, ExecuteResultModel executeResultModel) throws Exception {
     if(!jsonPathList.isEmpty()){
          if (attributeModel.isCompare()) {
                HsfCheck1
          }
     }else{
         if(attributeModel.isCompare()) {
                HsfCheck2
         }
     }
     if ( checkConfigStatusObject == null ||checkConfigStatusObject.isEmpty() || checkConfigStatusObject.getBoolean("isCheck") == false  ){
                return result;
            }else{
             if(assertExpectStr == null || !node.isCustom()){
                HsfCheck3
             }else{
                HsfCheck4
             }
            }
 }           

完整的代碼如下:

@Service("commonCheckHandlerAbandon")
public class CommonCheckHandler implements CheckHandler{
    @Resource
  private CaseConfigHandlerService caseConfigHandlerService;
    @Resource
    private CheckDataCaseService checkDataCaseService;
    private Logger logger = LoggerFactory.getLogger(this.getClass());
    @Override
    public CheckOutputModel doHandle(CheckCaseModel caseParam, BuildTestsuiteModel checkRecordModel, ExecuteResultModel executeResultModel) throws Exception {
        ThubNodeConfig node = JSON.parseObject(checkRecordModel.getTestsuiteDO().getStepConfig(), ThubNodeConfig.class);
        TestsuiteAttributeModel attributeModel = JSON.parseObject(checkRecordModel.getAttributes(), TestsuiteAttributeModel.class);
        if (checkRecordModel.getTestsuiteDO().getShadow()) {
            // 全鍊路壓測标
            EagleEye.putUserData("t", "1");
        }
        CheckOutputModel result = new CheckOutputModel();
        if(node==null){
            result.setSuccess(false);
            return result;
        }
        List<String> jsonPathList = Collections.emptyList();
        if(node.getJsonPath() != null && !node.getJsonPath().trim().isEmpty() ){
            jsonPathList = Arrays.asList(node.getJsonPath().split(";"));
        }
        try{
            //如果jsonPathList不為空,則執行jsonPath解析,執行jsonPath之後的對比
            if(!jsonPathList.isEmpty()){
                List<CheckDiffModel> totalFailInfo = new ArrayList<>();
                List<String> errorMessages = new ArrayList<>(); // 用于存儲錯誤消息
                for (String jsonPath  : jsonPathList) {
                    try {
                        if (attributeModel.isCompare()) {
                            String actualResultStr = StringEscapeUtils.unescapeJavaScript((String) executeResultModel.getValueByKey(CheckCaseInfoConst.ACTUAL_RESULT));
                            String expectResultStr = StringEscapeUtils.unescapeJavaScript((String) executeResultModel.getValueByKey(CheckCaseInfoConst.EXPECT_RESULT));
                            Object actualResult = null;
                            Object expectResult = null;
                            if (StringUtils.isNoneBlank(actualResultStr)) {
                                Object actualValueObject = JsonPath.read(actualResultStr, jsonPath);
                                String actualValue = JSON.toJSONString(actualValueObject);
                                if (JSON.isValidObject(actualValue)) {
                                    actualResult = JSON.parseObject(actualValue);
                                } else if (JSON.isValidArray(actualValue)) {
                                    actualResult = JSON.parseArray(actualValue);
                                } else {
                                    actualResult = JSON.parse(actualValue);
                                }
                            }
                            if (StringUtils.isNoneBlank(expectResultStr)) {
                                Object expectValueObject = JsonPath.read(expectResultStr, jsonPath);
                                String expectValue = JSON.toJSONString(expectValueObject);
                                if (JSON.isValidObject(expectValue)) {
                                    expectResult = JSON.parseObject(expectValue);
                                } else if (JSON.isValidArray(expectValue)) {
                                    expectResult = JSON.parseArray(expectValue);
                                } else {
                                    expectResult = JSON.parse(expectValue);
                                }
                            }
                            StringBuffer ignorBuffer = new StringBuffer();
                            ignorBuffer.append(node.getIgnorConfig());
                            List<CheckDiffModel> failInfo = QAssert.getReflectionDiffInfo("assert diff", expectResult, actualResult, ignorBuffer.toString(),
                                    ReflectionComparatorMode.LENIENT_ORDER, ReflectionComparatorMode.LENIENT_DATES, ReflectionComparatorMode.IGNORE_DEFAULTS);
                            failInfo.forEach(i -> i.setNodeName(jsonPath + "---" + i.getNodeName()));
                            totalFailInfo.addAll(failInfo);
                        }
                    } catch (Exception e) {
                        // 記錄錯誤消息
                        String errorMessage = "Error with JSON path: " + jsonPath + " - " + e.getMessage();
                        errorMessages.add(errorMessage);
                        logger.error(errorMessage, e);
                    }
                }
                if (!totalFailInfo.isEmpty()||!errorMessages.isEmpty()) {
                    if(!totalFailInfo.isEmpty()){
                        errorMessages.add(0, "value not same");
                    }
                    // 組合錯誤消息,用回車符分隔
                    String combinedErrorMessages = String.join("\n", errorMessages);
                    result.setSuccess(false);
                    result.setErrorCode(combinedErrorMessages);
                    result.setFailInfoList(totalFailInfo);
                } else {
                    result.setSuccess(true);
                }
//如果jsonPathList為空,走正常對比邏輯
            }else {
                result.setTraceId(EagleEye.getTraceId());
                if(attributeModel.isCompare()) {
                    String actualResultStr = StringEscapeUtils.unescapeJavaScript((String) executeResultModel.getValueByKey(CheckCaseInfoConst.ACTUAL_RESULT));
                    String expectResultStr = StringEscapeUtils.unescapeJavaScript((String) executeResultModel.getValueByKey(CheckCaseInfoConst.EXPECT_RESULT));
                    Object actualResult = null;
                    Object expectResult = null;
                    if (StringUtils.isNoneBlank(actualResultStr)) {
                        if (JSON.isValidObject(actualResultStr)) {
                            actualResult = JSON.parseObject(actualResultStr);
                        } else if (JSON.isValidArray(actualResultStr)) {
                            actualResult = JSON.parseArray(actualResultStr);
                        } else {
                            actualResult = JSON.parse(actualResultStr);
                        }
                    }
                    if (StringUtils.isNoneBlank(expectResultStr)) {
                        if (JSON.isValidObject(expectResultStr)) {
                            expectResult = JSON.parseObject(expectResultStr);
                        } else if (JSON.isValidArray(expectResultStr)) {
                            expectResult = JSON.parseArray(expectResultStr);
                        } else {
                            expectResult = JSON.parse(expectResultStr);
                        }
                    }
                    StringBuffer ignorBuffer = new StringBuffer();
                    ignorBuffer.append(node.getIgnorConfig());
                    List<CheckDiffModel> failInfo = QAssert.getReflectionDiffInfo("assert diff", expectResult, actualResult, ignorBuffer.toString(),
                            ReflectionComparatorMode.LENIENT_ORDER, ReflectionComparatorMode.LENIENT_DATES, ReflectionComparatorMode.IGNORE_DEFAULTS);
                    if (!failInfo.isEmpty()) {
                        result.setSuccess(false);
                        result.setErrorCode("value not same");
                        result.setFailInfoList(failInfo);
                    } else {
                        result.setSuccess(true);
                    }
                }
            }
            //執行斷言校驗
            JSONObject checkConfigStatusObject = JSON.parseObject(checkRecordModel.getTestsuiteDO().getCheckConfigStatus());
//無斷言直接傳回
            if ( checkConfigStatusObject == null ||checkConfigStatusObject.isEmpty() || checkConfigStatusObject.getBoolean("isCheck") == false  ){
                return result;
            }else{
//執行斷言校驗
                String assertActualStr = StringEscapeUtils.unescapeJavaScript((String)executeResultModel.getValueByKey(CheckCaseInfoConst.ACTUAL_RESULT));
                String assertExpectStr = StringEscapeUtils.unescapeJavaScript((String)executeResultModel.getValueByKey(CheckCaseInfoConst.EXPECT_RESULT));
                CheckDataCaseDO checkDataCaseDO = caseParam.getCaseDO();
                //斷言對比
                if(assertExpectStr == null || !node.isCustom()){
                    boolean checkResult = caseConfigHandlerService.resultAssert(checkRecordModel.getTestsuiteDO(), checkDataCaseDO,assertActualStr);
                    if (!checkResult){
                        result.setSuccess(false);
                        return result;
                    }
                    CheckDataCaseQueryDO checkDataCaseQueryDO = new CheckDataCaseQueryDO();
                    checkDataCaseQueryDO.setId(checkDataCaseDO.getId());
                    List<CheckCaseResult> checkResultList = checkDataCaseService.queryCheckCaseResult(checkDataCaseQueryDO).getResult();
                    List<CheckDiffModel> checkCoonfigFailInfo = new ArrayList<>();
                    for (CheckCaseResult checkCaseResult : checkResultList) {
                        String checkConfigResult = checkCaseResult.getCheckConfigResult();
                        List<Map> checkParse = JSONArray.parseArray(checkConfigResult, Map.class);
                        for (Map map : checkParse) {
                            CheckDiffModel checkDiffModel = new CheckDiffModel();
                            String checkConfig = String.valueOf(map.get("checkConfigResult"));
                            StringBuffer stringBuffer = new StringBuffer();
                            if(!StringUtils.equals(checkConfig,"true")){
                                stringBuffer.append((String)map.get("assertNode")+map.get("assertCondition")+map.get("assertErpect"));
                                checkDiffModel.setActualValue("false");
                                checkDiffModel.setNodeName(String.valueOf(stringBuffer));
                                checkCoonfigFailInfo.add(checkDiffModel);
                            }
                        }
                    }
                    if (checkCoonfigFailInfo.size() != 0) {
                        result.setSuccess(false);
                        result.setErrorCode("value not same");
                        result.setFailInfoList(checkCoonfigFailInfo);
                    } else{
                        result.setSuccess(true);
                    }
                    //跨應用斷言校驗
                }else {
                    boolean checkResult = caseConfigHandlerService.resultAssertComp(checkRecordModel.getTestsuiteDO(), checkDataCaseDO, assertActualStr,assertExpectStr);
                    if (!checkResult){
                        result.setSuccess(false);
                        return result;
                    }
                    CheckDataCaseQueryDO checkDataCaseQueryDO = new CheckDataCaseQueryDO();
                    checkDataCaseQueryDO.setId(checkDataCaseDO.getId());
                    List<CheckCaseResult> checkResultList = checkDataCaseService.queryCheckCaseResult(checkDataCaseQueryDO).getResult();
                    List<CheckDiffModel> checkCoonfigFailInfo = new ArrayList<>();
                    for (CheckCaseResult checkCaseResult : checkResultList) {
                        String checkConfigResult = checkCaseResult.getCheckConfigResult();
                        List<Map> checkParse = JSONArray.parseArray(checkConfigResult, Map.class);
                        CheckDiffModel checkDiffModel = new CheckDiffModel();
                        StringBuffer stringBuffer = new StringBuffer();
                        for (Map map : checkParse) {
                            Boolean checkConfig = (Boolean) map.get("checkConfigResult");
                            if(!checkConfig){
                                stringBuffer.append((String)map.get("assertNode")+map.get("assertCondition")+map.get("assertErpect"));
                                stringBuffer.append(",");
                                checkDiffModel.setActualValue("false");
                            }
                        }
                        checkDiffModel.setNodeName(String.valueOf(stringBuffer));
                        checkCoonfigFailInfo.add(checkDiffModel);
                    }
                    if (checkCoonfigFailInfo.get(0).getActualValue() != null) {
                        result.setSuccess(false);
                        result.setErrorCode("value not same");
                        result.setFailInfoList(checkCoonfigFailInfo);
                    } else{
                        result.setSuccess(true);
                    }
                }
                }
        }catch(Exception e){
            e.printStackTrace();
            result.setSuccess(false);
            result.setMsgInfo(e.getMessage());
        }finally{
            EagleEye.removeUserData("t");
        }
        return result;
    }
}           

以上代碼有以下幾點問題:

1.這段代碼是由冗長的if-else分支判斷組合起來的,且if-else的邏輯也比較混亂,然後這段代碼把4種Hsf的接口檢查都耦合在了一起,沒有擴充性。後續增加任何功能,都需要在原來耦合的代碼裡添加代碼,有可能會影響原有功能。

2.這段代碼沒有做到開閉原則,一段良好的代碼需要做到對擴充開發,對修改關閉。

3.所有實作Hsf校驗的邏輯都在一個handler類中,導緻這個類中的代碼很多,進而影響了代碼的可讀性、可維護性。

4.這段代碼的if-else條件判斷很難懂,無法判斷某個條件中的校驗到底是校驗哪一種Hsf校驗類型,每次檢視這段代碼都要研究好久。

三、解決方案

可以使用政策工廠模式來解決以上問題,把每種Hsf校驗的方式封裝起來,然後通過政策工廠模式來路由下發,把冗長的代碼解耦出來,形成了一套架構,并且保證了代碼的擴充性。廢話不多說,直接看代碼。

先建構一個政策工廠類:

public class CheckStrategyFactory {
    private final Map<CheckStrategySelector, HsfInterfaceCheck> strategyRegistry = new HashMap<>();


    @Autowired
    public CheckStrategyFactory(HsfAssertCheck hsfAssertCheck,
                                HsfCrossInterfaceAssertCompare hsfCrossInterfaceAssertCompare,
                                HsfFullCompareCheck hsfFullCompareCheck,
                                HsfMultipleJsonPathCompareCheck hsfMultipleJsonPathCompareCheck,
                                JsonPathCompareStrategySelector jsonPathCompareStrategySelector,
                                CrossInterfaceAssertCompareStrategySelector crossInterfaceAssertCompareStrategySelector,
                                FullCompareStrategySelector fullCompareStrategySelector,
                                AssertStrategySelector assertStrategySelector) {


        // 在構造函數或初始化塊中注冊所有政策
        strategyRegistry.put(assertStrategySelector, hsfAssertCheck);
        strategyRegistry.put(crossInterfaceAssertCompareStrategySelector, hsfCrossInterfaceAssertCompare);
        strategyRegistry.put(fullCompareStrategySelector, hsfFullCompareCheck);
        strategyRegistry.put(jsonPathCompareStrategySelector, hsfMultipleJsonPathCompareCheck);
        // ... 注冊更多政策 ...
    }
    public HsfInterfaceCheck getStrategy(ThubNodeConfig node, JSONObject checkConfigStatusObject,TestsuiteAttributeModel attributeModel , ExecuteResultModel executeResultModel) {


        for (Map.Entry<CheckStrategySelector, HsfInterfaceCheck> entry : strategyRegistry.entrySet()) {
            if (entry.getKey().matches(node, checkConfigStatusObject, attributeModel, executeResultModel)) {
                return entry.getValue();
            }
        }


        return null; // 兜底檢查政策傳回null
    }
}           

再建立2個接口,一個政策選擇接口CheckStrategySelector,一個Hsf校驗接口HsfInterfaceCheck。

public interface CheckStrategySelector {
    boolean matches(ThubNodeConfig node, JSONObject checkConfigStatusObject , TestsuiteAttributeModel attributeModel , ExecuteResultModel executeResultModel);
}           
public interface HsfInterfaceCheck {
    CheckOutputModel  check(CheckCaseModel caseParam, BuildTestsuiteModel checkRecordModel, ExecuteResultModel executeResultModel);
}           

再建立4個政策類和4個Hsf校驗類分别實作政策選擇接口CheckStrategySelector和Hsf校驗接口HsfInterfaceCheck。

以下是HsfCheck1和HsfCheck2政策選擇類,省略其他2個。

public class AssertStrategySelector implements CheckStrategySelector {
    @Override
    public boolean matches(ThubNodeConfig node, JSONObject checkConfigStatusObject, TestsuiteAttributeModel attributeModel , ExecuteResultModel executeResultModel) {
        String assertExpectStr = StringEscapeUtils.unescapeJavaScript((String)executeResultModel.getValueByKey(CheckCaseInfoConst.EXPECT_RESULT));
        return !(checkConfigStatusObject == null ||checkConfigStatusObject.isEmpty() || checkConfigStatusObject.getBoolean("isCheck") == false) && (assertExpectStr == null || !node.isCustom());
    }
}           
public class FullCompareStrategySelector implements CheckStrategySelector {
    @Override
    public boolean matches(ThubNodeConfig node, JSONObject checkConfigStatusObject, TestsuiteAttributeModel attributeModel , ExecuteResultModel executeResultModel) {
        return attributeModel.isCompare() && (node.getJsonPath() == null || node.getJsonPath().trim().isEmpty());
    }
}           

以下是HsfCheck1和HsfCheck2校驗類,省略其他2個。

@Service("hsfAssertCheck")
public class HsfAssertCheck implements HsfInterfaceCheck {
    @Resource
    private CaseConfigHandlerService caseConfigHandlerService;
    @Resource
    private CheckDataCaseService checkDataCaseService;
    @Override
    public CheckOutputModel check(CheckCaseModel caseParam, BuildTestsuiteModel checkRecordModel, ExecuteResultModel executeResultModel) {


}           
@Service("hsfFullCompareCheck")
public class HsfFullCompareCheck implements HsfInterfaceCheck {


    @Override
    public CheckOutputModel check(CheckCaseModel caseParam, BuildTestsuiteModel checkRecordModel, ExecuteResultModel executeResultModel) {


    }
}           

最後Handler代碼改造成了這一段。

@Service("commonCheckHandler")
public class CommonCheckHandler implements CheckHandler{
    private final CheckStrategyFactory factory;


    public CommonCheckHandler(CheckStrategyFactory factory) {
        this.factory = factory;
    }




    @Override
    public CheckOutputModel doHandle(CheckCaseModel caseParam, BuildTestsuiteModel checkRecordModel, ExecuteResultModel executeResultModel) throws Exception {
        ThubNodeConfig node = JSON.parseObject(checkRecordModel.getTestsuiteDO().getStepConfig(), ThubNodeConfig.class);
        TestsuiteAttributeModel attributeModel = JSON.parseObject(checkRecordModel.getAttributes(), TestsuiteAttributeModel.class);
        JSONObject checkConfigStatusObject = JSON.parseObject(checkRecordModel.getTestsuiteDO().getCheckConfigStatus());
        CheckOutputModel result = new CheckOutputModel();
        if(node==null){
            result.setSuccess(false);
            return result;
        }


        HsfInterfaceCheck hsfInterfaceCheckStrategy = factory.getStrategy(node, checkConfigStatusObject, attributeModel, executeResultModel);
        if(hsfInterfaceCheckStrategy != null){
            return hsfInterfaceCheckStrategy.check(caseParam, checkRecordModel, executeResultModel);
        } else {
            result.setSuccess(false);
            result.setErrorCode("未找到對應的校驗政策");
            return result;
        }
    }
}           
使用政策模式消除冗長的if-else|記一次smart-auto重構總結

以上通過政策工廠模式把那段代碼拆成了多個檔案,通過政策工廠模式把冗長的if-else代碼給分解了,我們再來看一下重構後的代碼是不是更好呢。下圖展示了重構的整體邏輯:

重構之後,建立了工廠類,由工廠類中的政策判斷邏輯來決定是哪一種政策類型,在運動時動态确定使用哪種政策,最終路由到對應的校驗方法裡。

1.最終代碼實作了以下幾個點:重構後的代碼符合了開閉原則,添加新政策的時候,最小化、集中化代碼改動、減少引入bug的風險。

2.重構後的代碼解耦了之前代碼的複雜度,解耦了政策的定義、建立和使用,控制代碼複雜度,讓每個部分的代碼不至于太複雜、代碼量過多。現在每個類的代碼基本上在一顯示屏就能展示完成。

3.大大增加了代碼的可讀性和可維護性。

使用政策模式消除冗長的if-else|記一次smart-auto重構總結

四、總結

當然,并不是所有if-else分支都是爛代碼,隻要if-else分支不複雜,代碼不多,這并沒有問題,隻要遵循KISS原則,怎麼簡單怎麼來,就是最好的設計。但是一旦if-else分支很多,且每個分支都包含很多複雜的邏輯判斷,這個時候就可以考慮是不是通過政策模式可以更清晰的梳理代碼,使得代碼維護性更強。

作者:汪峰(蔚風)

來源-微信公衆号:阿裡雲開發者

出處:https://mp.weixin.qq.com/s/tg4vTL6_TI-tnxaMyVLhsA