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

前言

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

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

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

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

如何理解代码

我们在看代码时,往往会一头钻入各种各样的实现细节。而我们的大脑并不擅长记忆,看完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时

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

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

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

都不是。

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

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

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

总结

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

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

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

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

中美合拍,敬请期待_

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

推荐阅读更多精彩内容