美文网首页
遗留系统改造-理解代码并编写测试

遗留系统改造-理解代码并编写测试

作者: JayWu的架构笔记 | 来源:发表于2019-08-28 14:22 被阅读0次

    前言

    当我们开发一个新功能的时候,也曾经有过深入了解遗留系统的冲动,但阅读那些错综复杂的旧代码让人感觉头痛不已——不仅仅需要耗费大量的时间,而且好像对实现新功能没有太大的帮助。

    但不理解整体代码,会让我们在修改遗留代码的过程中非常被动,原有逻辑往往充满了各种各样的陷阱,一个修改就可能引发各种血案。

    我们迫切想知道如何够快速理解代码,哪些代码需要测试,以及怎样编写测试。

    本文将深入探讨这些问题,并给出相关的解决方法。

    如何理解代码

    我们在看代码时,往往会一头钻入各种各样的实现细节。而我们的大脑并不擅长记忆,看完A逻辑,等到C逻辑的时候,你可能已经忘记什么是A逻辑了。

    更加雪上加霜的是,遗留代码的实现往往非常混乱,业务逻辑与技术细节相互纠缠,让我们无法看清整体脉络,看着看着,就可能迷失了方向。

    既然大脑的记忆能力有限,那我们就把这个工作交给合适的工具,让它有时间处理最擅长的工作:思考。

    把握全局

    遗留代码量往往非常大,我们可以选择一部分感兴趣的模块或者功能进行深入理解。

    在深入每个类的细节前,首先要先了解核心类或者方法之间的关联与职责。

    使用注记或者草图,写下它们之间的关联逻辑,非常有助于我们梳理思路。

    注意,我们并不是要整理详细的UML类图,而是关键类和方法的意图以及关联,能够用你的纸和笔就可以快速勾勒的草图。

    随着草图的逐渐完善,原本看似零散的和方法将呈现他们清晰的关联和作用。

    时刻谨记,克制住自己深入细节,特别是技术细节,这个环节最重要的任务是把握全局。

    把握细节

    当我们对全局功能有了初步了解后,可以进一步了解实现细节。

    但遗留代码的结构往往惨不忍睹,大类和大的方法随处可见,你可能会迷失在一个几千行的类,或者是几百行的一个方法。

    我们在对付这些实现细节时,同样可以运用全局观的方法,避免进入细节迷宫。

    职责分离

    把一个类中的不同方法,或者一个方法中的不同代码,按照它们的职责进行分组排序,并添加相应的注释。

    分组排序后,对帮助阅读和理解代码有着非常好的提升效果。

    样例1

    public class Demo {
        public static final String MULTIPART_RESOLVER_BEAN_NAME = "multipartResolver";
        
        private MultipartResolver multipartResolver;
        
        private FlashMapManager flashMapManager;
        
        private LocaleResolver localeResolver;
        
        public static final String LOCALE_RESOLVER_BEAN_NAME = "localeResolver";
    
        private MultipartResolver multipartResolver;
    
        public static final String THEME_RESOLVER_BEAN_NAME = "themeResolver";
    }
    

    调整后:

    public class Demo {
        public static final String MULTIPART_RESOLVER_BEAN_NAME = "multipartResolver";
        public static final String THEME_RESOLVER_BEAN_NAME = "themeResolver";
        public static final String LOCALE_RESOLVER_BEAN_NAME = "localeResolver";
    
        private MultipartResolver multipartResolver;
        private LocaleResolver localeResolver;
        private MultipartResolver multipartResolver;
        
        private FlashMapManager flashMapManager;
    }
    

    样例2

    processedRequest = checkMultipart(request);
    multipartRequestParsed = (processedRequest != request);
    mappedHandler = getHandler(processedRequest);
    if (mappedHandler == null || mappedHandler.getHandler() == null) {
        noHandlerFound(processedRequest, response);
        return;
    }
    HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
    if (!mappedHandler.applyPreHandle(processedRequest, response)) {
        return;
    }
    mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
    

    调整后:

    processedRequest = checkMultipart(request);
    multipartRequestParsed = (processedRequest != request);
    
    // Determine handler for the current request.
    mappedHandler = getHandler(processedRequest);
    if (mappedHandler == null || mappedHandler.getHandler() == null) {
        noHandlerFound(processedRequest, response);
        return;
    }
    
    // Determine handler adapter for the current request.
    HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
    if (!mappedHandler.applyPreHandle(processedRequest, response)) {
        return;
    }
    
    // Actually invoke the handler.
    mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
    

    理解方法结构

    在面对超长的代码块时,除了常规注释,我们还可以使用开始结束标记,配合IDE的展开/收缩功能,可以大大帮助我们回忆代码意图。

    // 发送邮件开始
    - { ... }
    // 发送邮件结束
    
    // -------------
    
    // 打印日志开始
    - { ... }
    // 打印日志结束
    

    理解修改影响

    在较好地理解代码后,我们开始考虑如何修改代码,在思考过程中,将改动所影响的变量/方法做记号,确保不会遗留。

    // TODO 需要修改
    private String a;
    
    private String say(){
        // TODO xx修改开始
        ...
        // TODO xx修改结束
    }
    

    无用的代码

    在阅读代码过程中,我们经常会发现一些不用的代码,甚至主动产生一些无用的代码。

    新的功能不需要这段逻辑,你可能会注释掉某些方法的引用。这些被注释掉的,或者无人引用的代码,就变成了无用代码。

    我们应该如何处理它们呢?

    不要犹豫,直接删除它们。

    或许你自己没有察觉,当你注意到那些无用代码的时候,你的注意力已经被它分散了,不管这个持续时长有多少。

    更加糟糕的情况是,如果无用代码对你造成了某些困惑,那我们浪费的时间就更多了。

    如果我们将来需要这些代码怎么办?

    放心,那些先进的代码版本库,可以轻松帮你找回来。

    草稿式重构

    在进一步了解代码后,我们可能会受到遗留代码的一万点伤害——有太多太多可以吐槽的地方了。

    我们已经堆积了不少的想法,只是碍于重构需要改动的地方太多,没有测试的保护无法动手。

    而等待正常的需求来迭代优化这些代码,可能需要漫长的时间。

    难道你就只能按捺住那颗燥热的重构之心?

    当然不。

    认识代码的最佳技术就是重构。

    如果不考虑那些众多的测试,不考虑是否破坏已有的功能,不考虑所有历史的负担,我们使用自己最喜欢的方式,对变量、对类名、对代码进行大胆的重构,是否能够完全释放你的灵感?

    你是这块全新代码的主人,在重构过程中,你的设想得以验证,新的想法相互碰撞。

    这就是草稿式重构。

    仅仅用于理解代码、验证想法、获取灵感的临时重构。

    因为没有测试保护,这些重构代码不能直接用于生产环境。但也因为无需测试和背负历史负担,我们可以快速重构,它往往能够给你带来意想不到的效果。

    所以,拿起你的键盘,尝试草稿式重构吧,它可以让你更加深入理解代码,还能为你提供更多好的想法,在未来正式重构中提供更多的帮助。

    确定应该测试的代码

    在充分理解代码后,我们终于有信心面对修改。

    但是在修改代码前,我们必须先确定好应该测试哪些代码,否则就无法判断是否影响原有逻辑。

    确定应该测试的代码最关键的地方,就在于确定修改产生的影响。

    推测代码修改影响

    一开始的时候,我们可能没有办法准确把握所有影响范围,可以先初步列出影响范围,并把它们记录下来。

    IDE的查找引用功能是一个非常强大的手段,可以帮助我们快速定位修改地方被调用的范围,进而观察调用方如何使用返回值。

    但很多时候,正真的陷阱却是在那些难以察觉的地方:

    • 方法会修改入参的引用对象。
    • 修改后被用到全局或者静态数据。

    这些是我们需要特别注意的地方,也是我们写代码时应该尽量避免的做法。

    应该怎样写测试

    万事俱备,只欠东风。

    我们前面所有的准备,都是为了正确编写测试。

    如果我们只是忙于寻找和修补Bug,这个工作永无止境,而且过程痛苦不堪,因为我们永远处于被动地防守。

    只有手持的测试盾牌,我们才能主动反击。

    特征测试

    虽然我们已经对代码有所了解,但对它们的效果还是存有疑虑,因为我们无法完全确定目标代码的所有行为。

    在修改代码前,最重要的事情就是确定当前系统或者代码能够做什么。

    很多人都不经过验证,凭感觉认为它应该可以做什么,而这种感觉往往会让你掉坑。

    所以我们需要一个方法,能够客观判断实际行为,这个方法就是特征测试。

    在修改前通过编写特征测试来观察代码的实际行为,确保修改后不会影响原有行为。

    步骤

    • 对目标代码块编写测试
    • 编写失败断言
    • 从失败中得知代码行为
    • 修改测试,让它与预期目标代码的实际行为
      • 查看目标代码
      • 直接断言目标代码实际结果,确保未来修改不会改变原有结果
    • 不断重复上述步骤

    寻找交汇点

    我们在编写特征测试时,希望尽可能覆盖所有关键行为和代码路径。

    如果能够找到一个合适的交汇点,只需要对少数几个方法测试就能覆盖大量场景,同时能有效减少编写测试的工作量(解依赖等)。

    需要注意的是,若我们寻找到的旧系统交汇点组合了大量的方法,那么它就不太适合作为一个测试入口,因为这会引导你编写出一个迷你型的集成测试,这可不是我们想要的东西。

    通过寻找交汇点,不仅仅有利于简化旧代码测试,还可以判断代码设计的好坏。

    那些不合适测试的交汇点,就是我们未来需要重构的地方。

    但是,修改、重构之旅往往都是漫长的,我们需要不断完善测试用例,直到完全理解行为,才可以大展身手。

    通过测试感知系统

    有些时候,我们出于好奇心想深入探索类的行为,测试也是一种非常好的手段,能够帮助我们快速了解系统的主要意图。

    我们可以寻找代码中的复杂部分,引入变量进行感知。

    随着我们对代码的熟悉程度逐渐加深,会发现一些问题或者存在一些疑问,把它们加入待测试清单,持续编写测试触发,直到完全了解行为。

    当发现bug时

    在探索、感知代码的过程中,我们很可能会发现上古期间遗留下来的深坑。

    因为这个探索并不是任务驱动,我们应该如何做处理?

    放任不管或者等到下次关联任务再修改?

    都不是。

    发现问题时,只有一个原则:尽快修复。

    如果功能还未使用,主动修复。
    如果功能已使用,需要分析造成的影响,然后尽快修复。

    未雨绸缪,永远好于亡羊补牢。

    总结

    遗留系统往往让人觉得深不可测。

    这就需要我们花更多的时间,耐心的、充分的理解代码,才能避免深坑,甚至主动填坑。

    在动手改造系统前,最关键的是为先那些忠实的测试代码们安家置业,只有它们落地生根了,我们才能安心开疆拓土。

    下期我们将主动出击,直面那些遗留系统中最纯正血统的继承者们:大类和大的方法。

    中美合拍,敬请期待_

    相关文章

      网友评论

          本文标题:遗留系统改造-理解代码并编写测试

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