写点什么

重构: 自己挖的坑自己填

用户头像
夏兮。
关注
发布于: 2021 年 04 月 03 日

前言

程序员经常吐槽的件事是,前人留下了一堆代码像一坨*....其实很多时候,我们自己也是写的那样样子的,当你隔半年在重新审查自己的代码,如果觉得依然很美好,要么没救了,要么就真的是大佬。那大部分的人应该是,咋就写了一坨啥呢?还有救!!!如何自救呢? -- 重构,自己挖的坑自己填

何谓重构

名词: 对软件内部结构的一种调整,目的是在不改变软件可观察行为的前提下,提高其可理解性,降低其修改成本。动词使:使用一系列重构的手法,在不改变软件可观察行为的前提下,调整其结构。

三次法则

来自 Don Roberts 的一条准则: 第一次做某件事时只管去做;第二次做类似的事情会产生反感但是无论如何还是可以去做;第三次再做类似的事情,你就应该重构了。重构的时机:1.添加功能时;2.修补错误时重构 3.复审代码时重构 #背景最近需要做一个工具,从代码层生成测试用例,那在这个过程中最讨厌繁琐的事情是解析 java class 让其可以对应到 test case。在这里通过正则匹配的方式获取到想要的字段/值,然后把这些字段组装成想要的字段写到用例库。

一个糟糕的类

这里处理测试文件的的时候,刚开始写的时候只有几个的方法,后来随着需要的字段越多,方法就越多了,类似的方法写了十多个。

对于这个类,当我在写多几个时候,其实有意识到要对代码进行调整了,但想着先把功能实现了吧,结果就演变到了十几个了,其实此时有一个好的地方是,我在写每个方法的时候有意识的都加了测试代码,我需要确保每个的匹配都是正确的,在为我后续的重构做了一层保护。



public static String getDescription(String fileString) { String rgex = "@Description\\(\"(.*?)\"\\)"; List<String> lists = getSubUtil(fileString, rgex); if (lists.isEmpty()) { throw new Error("pls add description"); } return !lists.isEmpty() ? lists.get(0) : ""; }
public static String getWhen(String fileString) { String rgex = "when\\(\\)\\.(.*?)\\)"; List<String> lists = getSubUtil(fileString, rgex); return !lists.isEmpty() ? lists.get(0) : ""; }
public static String getHeader(String fileString) { String rgex = "header\\((.*?)\\)."; StringBuilder header = new StringBuilder(); List<String> lists = getSubUtil(fileString, rgex); for (String string : lists) { header.append(string); header.append("\n"); } return !lists.isEmpty() ? header.toString() : ""; }
复制代码


开始我的重构


  1. 提炼函数,将重复调用的方法进行整理


    public static String getMatchStringValue(String fileString) {            String rgex = "header\\((.*?)\\).";        List<String> lists = getSubUtil(fileString, rgex);   }
复制代码


  1. 添加参数新增两个参数,rgex: 正则;isMultipleValue: 是否多个值,多个值的进行拼接


    public static String getMatchStringValue(String fileString, String rgex, Boolean isMultipleValue ) {        List<String> lists = getSubUtil(fileString, rgex);        if ( isMultipleValue && !lists.isEmpty()) {            StringBuilder multipleValue = new StringBuilder();            for (String string : lists) {                multipleValue.append(string);                multipleValue.append("\n");            }            return multipleValue.toString();        } else {            return !lists.isEmpty() ? lists.get(0) : "";        }    }
复制代码


  1. 移除临时变量


        String rgex = "@Description\\(\"(.*?)\"\\)";
复制代码


声明字面量


    public static final String CLASS_NAME = "public class(.*?)\\{";    public static final String FUNC_NAME = "public void(.*?)\\(";    public static final String DESCRIPTION = "@Description\\(\"(.*?)\"\\)";    public static final String WHEN = "when\\(\\)\\.(.*?)\\)";    public static final String HEADER = "header\\((.*?)\\)."; //TURE    public static final String COOKIE = "cookie\\((.*?)\\)."; //ture    public static final String BODY = "body\\(\"(.*?)\"\\).";
