网络知识 娱乐 阿里巴巴专家:重构失败后,我终于读懂了《重构》

阿里巴巴专家:重构失败后,我终于读懂了《重构》

在程序员的圈子里,流传着一句黑话:“重构祖传代码如迁祖坟,稍有不慎则万劫不复。”

这个世界上总有几个自命不凡的黄毛小子,在接手公司业务代码的第一天就萌生了“重新设计,流芳百世”这种容易遭雷劈的念头,我就是其中之一。当我经历了一次失败的重构项目之后,二刷《重构:改善既有代码的设计》这本书时,不禁老泪纵横:答案都在,只怪我当初没读懂。

今天我打算写下我这次重构的血泪史以鉴后人,以及我再读《重构》的感悟。

阿里巴巴专家:重构失败后,我终于读懂了《重构》


01

重构失败:开着飞机换引擎

在刚接手公司的搜索业务时,我毛遂自荐,试图承接整个搜索引擎业务代码的重构工作。正如《重构》的副标题所说——“改善既有代码的设计”,我的重构目标就是“简化业务代码中盘根错节的复杂关系网,实现核心功能的算子化”。一方面,可以实现通过利用既有的算子快速实现重复性的业务逻辑,另一方面,可以帮助新手和后来者,使代码更加友善,更易于快速上手开发。

然而,理想很丰满,但现实很骨感。开工不到一个月,我就感受到了始料未及的巨大压力:

首先,业务绝不会等你。在重构的同时,同事新开发的业务需求代码也在不停地集成到主干分支上,并且与我的代码风格迥然不同。

其次,深度学习模型与搜索引擎的捆绑,大大增加了重构的难度。一个相同的搜索请求,在不同的时间段,可能会得到截然不同的结果。这对测试用例的批量回归造成了巨大的压力。

彼时彼刻,我终于体会到了“开着飞机换引擎”这个比喻的真实含义——极度的危险,只有在濒临“坠毁”的时候才能体会到。果不其然,在第一版重构代码完成时,它的效果严重偏离了预期。我的“战果”也十分“显赫”——删除了近千行无效代码(约为重构前的十分之一),实现了三个通用算子,以及重写了部分晦涩难懂的业务代码。整个代码库看起来依旧臃肿不堪,持续维护难度仍然很大,其中还暗藏了一颗让代码四个月后需要紧急再次重构的“恶性肿瘤”。

毫无疑问,这次“开着飞机换引擎”的重构彻底失败了。

02

重构的源动力:问题驱动

重构失败之后,我一直在思考“重构”的源动力:在一个团队或者一个项目中,启动重构的充分必要条件到底是什么?

在《重构》这本书中,有这样一句话,“如果丑陋的代码能被隐藏在一个API之下,我就可以容忍它继续保持丑陋”。而我也终于理解了这句话背后的深意:不要以个人的代码审美来驱动重构。再引申一层,即不要为代码过度设计它短期内还用不到的功能。

那么,到底什么才能驱动重构呢?最好的模式,应当是“问题驱动”。在维护一套“祖传代码”时,往往会在团队内部先积累起问题。当问题积累到一定数量时,我们才会发现其中的问题模式。此时,我们需要做的,不是下达重构命令,而是将问题归类和抽象。在《重构》一书中,作者将代码问题归结为四大类:设计模式落后、理解困难、bug难以定位、迭代速度缓慢。只有将实际工作中遇到的问题归到四大类之中,我们才能从问题的本质出发,设计有针对性的重构解决方案。

找到了问题,找到了动机——这样就足够了吗?显然不行,我们还需要找到最正确的人。

在大多数人的观念之中,主导重构项目的负责人必然是工作年限长的、经验丰富的、代码能力强的。事实果真如此吗?代码能力固然重要,但在互联网这类非传统软件公司,对业务的理解才是重构者的核心素质。

唯有对公司的核心商业诉求有着最深刻的理解,才能在这个理解的基础上,对“未来”的业务需求做出合理的预判,从而在重构进行中预留出兼容业务迭代的空间。

03

重构进行时:敏捷开发中的重构指南

如果你还在认为重构的任务就是逐步替换主干代码,那你就大错特错了。任何代码库级别的重构,都无法一蹴而就。要想完成重构,必须先完成对重构的任务分解——以蚂蚁搬家的方式逐步完成整体重构。《重构》一书中认为:代码重构的第一块基石是自测试代码。可靠的测试框架是重构推进和验收的重要保障。

除了构建测试框架,重构工作的内容还包括问题分析、架构设计、节奏安排、代码开发和集成。这样看来,一次成功的重构也极度依赖于精细化的过程管理。

