好的重构与不好的重构

4,151 阅读16分钟

一直以来,我的心头都萦绕着这个问题。如何做好重构?

程序员们热衷于重构。重构可以彰显技术强项、可以优化软件性能、可以让软件的制作更具有美感、可以让程序员更快乐。一想到把糟糕的代码变成优美的代码,无数程序员就心里小鹿乱撞。

然而,业界充满了《XXX最佳实践》的文章、书籍以及网课,但是并没有太多关于如何重构的系统性的讲述。我唯一知道的,就是《Refactoring》,来自ThoughtWorks的大师Martin Fowler。而这已经是一本快20年的老书了。虽然书中很多东西都是经得住时间考验的,但是毕竟基于Java、基于单体软件、基于前互联网时代的商业假设。里面很多具体方法不能直接套公式了。

读《XXX最佳实践》就像看短视频里面的帅哥美女,除了让我们对生活中具体的人产生好不实际的幻想以外,没有什么指导性意义。而我们作为软件行业从业者,需要的是美妆教程。再不济,也至少需要医美种草。现实世界不是一开始就美的,而是通过很多必要的工作,才美起来的。

我们需要知道如何将糟糕的代码变成好的代码。仅仅知道什么是好的代码是不够的。


我曾经是这样的,想必你目前也许或曾经也是这样的。那时我才开始工作,面对一些比较糟糕的代码,就开始抱怨吐槽,甚至咒骂前人的智商。当然,我是一个会抱怨的行动派。我开始了我大刀阔斧的改造。虽然中途出现过一些因为改造而带来的短暂bug,但是经过近一年的不懈重构,软件终于变得更让我开心了。但是我的工作量并没有因此变少,软件的整体质量也没有根本性的提高。

一般人到了这个阶段,意识到了重构带来的巨大成本和微薄回报之后,一般就会收起她的锋芒,开始做个划水人。做任务点到为止,不要去触碰那些自上古时代以来就被降下禁忌的代码。

这样的觉悟也许可以成为一个可靠的员工(如果其他方面比较优秀),但终身无法窥视大道。想要成为神海境强者,必须要更够构筑体内世界,并且在有必要的时候重构体内世界。易筋洗髓,宛若新生。


人类已经进入到了一个复杂时代。任何一个领域都复杂到了任何一个人都不可能穷尽其知识、完全其体验的地步。在这样一个时代,做减法比做加法显得重要得多,比以往任何一个时代都显得重要得多。

哪怕用所有时间来看动漫、看电影,也看不完所有的作品。花所有时间来旅行,也走不完所有的路。

就连娱乐这样轻松的事情我们都无法以一己之力穷尽,试问,我们如何能有“我能将这个软件重构为完美”的愚昧、无知、与狂妄呢?

在知道什么时候应该重构、如何重构之前,我们要知道什么时候不该、不能重构。


不管我们作为从业者、理工人对软件开发赋予多少自身的热爱与高傲,我们都必须认清软件工程与科学的本质区别。科学是关于事实的,不以人的意志为转移,最多是当时那个时代的人总体认知不到而已。

软件工程本质上是工程,是人类活动,而且大多数时候是群体性人类活动。所以,一切群体性人类活动所具备的特征,在公司的代码开发中都会表现出来。

康维早在上个世界就总结了这条规律:软件是组织沟通方式的反映。

而热力学第二定律也告诉我们,一个封闭系统最终会走向混乱。

半罐水响叮当的人也许可以写下《XXX最佳实践》的文章,也可以发布卖出1万份的网课。但是,只有真正的大师,才知道有所为有所不为,才知道如何跨越人类群体行为的天生劣势,去优化一个系统。也只有真正的勇士,才敢逆流而上,去将本来已经丑陋的现实击碎,重新雕刻出自己的美感。


上面说了这么多虚的,现在来点实的。

在考虑重构问题时,我们需要从3个层面去思考。

  1. 组织
  2. 业务
  3. 代码

代码

我们先来说代码,这个最基层的东西。你在网上能轻松看到的《XXX最佳实践》也基本上止步于此。

虽然架构也是由大量的代码组成的,但我故意把这个区分开来。这里仅仅的代码指的是局部的代码,只影响少数业务或者少数基础架构功能的代码。这很难用具体的行数来区分。有时20行代码就改变了根本架构,有时1000行代码仅仅是一个页面。

代码层面的重构必须满足其中一个条件:

  1. 同样的复杂度,但是功能增加了。
  2. 同样的功能,但是复杂度减少了。
  3. 不影响功能、不增加复杂度,性能增加了。(非要说这是降低了时间或空间复杂度)

不满足任何一个要求的,比如纯粹将多行变成一行,将for变成while,把method变成function、将长一点的变量名改为短一点的等不改变任何代码内在逻辑的重构,都是浪费时间。

