前言
程序员经常吐槽的件事是,前人留下了一堆代码像一坨*....
其实很多时候,我们自己也是写的那样样子的,当你隔半年在重新审查自己的代码,如果觉得依然很美好,要么没救了,要么就真的是大佬。那大部分的人应该是,咋就写了一坨啥呢?还有救!!!如何自救呢? -- 重构,自己挖的坑自己填
何谓重构
名词: 对软件内部结构的一种调整,目的是在不改变软件可观察行为的前提下,提高其可理解性,降低其修改成本。
动词使:使用一系列重构的手法,在不改变软件可观察行为的前提下,调整其结构。
三次法则
来自Don Roberts的一条准则: 第一次做某件事时只管去做;第二次做类似的事情会产生反感但是无论如何还是可以去做;第三次再做类似的事情,你就应该重构了。
重构的时机:
1.添加功能时;
2.修补错误时重构
3.复审代码时重构
背景
最近需要做一个工具,从代码层生成测试用例,那在这个过程中最讨厌繁琐的事情是解析java class让其可以对应到test case。在这里通过正则匹配的方式获取到想要的字段/值,然后把这些字段组装成想要的字段写到用例库。
一个糟糕的类
做class解析的时候,刚开始写的时候只有几个的方法,后来随着需要的字段越多,方法就越多了,类似的方法写了十多个。
对于这个类,当我在写多几个时候,其实有意识到要对代码进行调整了,但想着先把功能实现了吧,结果就演变到了十几个了,其实此时有一个好的地方是,我在写每个方法的时候有意识的都加了测试代码,我需要确保每个的匹配都是正确的,在为我后续的重构做了一层保护。
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() : "";
}
开始我的重构
- 提炼函数,将重复调用的方法进行整理
public static String getMatchStringValue(String fileString) {
String rgex = "header\\((.*?)\\).";
List<String> lists = getSubUtil(fileString, rgex);
}
- 添加参数
新增两个参数,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) : "";
}
}
- 移除临时变量
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\\(\"(.*?)\"\\).";
- 移除参数
到第三步的时候,发现大部分场景下返回的都只有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) : "";
}
}
- 继续完善
在这里发现有方法包含特殊的处理,如下
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) : "";
}
- 到此就完成了一个基础的重构,那么接下来进行下测试
原本的测试
@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:jacocoTestReport
Deprecated 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_warnings
BUILD SUCCESSFUL in 13s
4 actionable tasks: 4 executed
- 继续思考,为何返回的有的是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();
}
继续测试,测试通过。
重构的步骤
对于重构而言最好的一种方式是要有足够的单元测试,它可以有效的保障不破坏原有的功能。
重构-》测试通过 -》重构 -》测试通过