复制代码


  1. 移除参数到第三步的时候,发现大部分场景下返回的都只有 list 的第一个值,只有有两是有多个的。此时把第二步加的参数 isMultipleValue 去掉改用判断。


    public static String getMatchStringValue(String fileString, String rgex ) {        List<String> lists = getSubUtil(fileString, rgex);        if ( (rgex.contains(HEADER) || rgex.contains(COOKIE)) && !lists.isEmpty()) {            StringBuilder multipleValue = new StringBuilder();            for (String string : lists) {                multipleValue.append(string);                multipleValue.append("\n");            }            return multipleValue.toString();        } else {            return !lists.isEmpty() ? lists.get(0) : "";        }    }
复制代码


  1. 继续完善在这里发现有方法包含特殊的处理,如下



public static String getClassName(String fileString) { String rgex = "public class(.*?)\\{"; StringBuilder suiteName = new StringBuilder(); List<String> lists = getSubUtil(fileString, rgex); String list1 = lists.get(0); list1 = list1.replace("Test", ""); for (int i = 0; i < list1.length(); i++) { if (Character.isUpperCase(list1.charAt(i))) { suiteName.append(" "); suiteName.append(list1.charAt(i)); } else { suiteName.append(list1.charAt(i)); } }
return suiteName.toString().replace("A P I", "API"); }
复制代码


搬移特殊的处理部分


        if(rgex.contains(CLASS_NAME)) {            String className = lists.get(0);            StringBuilder suiteName = new StringBuilder();            className = className.replace("Test", "");            for (int i = 0; i < className.length(); i++) {                if (Character.isUpperCase(className.charAt(i))) {                    suiteName.append(" ");                    suiteName.append(className.charAt(i));                } else {                    suiteName.append(className.charAt(i));                }            }            return suiteName.toString().replace("A P I", "API");        }
复制代码


提炼函数,将 if 内的提炼成一个函数


    private static String transferClassName(String className){        StringBuilder suiteName = new StringBuilder();        className = className.replace("Test", "");        for (int i = 0; i < className.length(); i++) {            if (Character.isUpperCase(className.charAt(i))) {                suiteName.append(" ");                suiteName.append(className.charAt(i));            } else {                suiteName.append(className.charAt(i));            }        }        return suiteName.toString().replace("A P I", "API");    }
复制代码


修改后



public static String getMatchStringValue(String fileString, String rgex) { List<String> lists = getSubUtil(fileString, rgex); if ((rgex.contains(HEADER) || rgex.contains(COOKIE)) && !lists.isEmpty()) { StringBuilder multipleValue = new StringBuilder(); for (String string : lists) { multipleValue.append(string); multipleValue.append("\n"); } return multipleValue.toString(); }
if(rgex.contains(CLASS_NAME)) { return transferClassName(lists.get(0)); } if(rgex.contains(DESCRIPTION) && lists.isEmpty()) { throw new Error("pls add description"); } return !lists.isEmpty() ? lists.get(0) : ""; }
复制代码


  1. 到此就完成了一个基础的重构,那么接下来进行下测试原本的测试


    @Test    public void testGetBody() {        String body = RegexUtil.getBody("    @Description(\"case name\")      @Test\n" +                "    public void addBlackboardShouldCorrect() {\n" +                "        RestAssured.useRelaxedHTTPSValidation();\n" +                "        RestAssured.given().cookie(LTM_COOKIE_KEY, LTM_COOKIE_VALUE).\n" +                "                contentType(ContentType.JSON).\n" +                "                body(\"{\\\"issId\\\":1,\\\"iss\\\":\\\"blcakBoard\\\",\\\"name\\\": \\\"my first blackboard\\\",\\\"desc\\\":\\\"for test\\\"}\").\n" +                "                when().\n" +                "                post(LTM_SERVER_URL + API_ADMIN_ADD_INSTANCE + EXTENSION_ID).\n" +                "                then().\n" +                "                assertThat().\n" +                "                statusCode(HttpStatus.SC_OK).\n" +                "                and().time(lessThan(TIME_OUT)).\n" +                "                and().assertThat().body(matchesJsonSchemaInClasspath(\"ltm.response/admin-create-item-response.json\"));\n" +                "    }");        Assert.assertEquals("{\\\"issId\\\":1,\\\"iss\\\":\\\"blcakBoard\\\",\\\"name\\\": \\\"my first blackboard\\\",\\\"desc\\\":\\\"for test\\\"}",body);    }