在常规的研发团队中,很多时候新功能或新需求的开发都采用了分支开发和定期集成的模式。展开来说,就是开发者从主干代码拉取一个新的独立分支,在这个分支上进行开发。当分支代码功能通过测试后,开发者再将分支代码向主干代码合并。在日常的工作维护中,这种开发模式可以帮助团队保障主干分支的稳定性,但在“重构”的特殊时期,这种开发模式往往会为重构造成诸多障碍。

原因之一是,重构代码的改动量十分巨大。在每次重构完一个完整的功能函数后,再向主干集成会产生巨大的代码差异,给代码评审、功能测试造成巨大压力。

原因之二是,其他非重构人员也会持续向主干集成,但他们大概率不会遵守你的“重构代码规范”。我对此就深有体悟,但凡有其他人员完成了他们的集成,我就需要再一次重新获取主干代码,修改他们新集成的功能以符合我的重构设计和规范。

针对此类状况,《重构》一书中给出了一个很好的建议,即重构者在主干代码上直接进行开发,并且高频率持续集成。举个列子,当一个重构者重写了一个函数,并进行单元测试通过后,就直接将改动集成至主干。这样一来,不仅可以让系统每次的集成压力大大减小,其他人也需要在集成重构后的代码上,被“强制”按照重构后的代码规范和功能设计持续开发。

04

重构常态化:团队代码公约

在《重构》的2.3节,提出了这样一个观点:“程序的内部设计(架构)注定会腐败变质。”这是由于重构不是一个人(重构者)的工作,而是整个团队共同的责任。只要在团队中存在一个不遵守开发规范的成员,就会极大地加速代码质量的腐败变质。

俗话说得好,“被误解是表达者的宿命,被理解是表达者的使命”,这句话在程序员圈子中依然适用。如果把程序员看成机器程序命令的书写者、机器生命剧本的创作者,那么他所表达的对象就有两方,即对应的机器和其他的代码维护者。即便一个重构者完成了他全部的代码重构工作,只要团队内有其他成员没有充分地理解他的新设计和意图,不受约束地增加代码,就会导致重构后的代码在极短的时间内腐败变质。

在我的重构经历中,失败的一个非常重要原因就是,我与当时的团队管理者并没有就重构理念的核心细节在认知层面达成一致。这导致我作为重构者完成重构之后,好不容易建立起来的代码设计模式,被团队的其他成员以新功能、新需求合并的方式冲垮了。

因此,延长代码重构“保鲜期”不仅是重构者的任务,更应是团队管理者的责任。团队的管理者不仅要对承担起重构理念传播的责任,即向所有刚接触到重构后代码的开发者普及重构理念,更要重视对代码评审的把控以及代码公约的制定。而这里所说的在日常迭代时严把代码评审关,建立良好的代码公约以及自动化的公约监测能力,其实就是“重构的常态化”。

唯有常态化重构,才能让团队的每一位开发者都能在日常开发中及时发现代码的“坏味道”。只有团队代码公约实实在在地落到文档之中,才可以让重构后的代码更容易被团队的每个成员“理解”。

05

阅读《重构》:一种架构思想

每一个研发团队的管理者都应该通读《重构》的前两章,充分理解其中的工程管理思想。阅读《重构》,可以让管理者学会如何使用高成功率的重构方法(例如节2.3至2.7),了解研发过程、管理阶段的智慧(例如53页的长期重构思想),让团队能够持续产出灵活性大、可读性高、稳定性强的代码。

而每一个软件开发人员都应该详读第3章之后的内容。这些内容不仅展示了一些重构的技巧,而且可以帮助开发人员养成良好的开发习惯和设计思维。例如,在书中8.1节就讲到重构中的函数搬运:“如果一个函数对自身上下文元素关注很少,但频繁引用其他上下文中的元素,那么它就需要被搬运到与其上下文关系密切的代码模块中,实现更好的模块化。”这其实就是在对软件工程当中非常抽象的思想“低耦合、高内聚的基本原则”的实操化与具象化。如果开发人员可以在日常的工作中遵守这本书中提出的实操性规范,减少随意性,以8.1为例,为函数选择合适的上下文环境,那么他在重构时就可以相对轻松些。

文章的最后,我想引用乔梁先生(《持续交付2.0》作者,腾讯外聘高级管理顾问)对本书的解读:“对软件工程师来说,重构并不是额外的工作,它就是编码本身。”重构真的不是一个突发的、额外的项目,而是一种需要持续坚持的架构思想。与君共勉!

作者:付聪(浙江大学CAD&CG国家重点实验室博士,南加州大学访问学者,TPAMI/KDD/TKDE/EMNLP审稿人,阿里巴巴算法专家)

审校:栾传龙

—END—