谈谈重构

最近几个月专门在做系统重构,写点东西记录自己的心得。

首先说说我的经历。更换团队之后,我从接手系统到开始重构,大概有一年半的时间,时间不短,因此我把它划分为几个阶段来描述:

  1. 熟悉阶段

    接手系统之后,由于业务需求排期比较急,我并没有足够的时间可以通过阅读代码熟悉系统的所有设计与逻辑,只能摸着石头过河。接到一个新的需求,往往都是先了解需求的前因后果,然后在前辈的指导下了解系统对应部分的大致实现,由于前期分配给我的需求一般都比较小,所以很多时候,我都是先了解系统特定部分的逻辑,然后加以修改。对系统接触和变更越来越多,加上前辈的指导,我对系统的设计与功能也渐渐熟悉。这一阶段大概持续了三个月,虽然有很多业务需求,有些需求很紧急,但毕竟是新人,很多业务压力没有透传到我这里。

  2. 改进阶段

    熟悉系统之后,我开始接触更多需求,同时也负责部分模块的后续规划与技术优化。这一阶段我不仅要考虑需求的实现,同时要考虑如何合理地实现。所以我做的更多的事情是模块功能与设计的梳理,将耦合在一起的功能拆分成不同的子模块,将实现不合理的部分逻辑重新实现,当然这些事情很多时候都是跟随需求一起完成的,在业务发展迅速的时候,很难停下来专门做优化。也就是在这一阶段,团队发展,有更多的新人加入,我开始带新人接手这些模块,让他们顺利进入“熟悉阶段”。这一阶段大概持续了六个月,我从新人变成了部分系统模块的owner,对系统的整体实现与逻辑都有了足够的了解。

  3. 踩坑阶段

    业务发展越来越快,需求越来越多,由于系统的复杂度太高,很多时候只能通过增加人力来解决问题,而新人对系统了解不深入,可能让系统越来越复杂,导致恶性循环。我跟老大表明我想重构手上的部分模块,这个让我正式进入踩坑阶段。我之前没有大规模重构的经验,有的只是一些小规模的重新设计与实现,这一次,我希望是把模块部分功能重新实现。我按照自己对业务的了解,以自己认为合理的方式,花了一个多月时间完成了。新逻辑测试发布上线之后,我们不断接到各种咨询与投诉,问题集中在我重写的这部分功能上面,很多边缘功能我并没有考虑清楚,导致线上故障频现。我又花了一个多月时间处理各种投诉,修复新逻辑带来的各种问题。这一阶段也持续三个月左右,这次失败的重构经历让我学习到不少新的东西,大概归类为以下几点:

    (1) 重构的大前提是系统稳定运行,不能以牺牲系统稳定性为代价
    (2) 重构的功能是否符合预期,不能只靠人来保证,要建立完善的自动化测试用例来验证新逻辑
    (3) 协议要做好兼容与转换,系统的重构是系统内部的变化,对外暴露的接口应该是不变的
    
  4. 重构准备阶段

    踩坑之后,我发现很多系统的边缘功能我还无法完全掌握,所以我又花了相当长的时间梳理现有系统的逻辑与实现,同时着手建立完善的自动化测试用例。每次系统变更,我们都首先在预发布环境上跑一遍用例,保证没问题之后再发布上线。但是系统的设计与实现的不合理,还是成为了我们团队前进的阻力,团队因此专门抽出部分人力进行重构。从上一次失败的重构到立项重新进行重构,我们又经历了大概四个多月。

其次说说我对重构的理解。我认为重构并不是对整个系统进行重写,它应该是平时对系统不合理的设计与实现的不断改进。

接着说说为什么要重构。我觉得重构的原因有两个,其一是程序员的天性,其二是系统的设计与变更。

  1. 程序员的天性

    作为程序员,无论是刚毕业加入第一家公司,还是工作之后跳槽到其他公司,从零开始开发一个新系统的机率是比较低的。作为新人,我们一般都是先接手并熟悉当前系统,了解它的逻辑与实现,并配合业务需求不断完善系统的功能与设计。绝大多数程序员都是有追求的,当我们足够了解现有系统之后,现有系统实现不合理的地方必然会成为我们的眼中钉,我们总会找机会进行重构。

  2. 系统的设计与变更
    没有一个系统是完美的,或者说没有一个系统在设计的时候,就能预见到以后的所有需求场景。有的系统可能在设计与实现之初就留下了非常详尽的文档说明,在相当长的时间内,后来者都可以在原来的框架上合理地添加新功能。而有的系统可能连代码注释都极少,后来者往往忽略原来设计的初衷,按自己的理解来开发新的功能。随着接手系统的人越来越多,系统的功能越来越复杂,一些问题逐渐暴露出来。这里列出几类可能促使我们进行系统重构的问题:

    (1) 系统原本的设计不合理,可能是数据模型设计不合理,也可能是代码框架设计不合理,总之这些不合理的设计使得实现新的需求与特性变得十分困难。
    (2) 系统复杂度太高,各类功能与实现耦合在一起,任何的小修改都可能影响到别的功能,任何的系统变更都可能导致现网故障,这个就像我们常说的“系统已经改不动了”。
    (3) 系统使用的技术或框架太旧,或者已经没有很好的社区与官方支持。
    

最后说说如何进行重构。

  1. 首先,我们要有能力保证重构不会影响我们系统的稳定运行,一种有效的方式是建立完善的自动化测试用例,覆盖我们系统的所有使用场景,每次变更前都统一执行一遍。建议完善的自动化测试用例的一种方法是对足够长时间内线上系统的请求日志进行归类与记录,用实际的数据与请求模拟线上的行为。
  2. 其次,开始设计新系统,从底层数据模型(数据库),到系统层次与功能模块划分,到每个模块的代码结构。
  3. 再次,做好新老系统的兼容,这里可能包括数据模型的兼容,比如数据双写、新老数据对账等,也包括系统协议的兼容,比如抽象出统一的协议转换层等。
  4. 再次,做好新系统的验证,新系统在人工验证通过之后,必须通过之前建立的自动化测试用例的验证。
  5. 最后,新系统上线,建立回滚机制,考虑通过灰度等手段降低切换风险。

我计划接下来读一读《重构:改善既有代码的设计》这本书,学习如何更好地进行重构。