遗留系统改造-修改代码的核心原则

前言

修改代码有非常多好的技巧,我们往往会被五花八门的方法所淹没,导致在正真需要的时候想不起来,或者在选择的时候无从下手。
因为我们缺少了一个指导原则。
在深入了解各种使用技巧前,我们必须先掌握修改的原则,才能更好地掌握技巧。

核心原则

修改代码的核心原则就是:为了使代码更好地测试。
具体到细节,会有两个主要的手段来保证原则的落地:感知和分离。

感知,即感知运行效果或者影响。
分离,即分离其他部分独立运行。

原则和手段都非常简单,但如果能够严格遵循,我们不仅仅可以写出更好测试的代码,同时还能写出高质量的代码。

接下来,我们会通过几个简单的案例,详细讲解如何运用原则,对代码进行修改和测试。

感知

下面是一个非常典型的场景,面对这种把内部逻辑包裹得严严实实的方法,很多时候我们是不是就放弃了治疗(测试)?或者直接跑跑页面看看结果?

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调用
  • 如何快速理解代码
  • 如何修改大量相同的代码
  • 如何修改超级大类
  • 如何修改超长方法
  • ……

不管道路如何坎坷,感知和分离原则,一直是我们坚实地后盾,将伴随、指导遗留系统改造系列所有文章。

你可能感兴趣的:(遗留系统改造-修改代码的核心原则)