美文网首页
遗留系统改造-如何安全地修改原有代码

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

作者: JayWu的架构笔记 | 来源:发表于2019-08-07 13:26 被阅读0次

    一个故事

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

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

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

    开发同学马不停蹄地打开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也越来越少,这时,他仿佛觉得这么做又是值得的。

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

    相关文章

      网友评论

          本文标题:遗留系统改造-如何安全地修改原有代码

          本文链接:https://www.haomeiwen.com/subject/jsjodctx.html