我公司里有个新人,就喜欢在PR里提诸如此类的意见。和他讨论代码就体验极差。但是我也理解他为什么会有这些想法。其中一个原因就是被网上低质量的《XXX最佳实践》荼毒。知其然不知其所以然。

代码行数的长短并不能直接反应代码的逻辑复杂度。400行的东西并不一定比200行复杂,甚至更加简单也更加可读。可读性也好、复杂度也好,只有在一个量级之差以上,才能存粹以行数来判断。完成同样一个事情,1000行必然是比100行差很多的。

从这个角度讲,重复地抄一些代码是可以的,甚至在不确定的情况下是最好的。重复是最彻底的解耦。

要降低逻辑复杂度,我们基本上可以遵循这个原则:

  1. 分支逻辑变成单线逻辑
  2. 交叉逻辑变成单向逻辑
  3. 将分散的聚合

分支逻辑变成单线逻辑

最常见的分支逻辑就是非常多的控制流,if、else、for、while等。如果可以在保证功能的情况下减少他们的分叉次数,甚至做到毫无分支,复杂度自然大大降低。

异步编程本质上也是一种分支。即某一个控制流从主控制流分叉了。只是这个比一般的if else还要复杂,因为增加了时间与顺序因素。

交叉逻辑变成单向逻辑

如果某两个函数互相调用,这就产生了交叉逻辑。A调用B,B反过来叫用A。更甚者,多个函数交叉调用,形成了一个复杂的调用关系网。如果能变成单向的A->B->C->····,复杂度就会大大降低。

这个在异步编程里对应的,就是微服务之间互相调用,或者一个对象同时是生产者,也是消费者。

有时,分支交叉的程序是不可避免的。但如果能避免,就要尽量避免。

从这个角度讲,代码层面的重构讲究的不是还可以做什么,而是删掉本不该做的事情。

成功的代码重构通常删除的比增加的多,而且让之后的工作变得更简单。

将分散的聚合

这里分享一个最近的例子。我在工作做一个Transpiler的项目。其功能是自动将GraphQL转译为数据库查询语句。这个Transpiler有3个模块,每个模块将上一个的数据结构转变为下一个需要的。之前有一个功能在是实现时,将其数据结构的构建分散在3个模块了。这就导致一个功能需要在多个地方看代码,才能理解其逻辑。而我需要做的新功能依赖于这个功能。如果不重构的话,我也需要将代码分散在3个模块里,开发变得很困难。

所以,我选择了先将之前的功能统一到第2个模块里,那么我的功能就能轻松地在第2个模块里实现了。重构修改了近600(300+300-)行代码,但是新功能只用了约50行。这就是一次成功重构的力量。

业务

上面提到了,分支交叉的程序有时是不可避免的。因为代码不可能比它要完成的业务更加简单。我们总是在完成了业务之余,加塞了不必要的代码。给予无限的时间和一成不变的业务需求,我们理论上可以做到毫无不必要代码。 这也是为什么业界众人会认为做底层技术如操作系统、数据库的人技术更强,而相关代码也更优质。这并不是因为上层应用代码的从业者本质上更愚蠢或者无知,而是因为越底层的需求越稳定,变化的速率越低,所以相关开发人员有更多的时间去完成这些需求。比如最近几年的数据库热潮,不过是使用更好的方法去解决存在了多年的需求。

所以,底层系统开发者的技术能力主要在于他们对这个领域的深刻理解、经验积累。而上层应用开发者的技术能力主要体现在能架构出善于变化的代码架构。从这个角度讲,一个优秀的应用架构师比某领域技术专家更难存在。因为他们即要变中求常,又要以不变应万变

所以,业务的改变直接影响代码的复杂度。如果可以通过业务或者产品功能的一点点小改变,就换取代码的大大优化,这个交易是非常划算的。但是,身为基层程序员,业务方、产品方不见得愿意和你做这个交易。更多时候,基层程序员甚至连参加相关讨论的资格都没有。

当然,这是一个企业文化和管理方式的问题。基层程序员没有发言权,或者发言了声音不被听到,本身是不好的。

所以工程团队的负责人,CTO也好、工程副总也好、小团队的工程主管也好,都需要一个必要的能力,就是和业务方、产品方“讨价还价”。“讨价还价”听起来有点敌对,换一个好一点的词,就是携手并进。

我们先从小一点的产品功能层面入手,如果一个公司是良性的,这种小功能的讨论是基层程序员或者级别不高的工程主管也有发言权的。比如产品经理希望技术团队多做3个界面。不专业的技术团队就去做了,甚至在做的时候还各种吐槽产品经理很蠢。但是,一个优秀的技术人可能会发现直接做这3个页面会让代码实现变得过于复杂,如果要重构来降低这个复杂度就无法按时完成。善于沟通的技术人就会去和产品来讨论,挖掘其本质需求是什么。也许最后发现不需要3个新页面,只需要1个,只是这1个的设计需要做出调整。如果能把技术问题转移为产品设计问题,就不要作为技术问题。