复制代码


重构测试代码


    @Test    public void testGetBody() {        String body = RegexUtil.getMatchStringValue("    @Description(\"case name\")      @Test\n" +                "    public void addBlackboardShouldCorrect() {\n" +                "        RestAssured.useRelaxedHTTPSValidation();\n" +                "        RestAssured.given().cookie(LTM_COOKIE_KEY, LTM_COOKIE_VALUE).\n" +                "                contentType(ContentType.JSON).\n" +                "                body(\"{\\\"issId\\\":1,\\\"iss\\\":\\\"blcakBoard\\\",\\\"name\\\": \\\"my first blackboard\\\",\\\"desc\\\":\\\"for test\\\"}\").\n" +                "                when().\n" +                "                post(LTM_SERVER_URL + API_ADMIN_ADD_INSTANCE + EXTENSION_ID).\n" +                "                then().\n" +                "                assertThat().\n" +                "                statusCode(HttpStatus.SC_OK).\n" +                "                and().time(lessThan(TIME_OUT)).\n" +                "                and().assertThat().body(matchesJsonSchemaInClasspath(\"ltm.response/admin-create-item-response.json\"));\n" +                "    }", RegexUtil.BODY);        Assert.assertEquals("{\\\"issId\\\":1,\\\"iss\\\":\\\"blcakBoard\\\",\\\"name\\\": \\\"my first blackboard\\\",\\\"desc\\\":\\\"for test\\\"}",body);    }
复制代码


测试结果


> Task :api-tools:test> Task :api-tools:jacocoTestReportDeprecated Gradle features were used in this build, making it incompatible with Gradle 7.0.Use '--warning-mode all' to show the individual deprecation warnings.See https://docs.gradle.org/6.3/userguide/command_line_interface.html#sec:command_line_warningsBUILD SUCCESSFUL in 13s4 actionable tasks: 4 executed
复制代码


  1. 继续思考,为何返回的有的是 lists.get(0),有的要拼接所有的值。继续读,发现在这里我应该可以返回所有的 list 的值,因为 test case,test step 已经处理成 list,做遍历了。



public static String getMatchValue(String fileString, String rgex) { List<String> lists = getSubUtil(fileString, rgex); if(rgex.contains(DESCRIPTION) && lists.isEmpty()) { throw new Error("pls add description"); } StringBuilder multipleValue = new StringBuilder(); for (String string : lists) { multipleValue.append(string); multipleValue.append("\n"); }
if(rgex.contains(CLASS_NAME)) { return transferClassName(multipleValue.toString()); }
return multipleValue.toString(); }
复制代码


继续测试,测试通过。

重构的步骤

对于重构而言最好的一种方式是要有足够的单元测试,它可以有效的保障不破坏原有的功能。

重构-》测试通过 -》重构 -》测试通过

发布于: 2021 年 04 月 03 日阅读数: 206
用户头像

夏兮。

关注

星辰大海... 2018.03.21 加入

测试开发工程师 热爱技术,热爱生活

评论

发布
暂无评论
重构: 自己挖的坑自己填