遗留系统改造-如何安全地修改原有代码

2019-08-07  本文已影响0人  JayWu的架构笔记

一个故事

在进入这个话题前,我们先讲一个故事。

开发同学从另一个团队接手了新的系统有一段时间了,但是平时都是加全新的功能,对已有的功能还没有完全熟悉。
这一天,我们的产品同学提了一个需求:我们需要在原来这个功能上新增一个东西,很简单,简单来说就是……。

开发同学听完需求后,发现这块功能并没有深入了解过,于是回去认真研究了下相关的产品功能,感觉改动不大,实现简单,于是信誓旦旦地对产品同学回复道:妥妥儿的,且看我一天搞定。

开发同学马不停蹄地打开IDE,一番摸索,很快找到相关功能所在的类,双击打开,IDE突然一阵卡顿。
开发同学有顿感不妙,仔细一看,一个庞大、难以理解的代码充斥着整个屏幕。

自己定的时间,含泪也得改完。

最后,开发同学在度日如年中,颤抖地完成了代码的提交,心里面却默默祈祷这次的改动不要引发其他问题。

不幸的是,最后还是出了故障。

不好的预感

墨菲定律:你越担心一件坏事发生,它就越可能发生。

上面故事中的场景,有没有一种似曾相识的感觉?

我们在平时工作中,是不是经常面临着时间紧迫,但必须修改的场景?

上线后,有没有问题全靠运气……

面对遗留系统,需要加入新的逻辑时,我们迫切需要一些具体的指导方案,能够安全地修改原有代码。

以下这些方法是你应该尝试的的方案:

安全地修改方法

使用TDD

TDD(测试驱动开发)非常适合用于编写新的方法/类。

修改步骤

使用TDD能够让我们更多时间去思考如何设计。

注意,我们一次操作只关注一件事情:重构或者编码。

我们大脑可不比计算机,如果同时处理多个事情,不仅仅降低效率,还容易引起问题。

使用新的方法

适用场景

若我们需要添加的代码连续出现在一个地方,使用新的方法来实现是一个好的做法。

修改步骤

确定修改点

public void scan(String x) {
    String result = x + x;
    display.show(result);

    // TODO 新增功能
    ...
}

插入新方法调用并注释

public void scan(String x) {
    String result = x + x;
    display.show(result);

    // TODO formatResult()
    ...
}

确定入参以及返回值

public void scan(String x) {
    String result = x + x;
    display.show(result);

    // TODO String result = formatResult(result)
    ...
}

测试驱动开发新的方法

@Test
public void testFormatResultWithLowerCase() {
    String result = new Demo().formatResult("x");
    Assert.assertEquals("X", result);
}

@Test
public void testFormatResultWithUpperCase() {
    String result = new Demo().formatResult("X");
    Assert.assertEquals("X", result);
}

在写单元测试时,我们必须注意,每次只测试一种行为。

然后不断完善代码,保证测试全部通过。

protected String formatResult(String result) {
    return result.toUpperCase();
}

去除注释,启用新方法

public void scan(String x) {
    String result = x + x;
    display.show(result);

    String result = formatResult(result)
    ...
}

protected String formatResult(String result) {
    return result.toUpperCase();
}

优点

缺点

使用新的类

适用场景

修改步骤

修改步骤与新的方法基本一致,区别在于新特性在新的类实现。

我们需要记住,始终坚持TDD方式。

最终效果如下:

public class ScanResultFormatter {

    public String format(String result) {
        // 更多复杂的格式化逻辑
        return newResult;
    }
}
public void scan(String x) {
    String result = x + x;
    display.show(result);

    String result = scanResultFormatter.format(result)
    ...
}

优点

所有特性实现都在新的类完成,我们可以更加安全地进行改动,以及进行更加优雅地设计,让代码更容易测试。

缺点

若新功能职责不清晰时使用新的类,可能使系统更加复杂和混乱。

使用包裹方法

适用场景

有时候,我们新增的功能与原来的逻辑并没有必然联系,仅仅是因为它们需要在一块执行,如果我们强行把功能塞到原有方法中,会使得原有方法职责混乱不清。

这个时候,使用新生方法/类就可能不太合适,手段外,使用包裹方法是另一个好的选择。

修改步骤

确定修改点

public void scan(String x) {
    // TODO 新增功能

    String result = x + x;

    display.show(result);
   
    ...
}

将原有逻辑重命名

private String handleAndShowResult(String x) {
        String result = x + x;

    display.show(result);
}

创建新方法,与原有方法一致,保持签名

public String scan(String x) {
}

新方法调用重命名后的原方法

public String scan(String x) {
    handleAndShowResult(x);
}

增加特性方法

新方法依旧使用TDD方法

public String scan(String x) {
    addSomething(x);
    handleAndShowResult(x);
}

protected void addSomething(String x) {
    ...
}

另一种修改步骤

不想改变原有行为,可以新增一个方法

public void scanWithAddSomthing(String x) {
    addSomething(x);
    scan(x);
}

优点

缺点

使用包裹类

适用场景

本质与使用包裹方法一样,但是通过包裹类,我们可以更加优雅地添加新特性。

修改步骤

确定修改点

新建类,接受修改类参数

public class WrapATDDemo {

    private Demo demo;

    public WrapATDDemo(Demo demo) {
        this.demo = demo;
    }

    public void scan(String x) {
        addSomething(x);
        demo.scan(x);
    }

    public void addSomething(String x) {
        ...
    }
}

使用TDD为包裹类实现新特性

替换原来使用旧类的地方为包裹类

new WrapATDDemo(new Demo()).scan();

优点

扩展

没错,这就是设计模式中的装饰模式。

Java中常用的各类输入输出流就是装饰模式的经典实现。

安全消除重复代码

我们在修改代码时,往往会发现大量的重复代码,不巧的是,我们需要使用这些代码来实现新的功能。

摆在我们面前有两个选择:

保持现状,会让系统继续腐烂;激进地重构,可能产生未知的问题。

我们需要一个安全的手段来消除这些重复代码。

修改步骤

使用TDD编写代码

重构

优点

消除重复是锤炼设计的强大手段,它可以使设计变得更灵活,同时让修改代码更容易。

故事的最后

开发同学决定开始编写测试,但一开始的时候是很糟糕的,他觉得写测试时间比写代码还多,感觉做了浪费了好多时间。

但是慢慢地,他开始发现,那些杂乱无章的遗留系统中出现了越来越多更好的代码,并且修改代码也变得越来越容易,bug也越来越少,这时,他仿佛觉得这么做又是值得的。

虽然编写测试花上了一些时间,但大部分情况下最终还是节省了时间,似乎不用再为每一次上线所祈祷,那些不起眼的的测试代码,仿佛安静却坚定地守护着那些美好的事情。

上一篇下一篇

猜你喜欢

热点阅读