很多时候,人们告诉你他们需要的,仅仅是他们认为能够满足他们需求的其中一种方式,但不是唯一的方式。而且在一个快速迭代的产品里,产品经理率先想到的方式通常都不是最优的方式。有时多想一下,就可以少做很多。很多时候甚至可以达到较好的技术实现和产品需求上的平衡。

但如果你很不不幸身在一个技术人毫无发言权的组织内,你就不要去妄想通过业务改变而带动重构了。

组织

一个公司的组织架构代码架构是一体两面。大重构通常会牵连组织架构层面的改动。一般的基层程序员通常不会接触这个层面的重构。

国内同学比较熟悉的,比如几年前很火的技术中台,不仅仅是一个技术的重构,是一个直接影响整个组织行为的大调整。这里面被引用最多的可能就是阿里的技术中台了。技术能够使以前不可能的组织形式变得可能,但是反过来固化的组织形式也会限制技术的发展。

如果你开始接触这个层面的工作,就不能就技术论技术、就业务论业务了。或者说,处理这个层面工作的人,必须能够综合地、辩证地看待技术、组织、商业等问题。

但是,再某些特殊的时候,基层技术人员也可能遇到因为组织架构而带来的技术障碍。这个时候就要思考你以为的那种重构方式,是不是也有同样的障碍。


那是一个2018年的夏天,我入职了一家电商公司的API中间件团队。这个团队维护了一个GraphQL的开发框架。不了解GraphQL的读者可以将其理解为一种RPC技术。

行业普遍来说,是后端开发者提供GraphQL,供前端使用。因为GraphQL本质上是一种后端技术,代码是运行在服务器的。业界很多人误以为GraphQL是前端技术,因为有很多JS客户端工具。

但是在那一家电商,变成了前端开发者来写GraphQL。更甚者变成了我们团队自己写GraphQL API对接每个业务逻辑。这确实是一个糟糕的开发方式,某种意义上,工程团队作为一个系统,找到了一个Local Max解,这个解不是全局最优的,但是也没办法自动调整了。当时我做了很多工作试图让后端来写GraphQL或者至少让前端自己写更多的GraphQL,而不是我们作为框架维护者同时还来做业务逻辑开发。但最终没有成功。这是因为我没有意识到这件事情的组织障碍。

这个电商公司一开始是传统php模板网站。后来才有了Java等后端程序。但是将HTML网页和后端业务逻辑分离是2015年才开始做的工作。所以JS工程师是一个2015年以后才出现的工种。因为是电商,所以搜索引擎优化很重要,所以SSR(服务端渲染)是必要的。这就使得虽然前端是用React写的一个单体应用,但是部分逻辑其实是在服务器端运行的。只是代码在架构之初,故意模糊了到底哪些代码在服务端渲染时会运行,哪些代码只在浏览器内运行。这样做,至少当时的一个论点是,可以让业务逻辑开发者不去想SSR的事。

而18年的时候,最成熟的GraphQL解决方案是JS的。那时这个电商公司并没有很好的服务器运维能力,所以我们团队为了做GraphQL,就只好把自己的类库嵌入到前端代码库里,然后在SSR阶段运行,也就是在前端的渲染服务器内运行。站在后端业务开发者的角度,他们自然地会认为,既然你的代码都在前端代码库里,那么自然就是前端开发者去写。

这就是我们当时推行GraphQL重构最大的障碍。很多公司的工程团队是以代码库分地盘的。这个代码是我的地盘,那个代码是你的地盘。这是人类自然而然产生的观念。无视这个现实去想当然地推广一些东西当然是自讨苦吃。


有些很天真的程序员就会给我说:那以后我有公司的话,就会避免这样具有地盘意识的人进入我的公司。只要这些人都如此这般满足种种条件,我们就不会出现这样的问题了。

这种想法就和“如果世界上都是清官,就不会有贪污了”一样不具备指导意义。

我们要思考的是,如果90%的程序员都是这样具有强烈地盘意识的,那么重构如何进行?

一切重构都要先认清组织的现实情况,然后顺应人性,才有可能调动其他人的积极性去完成你的目标。

当然,对于那一家电商公司的具体案例,只有CTO级别的人才有足够大的影响力去重构了。只是他们需要重构的远不止我们那个团队的东西。

所以,如果你发现自己作为一个基层处在类似的情况,就不要去重构。如果不重构就会让你的工作生活变得极其痛苦、心生厌恶,走为上策。有效的重构仅仅能发生在你能影响的范围内。

结语

这个话题非常之庞大,微观上涉及到代码的具体写作,宏观上牵扯到组织的架构形式,主观上依赖于人的审美与认知,客观上又取决于实际的需求与资源。

希望本文能对你有一些启发,开始逐渐认识到什么东西能重构,什么东西该重构,什么东西最好不要重构,什么东西重构了也没用。