前言
修改代码有非常多好的技巧,我们往往会被五花八门的方法所淹没,导致在正真需要的时候想不起来,或者在选择的时候无从下手。
因为我们缺少了一个指导原则。
在深入了解各种使用技巧前,我们必须先掌握修改的原则,才能更好地掌握技巧。
核心原则
修改代码的核心原则就是:为了使代码更好地测试。
具体到细节,会有两个主要的手段来保证原则的落地:感知和分离。
感知,即感知运行效果或者影响。
分离,即分离其他部分独立运行。
原则和手段都非常简单,但如果能够严格遵循,我们不仅仅可以写出更好测试的代码,同时还能写出高质量的代码。
接下来,我们会通过几个简单的案例,详细讲解如何运用原则,对代码进行修改和测试。
感知
下面是一个非常典型的场景,面对这种把内部逻辑包裹得严严实实的方法,很多时候我们是不是就放弃了治疗(测试)?或者直接跑跑页面看看结果?
public class Demo {
public void scan(String x) {
String result = x + x;
// 需要将结果输出到某个地方
System.out.println(result);
}
}
究竟应该怎么样才能在测试中感知隐藏其中的result?
我们需要一个合作者来帮忙。
伪装成合作者
伪装成合作者的关键思路为,用新接口取代旧代码进行测试。
让我们来尝试一番。
修改代码
首先创建显示接口。
public interface Display {
void show(String result);
}
然后实现原有显示功能。
public class ConsoleDisplay implements Display {
public void show(String result) {
System.out.println(result);
}
}
用新接口取代旧代码。
public class Demo {
private Display display;
public Demo(Display display) {
this.display = display;
}
public void scan(String x) {
String result = x + x;
display.show(result);
}
}
测试代码
首先创建伪对象,最核心的地方在于暴露感知点。
public class FakeDisplay implements Display {
private String result;
public void show(String result) {
// 捕获感知
this.result = result;
}
// 暴露感知
public String getResult() {
return result;
}
}
最后使用伪对象进行测试。
@Test
public void testSuccessfulResult() {
FakeDisplay display = new FakeDisplay();
String x = "1";
new Demo(display).scan(x);
Assert.assertEquals(x + x, display.getResult());
}
至此,我们不仅把代码变得可测试,同时还抽象出了一个显示接口,正好吻合了职责分离的设计原则,同时为未来输出到不同的设备做好了扩展的准备。
分离
还是刚刚那个例子,我们发现在显示逻辑后面,紧接着一个异常复杂的方法调用。
Invoker类里面的逻辑不仅仅庞大难懂,更糟糕的是里面充满了错综复杂的依赖,吓得你赶紧关了这个类,看半小时朋友圈压压惊,回来都不愿意看第二眼。
public class Demo {
public void scan(String x) {
String result = x + x;
display.show(result);
// 这里是一个非常复杂的方法
new Invoker().invoke();
}
}
但不幸的是,这个方法隐藏在需要测试的scan方法中,但我们只想静静地测试result,完全不想关心invoke的变幻莫测。
该怎么办呢?
我们需要找一个缝隙,把它彻底隔离掉。
对象接缝
这个方法的关键思路在于:将接缝对象以入参形式传入。
修改代码
public void scan(Invoker invoker, String x) {
String result = x + x;
display.show(result);
invoker.invoke();
}
测试代码
首先创建伪对象,分离不关心逻辑。
public class FakeInvoker extends Invoker {
@Override
public void invoke() {
}
}
最后用使用伪对象分离测试。
@Test
public void testSuccessfulResultWithoutInvoker() {
FakeDisplay display = new FakeDisplay();
String x = "1";
new DemoV2(display).scan(new FakeInvoker(), x);
Assert.assertEquals(x + x, display.getResult());
}
或许你会问,如果我不想改变原来的方法签名怎么办,因为很多地方都调用它。
针对这种情况,我们可以保持原有签名,创建默认对象即可。
public void scan(String x) {
scan(new Inovker(), s);
}
方法接缝
除了常见的对象接缝,我们还会遇到一些奇怪的方法,比如下面的x(),你又得看半小时朋友圈来压压惊。
public class Demo {
public void scan(String x) {
String result = x + x;
display.show(result);
x();
}
private static void x() {
// 这里是一个非常复杂的方法
}
}
我们如何能够安静地测试result,同时去除静态方法的依赖呢?
修改代码
静态方法改为成员方法,使用protected可重写。
public void scan(String x) {
String result = x + x;
display.show(result);
x();
}
protected void x() {
// ...
}
测试代码
重写方法进行分离测试。
@Test
public void testSuccessfulResultWithoutX() {
FakeDisplay display = new FakeDisplay();
String x = "1";
new DemoV2(display) {
@Override
protected void x() {
}
}.scan(x);
Assert.assertEquals(x + x, display.getResult());
}
我们发现,分离原则能够更好地指导我们解除各种复杂的依赖。
总结
通过几个简单的案例,我们已经可以感受到修改原则的威力:
在不改变原有行为的基础上,进行安全地重构,得到可测试的代码,以及具有良好设计的代码。
本文是改造系列终章吗?
当然不是,我们才刚刚开始改造的步伐,前路漫漫,且看后续需要面临的荆棘:
- 如何安全地修改原有方法
- 如何添加新特性
- 如何调整为可测试的代码
- 修改时应该测试哪些方法
- 修改时应该怎样写测试
- 如何测试错综复杂的API调用
- 如何快速理解代码
- 如何修改大量相同的代码
- 如何修改超级大类
- 如何修改超长方法
- ……
不管道路如何坎坷,感知和分离原则,一直是我们坚实地后盾,将伴随、指导遗留系统改造系列所有文章。