使用策略模式消除冗长的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;
        }
    }
}

image.png

以上通过策略工厂模式把那段代码拆成了多个文件,通过策略工厂模式把冗长的if-else代码给分解了,我们再来看一下重构后的代码是不是更好呢。下图展示了重构的整体逻辑:


重构之后,创建了工厂类,由工厂类中的策略判断逻辑来决定是哪一种策略类型,在运动时动态确定使用哪种策略,最终路由到对应的校验方法里。


1.最终代码实现了以下几个点:重构后的代码符合了开闭原则,添加新策略的时候,最小化、集中化代码改动、减少引入bug的风险。


2.重构后的代码解耦了之前代码的复杂度,解耦了策略的定义、创建和使用,控制代码复杂度,让每个部分的代码不至于太复杂、代码量过多。现在每个类的代码基本上在一显示屏就能展示完成。


3.大大增加了代码的可读性和可维护性。

image.png

四、总结

当然,并不是所有if-else分支都是烂代码,只要if-else分支不复杂,代码不多,这并没有问题,只要遵循KISS原则,怎么简单怎么来,就是最好的设计。但是一旦if-else分支很多,且每个分支都包含很多复杂的逻辑判断,这个时候就可以考虑是不是通过策略模式可以更清晰的梳理代码,使得代码维护性更强。


来源  |  阿里云开发者公众号
作者  |  
汪峰(蔚风

相关文章
|
3月前
|
存储 缓存 关系型数据库
提升代码可读性问题之Repository仓储层在细节无关原则中的作用是什么
提升代码可读性问题之Repository仓储层在细节无关原则中的作用是什么
|
3月前
|
设计模式
对抗软件复杂度问题之组合(Composite)方法设计模式是什么,如何解决
对抗软件复杂度问题之组合(Composite)方法设计模式是什么,如何解决
|
3月前
|
数据库
代码的应用重构问题之BaseActivity类的主要功能问题如何解决
代码的应用重构问题之BaseActivity类的主要功能问题如何解决
|
设计模式 Java Apache
设计模式第九讲:常见重构技巧 - 去除不必要的!=
设计模式第九讲:常见重构技巧 - 去除不必要的!=
|
设计模式 存储 SQL
【Java设计模式 规范与重构】 五 重构实战:基于ID生成器case(上)
【Java设计模式 规范与重构】 五 重构实战:基于ID生成器case(上)
131 0
|
设计模式 存储 Java
【Java设计模式 规范与重构】 五 重构实战:基于ID生成器case(下)
【Java设计模式 规范与重构】 五 重构实战:基于ID生成器case(下)
196 0
|
设计模式 JSON 缓存
如何“好好利用多态”写出又臭又长又难以维护的代码?| Feeds 流重构方案
如何“好好利用多态”写出又臭又长又难以维护的代码?| Feeds 流重构方案
79 0
|
消息中间件 SQL 缓存
程序命名的原则与重构
命名是对事物本质的一种认知探索,是给读者一份宝贵的承诺。糟糕的命名会像迷雾,引领读者走进深渊;而好的命名会像灯塔,照亮读者前进的路。命名如此美妙,本文将一步步揭开它的神秘面纱!
程序命名的原则与重构
|
XML Java 数据格式
Attribute(特性),怎么用才更好?
前几年:   2008年的某一天,我坐火车去北京。硬卧上铺,一晚上就到北京了。爬到上铺之后发现,旁边上铺有一老兄抱着一个笔记本,一开始还以为是看电影呢,仔细一看才发现——老天呀,居然在写代码!     这老兄也太工作狂了,当时可是晚上九点多了呀。
1021 0
重构-改善既有代码的设计-简化函数调用
Rename Method 函数改名 问题函数的名称未能揭示函数的用途。方法修改函数名称。动机好的函数需要有一个清晰的函数名。
1004 0