重构的流程
重构手法
正如上一次所讲的那样,重构有两个基本条件,一是要保持代码在重构前后的行为基本不变,二是整个过程是受控且尽可能少地产生错误。尤其是对于第二点,产生了一系列的重构手法,每种重构手法都是一系列简单而机械的操作步骤,通过遵循这一系列的操作来实现代码的结构性调整。因此,重构的整个过程就是不断运用不同重构手法的过程,是一个相对有章可循的流程。
重构手法有大有小,大的重构手法一般由若干小的基础重构组成,进而聚沙成塔实现对代码结构大幅度的调整。完整的重构列表请参见《重构,改善既有代码的设计》一书。
例如,replace conditional with polymorphism这项复杂重构手法,就至少需要使用self encapsulate, extract method, move method, pull down method这四种基础重构手法。因此在学习类级别的复杂重构手法前,需要先掌握行级别和方法级别的基础重构手法。
重构步骤
重构的宏观步骤一般有如下两种:自上而下式和自下而上式。
自上而下的重构在重构前,心中已经大致知道重构后的代码将会是什么形态,然后至上而下地将步骤分解出来,并使用相应的重构步骤一一实现,最终达到重构后的形态。其流程为:
1. 识别代码中的坏味道
2. 运用设计原则,构思出修改后的目标状态
3. 将目标状态分解为一或多项重构步骤
4. 运用重构步骤
自下而上的重构则对重构后的代码没有一个完整而清晰的认识。一般而言,每种重构手法都有助于我们解决某种类型的代码坏味,而自下而上的重构则针对每个发现的代码坏味直接运用对应的重构手法,直到没有明显的坏味,此时的代码即能自动满足某种设计模式。是一种迭代的思路,也是所谓重构到模式的思路。其流程为:
1. 识别代码中的坏味道
2. 运用一项或多项重构步骤,消除坏味
3. 重复1-2,直到没有明显坏味
在一般的情况下,这两种重构流程并不是互斥的,经常交错进行或互相包含。如先运用自上而下的方法识别出代码中的坏味,然后根据设计原则重构到某个实现,再运用自下而上的方法重新寻找新的坏味,迭代重构。
基础重构手法
由于基础重构手法比较多,而且相对比较简单。因此先列出常用的基础重构手法和简单介绍,并在最后的实践案例中结合基础重构手法来重构代码。
rename(重命名变量/方法/类)
- 坏味:含义不清的命名
- 说明:变量名应当体现出变量的作用和含义、方法名应当表现出方法的效果、类名也应提示类的职责和在继承体系中的位置。
- 操作方法:IntelliJ Shift+F6
reorder(调整语句顺序)
- 坏味:变量的申请和使用分离太远
- 说明:变量的使用应当尽可能离使用近一些,否则会扩大变量的作用域,在重构时也会产生困难。
- 操作方法:IntelliJ Alt+Shift+↑↓ 针对无副作用的语句,直接调整语句位置。
split for/block(拆分for循环/代码块)
- 坏味:一个循环或代码块中同时操作了多个变量或执行了多个职责
- 说明:一个循环中若有太多变量要计算,不利于将此循环提取为单独方法。
操作方法:
- 将循环复制一次
- 每个循环中只保留一个变量的计算
- 将循环提取为独立方法
- 将所有循环的出现替换为方法的调用
guard clauses(卫语句)
- 坏味:过深的条件嵌套
- 说明:先判断跳出/过滤的条件,并直接return或continue,可除去多余的else嵌套深度。
操作方法:
- IntelliJ 在if语句上Alt+Enter,选择invert if,可倒转if和else语句
- IntelliJ 在else语句上Alt+Enter,选择remove redundant else
extract variable(提取变量)
- 坏味:单条语句过长,含义不清
- 说明:将部分语句提取出变量,并为变量起一个能够解释变量含义的名称来替代注释
- 操作说明:IntelliJ Ctrl+Alt+V
extract method(提取方法)
- 坏味:单个方法过长,含义不清
- 说明:将做同一件事的代码提取出方法(一般为计算某个变量,或进行单个复杂操作),并为方法起一个能够解释”这件事”的名称来替代注释
- 操作说明:IntelliJ Ctrl+Alt+M,需要考虑返回和参数的列表,返回不能超过1个变量
inline method(内联方法)
- 坏味:方法只有一行代码,且内容本身已经很明确(多行也可以,但若原方法有返回值,则会比较复杂,不推荐)
- 说明:方法的作用是聚合操作并提供注释信息,若方法内容已经明确,则方法本身就起不到作用,反而增加复杂度
- 操作说明:将方法内容复制后,替换方法调用的部分。再删除方法本身
add parameter(方法增加参数)
- 坏味:方法主体只有部分变量不同
- 说明:可以提取变化的部分成为参数,从而合并两个相似的方法
操作说明:
- 将变量的部分提取为变量,并提到方法的最开始处
- 将方法剩余的内容提取为一个新的方法,新方法会含有新的参数
- 将原来的老方法内联
案例实践
重构前
private Set<String> channelColumns;
public String generateSql() {
String channelColumnClauseTemp = StringUtil.flat(channelColumns, ",", "", "");
String channelColumnClause;
Set<String> columns = new TreeSet<>();
for (String str : channelColumns) {
if (ChannelId.CLT_BUS_EML_ADR.toString().equals(str)) {
columns.add(ChannelId.CLT_EML_ADR.toString());
} else {
columns.add(str);
}
}
channelColumnClause = StringUtil.flat(columns, ",", "", "");
String channelColumnsReviewTemp = "";
String channelColumnsReview = "";
if (!channelColumns.isEmpty()) {
channelColumnsReviewTemp = channelColumnClauseTemp +
(channelColumns.contains(idTypeColumn) ? "" : ("," + idTypeColumn)) + ",batch_id";
channelColumnsReview = channelColumnClause +
(channelColumns.contains(idTypeColumn) ? "" : ("," + idTypeColumn)) + ",batch_id";
} else {
channelColumnsReviewTemp = idTypeColumn + ",batch_id";
channelColumnsReview = idTypeColumn + ",batch_id";
}
StringBuffer vsql = new StringBuffer();
vsql.append("insert into ").append(Constant.DB_SCHEMA).append(".").append(tableName)
.append(" (").append(channelColumnsReview).append(")")
.append(" select distinct ").append(channelColumnsReviewTemp.replace(Constant.CITYNAME_COLUMN, "isnull(" + Constant.CITYNAME_COLUMN + &#