重构精髓:十六字心法

上一步替换常量、改变函数签名的过程中,有没有同学一上来就将整个构造函数或者as()方法的签名替换掉?如果这么操作,在你一口气把 10+个函数的引用点修改过来之前,软件将一直处于不可工作的状态。这节内容,我们要向你介绍一种更精细的做法,也就是《重构》一书里反复展演的重构手法:如何让代码在重构的过程一直保持可工作。

这套手法背后的原理,ThoughtWorks 的王健将之总结为一句十六字心法,那就是:

旧的不变

新的创建

一步切换

旧的再见

什么?你说心法太难懂?那下面我们以替换as()方法签名为例(as(String targetUnit) -> as(Unit targetUnit))来理解一下这个过程:

旧的不变,新的创建

“旧的不变,新的创建”,指的是对于你要重构的编程元素(这里是指as()方法的String unit字符串变量),先不去做直接的修改或替换,也不直接删除它,而是先创建一份新的拷贝,并传入你期望的参数类型。做完以后,运行一下测试,因为新函数并没有任何调用点,所以此时测试应该都能够正常通过。

    ...
    public Length as(String unit) { ... }
    public Length temp_as(String unit, Units temp_unit) { ... }
    ...

创建完新方法后,你需要将旧方法内的实现直接委托给新方法:

    ...
    public Length as(String unit) {
        return this.temp_as(unit, null);
    }
    public Length temp_as(String unit, Units temp_unit) { ... }
    ...

做完这步后,同样需要运行一下测试。由于新函数内仍然是原来的方法体,而旧方法的对外接口并没有改变,因此测试仍然不太应该挂掉。如果挂了,你应该能很快地撤销,并发现问题所在,修改以后再运行测试。

这一步的作用是建立一个中间层:对旧方法的引用点仍然不改变,因此你不需要一下改变方法的所有调用点。这一步的作用很关键,因为它创建了一种“新旧共存”的机制,这使得你能逐步完成替换,随之带来的变化是:你可以在重构的任意一步中停止,同时保证系统仍然处于可用的状态。

事实上,在做完这步后,你已经可以提交代码,然后再慢慢地一步步将旧方法的引用点切换到新的方法上去,系统在重构的整个过程中将仍然保持可用的状态。

接下来,我们要在temp_as中使用Unit类型的temp_unit取代原来的unit,可以在as()方法中完成这个调整:

class Length {
    ...
    public Length as(String unit) {
        Unit temp_unit = unit == Length.YARD ? Unit.YARD : null;
        return this.temp_as(unit, temp_unit);
    }
    public Length temp_as(String unit, Units temp_unit) { ... }
    ...
}

enum Unit {
    YARD,
}

做完这步以后,运行测试。测试应该也不会挂,因为我们只是新增了一个尚未被使用到的变量。接下来,我们再把这个变量在temp_as中用上:

    ...
    public Length as(String unit) { }
    public Length temp_as(String unit, Units temp_unit) {
        ...
        if (this.unit.equals("f")) {
-            if (unit.equals("yard")) {
+            if (temp_unit == Unit.YARD) {
                len = new Length(this.value / 3, unit);
            } else { ... }
        }
        ...
    }
    ...

运行测试。

一步切换

搭建好新旧函数之间的桥梁以后,下一步要做的是找到所有的引用点并一一替换。我们在老的as()上使用快捷键cmd + B查找它的所有引用点如下:

好家伙,还真不少,引用点一共有 11 个。但不要紧,因为我们已经搭建好了新旧方法的桥梁,可以慢慢替换。首先找到第一处引用点,将它替换为直接调用temp_as(),运行测试。测试应该能完全通过。

- Length length = new Length(1, Length.INCH).as(Length.INCH)
+ Length length = new Length(1, Length.INCH).as(Length.INCH, Units.INCH)

对于剩余的引用点,我们如法炮制,直至把所有的as(String unit)都替换成为temp_as(String unit, Units temp_unit)。这样,我们就完成了“替换as()方法参数类型”的重构。后续as()方法仍然在其他地方使用了unit参数,对这些地方的替换,就留给你去练习了。

这里也就显示出真正的重构技巧与一般的刀砍斧劈之间的差别了:我们重构的每一步都以极小的步子前进,随时都能保证测试通过、系统正常。更重要的是,这个过程是随时可以停止的,在真实的项目中,你可能没有大段的时间重构,可能会不时出现优先级更高的事情打断手头的工作。有了随时停止的能力,你就可以将宏大的重构目标分解成许多细小的动作,日拱一卒,哪怕每天空闲的时候花上 5 分钟重构、提交,最终也能安全地、聚沙成塔地完全大的重构目标。

旧的再见

完成所有的引用点替换后,IDE 随即也会提示我们:as(String unit)已经没有引用点了。此时,我们就可以把这个函数安全地删除,运行一下测试。测试应该依然能通过,我们也完成了一次简单的重构。

最后,我们把那个临时的方法名temp_as(Unit temp_unit)重新命名回来,为它重新正名:as(Unit unit),此时,我们甚至可以将它改成更加表意的as(Unit target)

这节的信息量有点大,所以,我录了一个视频,你可以放松地看一下,直观感受一下这个心法的实操过程。

Last updated