重构是什么?为什么?什么时候该做?
何谓重构
在不影响原有行为的前提下,对既有代码设计进行调整,使其更容易理解,更容易修改。
为何重构
重构改进软件的设计
经常性的重构有助于代码维持自己该有的形态。很多情况是代码一开始设计的很好符合当时的需求,但随着新增预想不到的需求、没完全理解代码设计去修改代码等情况增加,会使得完成一件事情所需要的代码越来越多。
改进的一个重要方向是消除重复代码,其表现是我在这里做了点儿修改,系统却不如预期那样工作,因为我没有修改另一处。
- 不同函数有完全相同的语句做同样的事,而你只修改了一个函数。
- 新增功能的配置太分散,你需要在多处新增分支来加入新功能
重构使软件更容易理解
再精巧的设计,如果阅读你代码的人看不懂,别人任何一处改动将破坏你原先巧夺天工的设计,更何况这个别人通常是几个月后的你。
将需要记忆的部分,写在代码里,让你大脑减负。(比如前面提到的新增功能需要同时修改很多处地方)
重构帮助找到bug
好的代码能让你更容易去debug,这里举最近遇到的例子
- 比如游戏中模型的位置和角度不如预期,你需要去找哪里修改了transform,如果有个通用的接口去修改,你只需要去追踪接口的调用栈。但如果很多地方另起炉灶,实现很多不同的修改transform接口,将耗费更多的时间去定位。(有时候只能等灵光一现)
- 传递的数据有问题,如果有统一的地方管理数据的修改,定位问题也会很简单。但如果到处都能修改数据,到处都能传递数据,如同蜘蛛网般的结构,正如著名诗人那句剪不断理还乱
重构提高编程速度
没有维护过的架构到后期经常会出现需要花越来越多的时间去考虑如何把新功能塞进现有的代码库,不断蹦出来的 bug 修复起来也越来越慢。代码库看起来就像补丁摞补丁,需要细致的考古工作才能弄明白整个系统是如何工作的。这份负担不断拖慢新增功能的速度,到最后程序员恨不得从头开始重写整个系统。
需要添加新功能时,内部质量良好的软件让我可以很容易找到在哪里修改、如何修改。良好的模块划分使我只需要理解代码库的一小部分,就可以做出修改。如果代码很清晰,我引入 bug 的可能性就会变小,即使引入了 bug,调试也会容易得多。
何时重构
第一次做某件事时只管去做;第二次做类似的事会产生反感,但无论如何还是可以去做;第三次再做类似的事,你就应该重构。事不过三,三则重构。
预备性重构:让添加新功能更容易
添加新功能一个好的习惯是
- 先阅读原有代码库是不是已有相关逻辑或者是否能通过已有逻辑组合来实现新需求。
可能会出现的情况
- 已有逻辑和你的需求有细微不同,一般是修改已有函数参数去扩展,又或者新增一个重载函数,部分逻辑调用已有接口
- 不要重复复制原有函数逻辑,这会坑了未来的你和别人
帮助理解的重构:使代码更易懂
如果不想你精心设计的代码被后来人没有充分理解的情况下破坏了,那就应该写的更易懂。常用的手段有
- 拆分长函数
- 修改变量名/函数名
- …
捡垃圾式重构
摇曳露营里提到:至少要让营地比你到达时更干净
有时看到一段代码确实比较难忍,但是需求排期又比较紧。细微的重构可以先做了,比较棘手的可以先留下一处TODO
有计划的重构和见机行事的重构
- 前面提到的三种重构时机都是见机行事的重构,属于顺手捡垃圾那种程度,不会影响已有需求的排期,又能让你更愉快地写代码
- 如果代码出现了上面说的剪不断理还乱的情况,这是一个警告,意味着是时候该进行一次大型重构。
- 只要日常开发中有重构意识,大部分重构应该是不起眼的、见机行事的
怎么对PM说
“重构”被视为一个脏词——经理(和客户)认为重构要么是在弥补过去犯下的错误,要么是不增加价值的无用功。如果团队又计划了几周时间专门做重构,情况就更糟糕了——如果他们做的其实还不是重构,而是不加小心的结构调整,然后又对代码库造成了破坏,那可就真是糟透了。
我觉得只要说清楚重构的必要性以及能带来什么收益,一切都是可以聊的。最好能带上一些量化的数据和案例,做完这次重构我们新增需求能节省多少时间之类、减少BUG的排查时间。
何时不应该重构
- 没有人需要去了解代码的工作原理,它被API好好保护着时,就没有必要去浪费精力(比如一个大型函数,你不需要对其新增功能
- 重写比重构快的情况,比如一些比较抽象的代码,你根本无法理解
重构的挑战
延缓新功能开发
- 有足够的理由和数据去说明重构带来的收益:添加功能更快,修复BUG越快
- 不重构无法进行新功能的开发
- 重构完可以通过配置组合已有代码,快速实现新功能
代码所有权
你无法修改那段让你恶心的代码或接口,因为他不只你一个系统在维护,或者被封装起来导致没有修改权
- 如果是接口已经派发出去,重构接口名或参数会影响很多第三方使用者,可以新增一个接口,将旧的接口保留并标记为过时,慢慢去过渡新接口
- 没有修改权的情况,提交修改需求或者提交修改代码去让相关方审核
分支
如果多个人有自己的特性分支,这种情况下做重构确实会影响很多人。即使你经常拉取主分支修改,但是其他第三方分支对你来说是不透明的,一但你的重构提交上去,将会对其他分支造成大范围的冲突,这将严重影响其他人的工作。
解决的思路:让特性分支存在的时间足够短,或者同时只存在一个特性分支
书中给出了持续集成(Continuous Integration,CI)的方案,CI的核心目的就是让特性分支存在的时间足够短,每天都把特性分支的修改提交到主干分支,让其他人提前知道你的改动,谨防滚雪球效应。但如果你不希望主干分支受到你未完成的功能影响,这时候就要使用一些开关来让主干分支不会运行到你的未完成代码。这对自我测试比较苛刻,你要时刻记住你提交的代码会对主干造成什么影响。
重构与性能
除了对性能有严格要求的实时系统,其他任何情况下“编写快速软件”的秘密就是:先写出可调优的软件,然后调优它以求获得足够的速度。
对于游戏来说,在update运行的逻辑需要谨慎对待。其他情况下更应优先考虑代码的可阅读性,再来考虑性能。很多时候你预想的性能优化过的代码可能根本都运行不到几次,更应该使用性能分析工具去分析热点代码,再来逐一击破。
自动化重构
现代代码编辑器都有重构代码相关的工具,方便你去替换函数名和变量名。
- 但在像lua这样的弱语言还是要谨慎,避免把其他不同table同名的函数给修改了
- 尽量避免多个功能模块使用了相同同名无意义的名字,会给名字修改带来麻烦。
- 使用Emmylua的标记功能来减少重构难度
最近见机行事的重构
有A模块和B模块,A模块有个地方需要根据B模块逻辑特殊处理,一开始是使用A模块去if判断B模块是否处于某个状态的特判代码,后面觉得不妥,还是改成A模块本身支持处理不同情况,由B模块去切换A模块运行模式,避免A模块去依赖具体的业务逻辑。