> For the complete documentation index, see [llms.txt](https://ethan-lin.gitbook.io/refactoring/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://ethan-lin.gitbook.io/refactoring/dan-wei-zhuan-huan/7-refactor-secrets.md).

# 重构精髓：十六字心法

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

这套手法背后的原理，**Thought**Works 的王健将之总结为一句[十六字心法](http://insights.thoughtworks.cn/principles-of-refactoring/)，那就是：

> 旧的不变
>
> 新的创建
>
> 一步切换
>
> 旧的再见

![重构十六字心法](/files/-Lr58_tQFeykyvzit1VX)

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

## 旧的不变，新的创建

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

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

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

```java
    ...
    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()`方法中完成这个调整：

```java
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`中用上：

```java
    ...
    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`查找它的所有引用点如下：

![find-references](/files/-Lqv4_bpGDNmiWdCd_Jw)

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

```java
- 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)`。

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

&#x20;[![](/files/-LrEmtlXk08HnjCKvU3V)](https://github.com/linesh-simplicity/refactoring-course/tree/584195717663db01b5acb8fbe3bdf5e11d4049ba/content/units/videos/1-renaming-steps-480p.mov)
