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

一个故事

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

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

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

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

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

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

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

不好的预感

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

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

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

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

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

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

  • 使用TDD
  • 使用新的方法
  • 使用新的类
  • 使用包裹方法
  • 使用包裹类
  • 安全消除重复代码

安全地修改方法

使用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中常用的各类输入输出流就是装饰模式的经典实现。

安全消除重复代码

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

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

  • 复制粘贴,一切尽在掌握之中。
  • 开始重构。音乐在哪里?都起来high!

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

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

修改步骤

使用TDD编写代码

  • 复制粘贴实现功能
  • 测试通过后再进行重构

重构

  • 不急于设计最终的完美类
  • 从抽离独立小块重复代码开始
  • 即使是小小的重复块也不要忽略
  • 编写公共类
    • 相同流程,提供抽象类
    • 相同代码,独立职责类
  • 命名
    • 尽量使用全称,而非缩写
    • 新类/方法具有明确的含义

优点

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

故事的最后

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

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

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 224,983评论 6 522
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 96,354评论 3 403
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 172,216评论 0 367
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 61,061评论 1 300
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 70,073评论 6 400
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 53,541评论 1 314
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 41,906评论 3 428
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 40,881评论 0 279
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 47,428评论 1 324
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 39,460评论 3 346
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 41,578评论 1 355
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 37,176评论 5 351
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 42,913评论 3 339
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 33,348评论 0 25
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 34,490评论 1 277
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 50,142评论 3 381
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 46,650评论 2 366

推荐阅读更多精彩内容