原文发于公众号“百川海的小记”,一个菜鸟的自留地,欢迎关注讨论
写在前面:
1.代码的改写从大范围到小范围大致可以分为四级:系统级别,功能级别,代码级别,机器级别;
2.代码级别以下改动可视为“重构”,功能级别以上级别只能视为“重写”
3.重构是持续的日常过程,而重写不是
###############正题##################
对于一个开发人员来说,或多或少会遇到下面的这样的场景:
1.大佬:XX调度模块跑得太慢了,胖海你这两天把它重构一下,让它跑得快一点
胖海:是是是,大佬说得对
2.贾苟施:XX系统的底层太臃肿了,部署也太麻烦了,我们用springboot重构一个新系统吧
胖海:是是是,您说得对
3.胖海:#@#*&@!#$,这代码谁写的,存心让人看不懂啊
劳元宫:这是小坑写的,他几个月前已经离职了,不如你把它重构一下吧
胖海:是是是,您说得太对了
在这些场景里面,我们都提到“重构”,但是显然,它们的意思并不一致。在我们日常的语境中,不少人把“重构”看作对于一切代码改写的一种称呼。那究竟“重构”是一种什么动作呢?
重构与重写
在经典书籍《重构-改善既有代码的设计》(下称《重构》)中,Martin Fowler有以下的描述:
『所谓重构(refactoring)是这么一个过程:在不改变代码外在行为的前提下,对代码做出修改,以改进程序的内部结构。重构是一种经千锤百炼形成的有条不紊的程序整理方法,可以最大限度地减少整理过程中引入错误的几率。本质上说,重构就是在代码写好之后改进它的设计 。』
—— Martin Fowler
简化一下这个描述,Martin对“重构”的理解是:
重构是一个过程;
重构不改变外部行为;
重构改进内部结构;
重构的对象是代码;
重构有一套稳定的方法。
《重构》对于这个问题的,给出的回答是十分严格的。Martin在书中对重构对象的范围限定,基本可以理解为单线程的,代码库完全独立的软件项目。直到今天,这个依然是对“重构”的一个非常精准而具体的范围定义,而且在业界的话语语境中,可以说是一个标准的说法。但是,这放在今天的软件系统项目中,这样的定义却显得非常的苛刻了。事实上,我们在工作中挂在口边的重构,和这个定义本身就有很大的差别。那么,问题出在哪里呢?
如果分析我们的软件,任意一个软件总是包含若干特性的,即若干区别于其他软件的表现特征,比如软件的可移植性,软件功能,软件性能等。小至Hello World,大至各类互联网商用的综合系统,一切软件都有其特性。而“重构”的行为本身,必然是对任何特性无损的。任何对于软件特性有所改动的操作,严格来说都不算“重构”,我愿意将这些操作称为“重写”。重构与重写的界限,在于是否可能对软件特性造成变动。
改动范围与级别
在一个软件项目中,对于代码的改写,有大范围的批量改写,也有小范围的局部改写。我列举以下几个用例:
用例一:将一个以PHP为语言开发的系统,功能原封不动地用Java重新实现
用例二:为了准备即将发生的业务拓展,对一个支付接口进行改造,将其从固定面向单一支付渠道“X付”,改为可配置、可插拔地面向多个支付渠道,但当前依然只接入“X付”
用例三:将某个连续使用上百个if else语句的方法,改为使用卫语句进行实现,以增加其可读性(这一改动范围,与《重构》中的描述的重构方法影响范围基本一致)
用例四:编写一个方法,使得CPU运行使用率-时间曲线在排除其他进程的影响的理想情况下,呈正弦函数曲线(本问题重述自《编程之美》)
在以上四个用例中,从功能上来看,我们都不难划分出改动的影响范围和功能使用者/调用方。我根据四个用例改动范围的大小,拆分出四个等级。
| 用例 | 级别 | 调用/使用方 | 说明 |
| 一 | 系统级别 | 系统使用者/业务方 | 直接面向使用的业务人员 |
| 二 | 功能级别 | 功能接口调用方 | 面向调用接口的模块(第二/第三方) |
| 三 | 代码级别 | 调用该方法的上级方法 | 面向内部调用方法 |
| 四 | 机器级别 | 硬件运行指令 | 面向硬件/指令集 |
如果单纯从功能上来看,我们都可以将改动的部分视作黑盒,但是从其他方面来看,随着黑盒的变大,黑盒本身便越发容易松散。改动范围越小,改动用例越靠近底层,外部的影响则越小,反之亦然。
如果改动是通过机器语言直接实现的,除非将硬件的结构或者内置指令集进行修改,外部无法动摇该软件的任意特性;
如果仅通过高级语言进行一段代码语句结构的调整,而不改变运行逻辑,其特性也相当稳定;
如果再向上一个级别,以功能为改动维度,我们发现功能模块的实现已经不能可靠地剔除其他代码的影响,比如第二方的模块引用,第三方外库,直接或间接引用的其他计算资源……毕竟对于一个功能的实现,可以引用的资源是不受限制的。
到此为止,我们明显看到稳定特性的黑盒已经被戳的满是洞洞,无法维持内部与外部的清晰界限了。如此看来,《重构》将重构的方法的范围限制在代码级别,确实是经过深思熟虑的。而超出了这一级别的改动,则只能被视为“重写”。
重构是一个过程,而重写不是
重构是一个过程。我们强调这一点,是为了避免一个误区:我们在按照设计进行一次重构后,可能会以为重构已经结束了。然而,真实的情况是,重构是没有终点的,除非一个系统随着业务价值下降而被完全放弃维护。开发人员应该持续地发现代码的不足,并且设法优化这些不足。哪怕我们不能马上实施这些优化,我们也必须清晰地意识到,这是我们应该做的,重要而不紧急的事情。一方面,这是为了软件质量的可持续维护,另一方面,也是为了保证开发人员对于“坏味道”的嗅觉。
而重写则不一定可以被视为过程。重写面向的是功能以上范围的变动,对外部特性的并不封闭(甚至在工作中经常是通过外部引用资源的修改来达到优化功能的目的),因此客观上也难以阻止错误引入,而且测试上也无法完全覆盖。因此对于重写,必须是有计划性的,同时只要我们进行重写,我们就必须为错误的引入做好准备,功能的补偿、数据与日志的监控都必不可少。