你为什么不敢重构代码?
来源:ES2049 / 黑石
https://juejin.cn/post/6951373058544730125
代码重构有两大难点,一个是「考古」,也就是如何快速梳理出代码的原有逻辑,还有一点就是「发布」,如何让新的代码可以稳定的发布到线上,而不产生故障。下面我们就聊聊我一个朋友的故事,看看他是怎么把代码稳定搞上线的。为了表达更为亲切,你现在就是我那个朋友。
重构代码对很多人来说,绝对是一件脏活、累活。没有可以大幅度提效的方法,难以沉淀有效的体系化的可复用的技术抓手,对业务来说没有明显的增量,精力和时间消耗巨大,没有测试用例,也不一定能得到测试的支持,自测很难做到充分,最后开发完了很难上线,主要原因是害怕!当然并不是我们不自信,是真的恐惧。
一、你为什么不敢发代码?
通过代码还原当时完整的产品逻辑太难了
你重构的代码是谁的?鬼知道是谁的!能让你重构的代码大概率不是你写的代码,而且是远古代码,用的是一种过时的技术栈。当然一般情况下,当年的开发、测试、甚至产品早已不见了踪迹,只能在注释的代码里看见了了数语。言语中透露着无奈,用一个程序员的良心提醒着后来人,「小心前面的脏东西」。看了这些话,你只能收回口中马上要吐出的芬芳,默默离开工位,倒点热水。
从此你会发现,注释不仅能够帮你读懂代码,还能有警示作用,告诉你重构代码的同时,记得把 bug 一并改了。你想要通过注释来梳理出原始需求的愿望宣告失败,接下来你只能死磕了,祈祷千万不要漏掉业务逻辑。
没有自测用例
别以为大公司制度完善,测试都有完整的测试用例,现实会狠狠的夹你脑门。频繁的迭代,功能早已面目全非,老的用例根本不可用,更何况根本找不到老的测试用例。没有用例怎么自测呢?全靠个人想象。
没有测试同学跟进
多一个人多一分力量,让一个有经验的测试参与到功能回归中来,无疑会给你的重构事业吃上定心丸,但真实的情况是,测试同学根本不想参与这种脏活累活。他自己手里的需求还测不过来,怎么会把时间奉献给一个前端发起的重构工作上呢。无增量,无抓手,纯体力,他们同样心知肚明。
没有稳定发布方案
在没有上述保障的前提下,如果你还能硬着头皮上线,就会遇到更大的难题,如何上线?直接全量替换吗?如果线上出问题怎么办?好在前端的回滚是非常迅速的,但是即使再迅速的回滚,从发布完成到发现问题回滚,在提醒用户重新刷新页面,这个过程也足以造成难以估量的后果,尤其是那些高频使用,且极易产生脏数据的场景。这就是没有一个有效的发布方案所导致的常见后果,这个后果还有可能导致你背上故障,这一年加过的班,熬过的夜,掉的头发,什么也换不来,只能催生你换个地方重新做人的念头。
综上因素直接导致开发者极度缺乏安全感,一个不敢上线自己代码的程序员,就像半夜被自己一个月大孩子的哭声吵醒,那时那刻你只想装死摸鱼。更何况你的工作往往不是只有重构这一件事,写写新需求他不香吗?就这样你眼看着一个页面重构了两个星期,迟迟不能收尾,你变得越来越不自信,越来越害怕了起来,不敢面对那些重构了一半的代码,开始恐惧老板的问题:「重构搞的怎么样了?」,你简直不像个程序员。
终于到了年底,你的重构事业还未完成,更可怕的是,这件事还被打上了「承诺型」OKR 的标,于是你痛定思痛,做了个梦。
时间回到年初你刚刚接到重构任务的时候。
二、寻求组织保障
你的重构工作是把 177 个 jQuery 页面用 React 重写一遍。你立马想到,自己一个人一年时间,一定是做不完的,此时此刻,切记不要满口答应,一定实事求是,甚至向着最坏的方向想,让老板充分认识到这项任务的艰巨性,不要抱有太高的期望。最重要的是保证人力的投入,必须有更多的同学一起参与进来,有效的分工才有可能完成这项艰巨的任务。有人参与进来,也只是基础,因为他们极有可能会像上面描述的一样,从兴致勃勃到唯唯诺诺,因此一定要确保时间的投入,必要时把老板也拉进来跟你一起做,老板一旦参与进来,就会更有体感,能体会到大家的不易。接下来,就应了那就老话,「别忘了,你是一个 owner!」做好基础设施建设,让每个同学有趁手的工具,有安全的保障,去除他们的后顾之忧至关重要。因此,你要做下面几件事。
三、划分重构页面优先级
你通过细致的研究发现,这些页面中,有 77 个页面是用户使用较多的页面,也是相对比较复杂的页面,剩下的 100 个页面,大部分是给开发用的增删改查页面,用户的使用频率不高。于是你做了如下划分:优先级划分好优先级以后,就要对不同优先级的页面使用不同的稳定发布策略。
复杂高频页面:重兵压上,细致还原原始需求,抠代码,拉测试同学一起整理测试用例,按照测试用例自测,测试同学回归所有功能。但其实这部分页面中,也可以分为两种页面: 编辑页面:这样的页面是风险最高的页面,一旦因为后端接口没有做完整的数据校验,就会编辑出脏数据,或者错误的数据被保存,导致线上运行异常,这种后果将是不堪设想的,即使非常短的时间内回滚,也会造成难以挽回的故障,因此必须要像新需求一样测试到位。 展示页面:这样的页面不会影响运行时,不会产生脏数据,是风险相对低一点点的页面,本着不麻烦合作方的原则,毕竟资源有限,可以让测试帮你出完整的用例,然后你自己自测,或者多找几个同学帮你自测。 高频简单页面:自测,当然最好是能绑架几个经常用这个功能的开发,来帮你点点,但是自己测总是会有可能会有遗漏,因此就需要下面的步骤来保证了。 低频运维页面:选择性重构,因为很多页面基本上不会有迭代,且使用频率较低,基本上不需要重构,即使是有新的需求,也可以在做新需求的时候顺便重构下,以为并不能占用太多时间。
将页面划分完毕后,你会发现重构的工作量降低了很多,因为本着「无需求,勿变更」的原则,很多页面都可以不需要重构。且上述重构完的页面都必须做灰度发布。
四、单测
前端不太喜欢写单测,你大概总结了一下,主要有下面几方面的原因:
当下的收益不高。 相比后端接口的单测,前端单测写起来相对复杂。 前端更多是面向 UI 的编程,但 UI 变动大,难以使用 TDD (测试驱动开发) 的开发模式。 没有写单测的习惯,可能是因为单测增加了工作量,且没有写纯函数的意识,不利于测试。 单测的工具难学又难用。
你发现前端不喜欢写单测,有各种各样的原因,但是当你重构那些复杂页面,尤其是 jQuery 技术栈重构为 React 技术栈的时候,单测真的非常有用。比如这里有一个编辑页面,包含两部分:基本信息和运行逻辑,在重构运行逻辑时候,你首先要保证的是重构过后的页面在保存的时候,保存的数据结构必须跟之前的接口参数必须一致,所以在重构运行逻辑这个组件的时候就会有很多数据转换逻辑。可以看到为了保证你的新组件不影响保持原有功能,就要保证原始数据通过新组件的一顿操作最终保留了原来的结构,此时你就可以写单测来保证这个过程。
describe('utils', () => {
it('流程图:转换为提交的数据 transformForm', () => {
const result = transformForm(canvasData);
expect(result).toEqual(settingData);
});
it('流程图:转换为需要的数据 parseRuleSetData', () => {
const [result] = parseRuleSetData(settingData, rules);
expect(result).toEqual(canvasData);
});
it('流程图:反复转换 transformForm - parseRuleSetData', () => {
const [result] = parseRuleSetData(visualSettings, rulesData);
const newResult = transformForm(result);
expect(newResult).toEqual(visualSettings);
});
});
复制代码
前端单元测试写起来复杂,其实只是 UI 的单测复杂而已,如果你把代码做好了足够的拆分,拆出更多函数,更多 hooks ,单测就是轻而易举了。
五、测试用例
你在的团队,一直测试资源都不是充足,测试用例似乎一直都是一种可遇不可求的东西,尤其是在敏捷开发的趋势下,产品功能变动快,很少有测试会一直去维护那个最初的测试用例,往往是写过用过就再也找不到了。但测试用例在重构这个场景下,真的非常重要,他解决的核心问题是把测试同学拉到重构的质量保障中,一起梳理老的逻辑。这份宝贵的测试用例,可以成为你自测的依据,也可以为你提供对于同一个功能的不同视角,如果你通过代码看到的是实现细节的逻辑,那测试看到的就是整个链路的流程图。很多中后台系统都有管理态和运行态之分,管理态,前端是非常熟悉的,但是运行态,测试往往更加熟悉。
六、自测
拿到测试用例,你就可以自测了,但是这里有个坑,就是如果你完全依赖测试同学给你的测试用例。只要保证测试用例验证通过就行了,这种想法会出大问题,因为负责这块功能的测试可能是个新手,可能并不是一直负责这块功能的测试,他们的测试用例可能只是浮于表面的。所以你需要把通过代码考古发现的测试用例里没有的逻辑,暴露给测试同学,并补充到测试用例里。并且如果发现有一些看不懂的逻辑,就应该搞懂他,那些你不懂的死角,往往上线后就会有大问题,不要心存侥幸。自测非常重要,但是往往你会觉得开发完了,就算是把这个事做完了,然后就去忙别的事了,并没有好好的自测,心想还有测试呢,等他们提问题,我再改吧。这是一种很普遍的程序员心理,其实很难避免,毕竟事情有很多。这个时候你可以找同组的开发同学帮你点一点,先解决那些显而易见的问题,也算是一个认真负责的程序员了,不要让测试同学给你提太多低级 bug。
七、回归测试
能有测试同学帮你做功能的回归测试真是一件可遇而不可求的事,一定要珍惜,拿出你的大块时间配合好。这其中最重要的就是多交流,测试同学也不一定知道所有的逻辑,在做回归测试的时候,就需要开发和测试反复核对每个逻辑死角,弄清楚,才敢上线。当然,能够有测试帮你回归的功能都是极易引起故障的功能,这里就有一个技巧就是如何拉测试参与你的重构中来。像这样重要的功能如果测试知道里面的逻辑,你可以怀着请教的心态去问对方,如果对方并不了解,那你就可以讲给他听,一个负责任的测试,应该都非常想了解自己负责系统的重要模块的来龙去脉。
八、灰度发布
即使你做了再多的测试,都有可能有没有考虑到的遗漏点,这个时候灰度就非常重要了,灰度就必须要有灰度工具才行。重构一般是以页面或者区块为粒度按照人来进行的。所以你的灰度工具必须要包含这些功能:
配置用户或者用户组 配置老路由和新路由 配置灰度状态提示 新老页面的自动打点
灰度配置页面,新老动态路由的参数需要保持一致,这样才能把参数传递下去。展示灰度提示,并提供一个快速「返回旧版」的按钮,为了更快速解决问题,可以给出开发者联系方式。当用户访问老路由的时候,按照灰度配置验证当前用户是否在灰度中,如果在灰度中,则立即跳转到新的路由,并显示灰度提示。如果重构的是页面中的区块,则可以提供灰度命中的方法,在页面调用区块的部分做判断。
灰度策略可以按照以下用户级别分布进行:
L1:所有项目开发,测试,设计师,内部运营人员 L2:核心用户,建立钉钉群,观察用户反馈,及时解决用户问题。 L3:适当加入更多用户,直到全量后,删除灰度策略的配置。
发布后,注意观察打点数据:
打点的时候需要注意,要按照动态路由来打点,并分成命中灰度的,点击使用旧版的,不在灰度内的三个维度来看数据,同时每天调整灰度用户,这样就能保证页面是有人用的。如果有很多用户使用了返回旧版的功能,那你就得找找这些用户了解下情况了,到底是有 bug 还是交互不舒服,一对一的解决用户问题,在反复去优化你的页面,慢慢扩大用户灰度范围,直到老的路由访问数据 PV 为 0。
九、全量上线
全量上线并不是灰度所有人,而是真正下线老的页面,并删除老的代码,只有到这一步才算重构完成了。
十、总结
经历千难万险,你终于把重构好的页面上线了,经历了这个过程,感慨良多,只求以后再也不要做重构了,好好做需求不香吗?后头看看整个过程,要想重构的页面上线,不仅要下苦功夫,还要克服人性的一些弱点,要做到这几点:
Double Check:让其他人参与进来,多一个人就能帮你发现更多问题。重构面前,不要相信自己,相信伙伴。 逻辑无死角:不要还有不懂的代码,不清楚的逻辑,按照程序员的第六感,不确定的都会出大问题。 集中注意力:重构不能碎片化进行,要集中大块时间来做,并一做到底,不然过个几天,你自己的代码都会不认识。 一跟到底:开发完成不是重点,全量上线,并下掉老的页面才是结束。
致敬每一位重构路上的勇士。