重构剑法之山重水复疑无路

这真的是一篇正经的技术文章。 —— 鲁迅

楔子

是夜,一轮明月挂在树梢,微风轻拂,掀起淡淡花香。

江边有一艘小船靠岸停下,烛光悠悠。之前吹笛的白衣少年,此刻已起了倦意,合上了眼眸。然而夜晚并不似表面这般平静,一股杀机正从江边小树林中悄悄蔓延出来。定睛一看,一名黑衣男子正朝着少年的方向潜行过来,妄图趁白衣少年不备,就要使出一招吸星大法。

眼见那男子快要得逞,殊不知白衣少年早已用“委托”和“代理”心法,将自己七魄中的灵慧移到了鞘中利剑。还未等男子靠近半分,“咻”的一声,宝剑自行飞出,带着凌冽的剑气直指黑衣男子而去。心中暗道“不好”,黑衣男子旋即放弃进攻,抽身后退。“你在等我?”,刚站稳脚跟,男子的语气中夹带了些许诧异,显然没料到这是少年故意布下的局,局中先前隐匿的白棋一一浮现,杀气四漏。

“不,是在等一个将死之人”,少年睁开了眼睛,收回宝剑。男子闷哼一声,随即不知用了什么奇门遁甲之术,将自己的身形隐去,百般招式接连不断向少年扔来。少年微微皱眉,未见犹豫,便将自身内力放了出来,一道“卫语句”屏障浮现在身体周围,使得黑衣男子一时间无法靠近半丈。未等男子发动下一轮攻击,少年一记“断言”剑法朝前方劈出。男子再无所遁形,身法中的破绽也一一展现,每一击都被少年轻松接下。但看那少年,依旧如先前一般,左手握着一支竹笛背在身后,只用了单单右手应对,下盘纹丝未动,好生潇洒随意。

男子面露难色,知道自己凶多吉少,便横下心以燃烧生命为代价,使出那“杀敌一千自损八百”的秘技,硬生生将自己的内力提高了好几个层次。手中的动作也变得迅猛狠辣起来,上中下三路齐出,妄图打他个措手不及。但看那白衣少年神色坦然并不慌忙,左手“函数式编程”,右手“策略模式”将男子各路套数纷纷化解。末了,少年还不忘喂给男子一剑。这看似简单的一剑,却蕴藏了无穷的内劲,缘是那少年使用“继承”和“封装”,借助“模板方法”将万般武学精华融进了这一招式中。这一剑直中要害,打的男子元神俱灭,卧地不起。

事罢,白衣少年重新点燃了刚才打斗中熄灭的烛火,拂衣而去,留下已无还手能力的男子任他自生自灭。“这,是什么剑法?”,躺在地上的男子呼吸渐失,问了自己在这世上的最后一个问题。“重构”,一声回复从远处悠悠传来,碧波荡漾,岸边已不见之前停靠的小船。

此刻,一轮红日缓缓升起,照的江面熠熠生辉。

序章

随着对团队项目的理解不断加深,编码也愈发熟练,开发之余便有一些时间去思考项目代码的优化,把自己的一些想法付诸实践。也常常否定昨天的自己,不断进行反思总结,希望探索出更好的设计。

本系列文章便是记录自己从《重构》一书中所学各项技巧在真实项目(以 Kotlin 为编程语言的 Web 后台项目)中的思考和运用。如果其中有一条能给你带来启发,运用在后续的代码编写中,对我来说就是莫大的鼓舞。我非常推荐你阅读《重构》这本书,事不宜迟。

在此非常感谢送给我《重构》这本电子书的引路人 —— 张师傅(公众号:张师傅的博客)

重构定义

关于重构,Martin Fowler 曾在书中这样说道:

Refactoring is the process of changing a software system in such a way that it does not alter the external behavior of the code yet improves its internal structure. It is a disciplined way to clean up code that minimizes the chances of introducing bugs. In essence when you refactor you are improving the design of the code after it has been written.

在不改变代码外在行为的前提下,对代码做出修改,以改进程序的内部结构。重构是一种经千锤百炼形成的有条不紊的程序整理方法,可以最大限度地减少整理过程中引入错误的几率。本质上说,重构就是在代码写好之后改进它的设计。

重构既是让设计贯穿代码的整个生命过程:设计、编写、测试、优化、运行、维护、修改和删除。通过不同的技巧,针对代码某些阶段进行优化,实现正反馈机制,延续代码生命周期。

我们所写的代码直接面向的对象主要有三个:当下的自己、计算机和未来的开发者。

对我们自己而言,短时间内为了开发效率或者赶进度,并不强求代码的简洁和扩展性。根据我自身经历,也不推荐在没有一定经验或者方法时就在开发初始夸夸其谈未来或者扩展性。过度设计是新手比较容易掉入的一个大坑,但黑格尔也曾说过“只有那些躺在坑里从不仰望高空的人,才不会再掉进坑里”。我们需要找到一个调节点,平衡这其中的矛盾,那就是重构。

对于计算机,我们只需要利用编码准确描述自己的意图即可。因为各种缘由,我们时常会表述不清或者思考不全,导致计算机的理解与预想情况大相径庭。多数情况下,如果程序本身的逻辑没有问题,通过重构也许会带来一些性能优化,但通常不会改变其外在行为。

未来的开发者可能仍旧是我们自己,也或许是接手代码的人,多数情况下则是代码调用方。此时,重构的意义得以完整彰显。据说保安有灵魂三问:你是谁?从哪来?到哪去?重构对于我们的代码而言,恰是处理这三个问题的执行者。它帮助我们调节代码结构,使代码更具前瞻性和包容性。它帮助未来的开发者理解代码的意图,更清晰的回答这三个问题。

###重构目的

计算机科学相信所有问题都可以通过增加一个间接层来解决,重构中很重要的部分就是对间接层的管理,视情况进行增加或者减少。

重构不是万金油,能打理程序的方方面面,更不是一颗能让人顿悟编程之禅的银弹,但它可以是新冠疫情下的医用口罩,是 75% 的酒精。对于国家这个庞大的系统来说,它们阻止了事态恶化。对于个人,则成了人与人之间和谐相处的桥梁。总体而言,重构大概有这样一些目的和好处:

  • 重构可以改进系统设计,让其在崩坏之前重回正轨,使每一行代码物尽其用,提高开发人员的重视程度和规范性,减少技术债。通过重构,封装条件逻辑,将变化进行隔离,对扩展进行开放,减少修改代码带来的副作用。
  • 重构可以提高代码的理解性,降低其阅读难度和修改门槛。此外, 适当借助其中的一些技巧,对已有代码按照自己的理解进行修改和验证,也能帮助我们更快理解不熟悉的系统。
  • 重构还能帮助找到程序漏洞,促使开发人员写出的代码更具鲁棒性。一方面,当调用他人的代码出现问题时,势必要借助重构的一些技巧来梳理代码,揪出问题所在;另一方面,不断对自己的代码进行重构,也能在问题发生前让其无所遁形。
  • 重构能减少后期的开发工作和维护成本。很多时候,重构的受益者就是我们自己,编写的代码还是需要我们亲自维护。根据墨菲定律,我们在匆匆忙忙间写下的代码,总有一天会出其不备产生一个隐藏的漏洞,然后夜半三更被 leader 叫起来改 bug。新增一个 feature 时,也会因为糟糕的设计,使得编码道路险象迭生。稍不注意就会跌到一个隐藏的坑中,尽管这个坑是你挖的,但它不会因为你是主人就放你一马。
  • 最后,重构可以提高开发人员的编程能力。曾子曰:吾日三省吾身,通过不断地反思和总结,勇于否定昨天的自己,亦是我们能力成长的一个重要途径。

重构时机

什么时候开始重构呢,一定要掌握相当多的技巧之后才能进行吗?曾经的我一度这么以为,但其实重构的起点非常简单,几乎没有任何门槛。只需要从整理代码开始,给予变量或方法一个合适的名称并将其中拼写错误的单词进行修正,将意图相似的方法移到一起进行管理,把类按照不同层次划分到不同包中,根据 IDE 的提示减少项目中的 Warning,与团队保持一致的编码风格等等。这些都是随手就可以做的事情,花费不了多少时间,也不需要太多技巧。相反,如果我们选择视而不见,总是想着下次再改进,久而久之,代码就会慢慢腐烂,散发出阵阵恶臭,接手的人即使捂着鼻子也不太愿意去修改。

凡是原本可以做得更好的,也算是懒! —— 苏格拉底

美国童子军有这样一条规则:“Leave the campground cleaner than you found it”(离开营地前让它比你到来时更干净)。对于重构来说,亦如此,最好的时机就是当下。重构理应是代码生命过程的一部分,就好比打扫卫生,整理房间是我们生活的一部分,但具体什么时候怎么做,需要靠我们自己去规划,去衡量。值得一提的是,修改并不等于重构,修改有时候是为了性能优化,有时候是响应需求,有时候则是为了妥协。

在着手开始重构之前,先来捋一捋代码的哪些征兆告诉我们项目应该开始重构了:

  • 低级错误:变量命名、单词拼写、代码缩进、注释维护等。对这些点的修改也许还算不上重构,但这些却是我们常犯也常忽略的问题。
  • 重复代码:DRY(Don't repeat yourself)和 Rule of three(三次原则)告诉我们,相似的代码出现两次时无关紧要,但出现第三次时就应该开始重构了。事不过三,代码复制短期可以节省开发时间,但长期这么做必然会加剧维护和修改的工作量,也会降低开发者的热情。
  • 职责繁杂:让一个函数或类承担过多职责,是产生 bug 的主要根源。古代道家哲学就告诉我们“大道至简”,对于一个函数或是类来说,同样如此。我们不希望别人读自己写的函数到一半时就已经不知道函数第一行的目的是什么了。虽然注释能帮助我们读懂代码,但请注意,注释不是除臭剂。只有保持简单性,才可以使函数更易读,易复用,易测试,也易于再次重构。
  • 复杂条件:有着复杂逻辑的代码难以扩展,是大多数程序员的共识。对这类型的代码进行修改,可谓牵一发而动全身。大量的 if-else 头尾相连,构筑出一道铜墙铁壁,给人一副生人勿进的景象。
  • 盲目优化:费尽心思将一个简单的方法从10行代码扩展到了100行,就是为了简化其中的数据拷贝或者别的什么,所带来的效果是将执行耗时从100ms减少到10ms。然而系统的真正的瓶颈却是数据库或是网络请求,这点优化带来的效果微乎其微或者说甚至没有。此时,如果代码变得晦涩难懂,我们也需要考虑是保留还是舍弃本次修改。
  • 出现错误:程序出现错误的原因很多,开发者本身没能完整正确理解自己的设计是其中占比较大的部分。这样的设计,又这么放心让别人去使用呢?亡羊补牢,未为晚矣。有些错误,如果我们能及时对代码进行重构,不仅能对系统进行修复,也可以防患于未然。
  • 过度设计:对未来考虑过多,不仅让设计者忧心忡忡,设计出的系统随着版本迭代也可能会成为累赘,没能发挥应有的作用。凡事讲究适可而止,当学会运用重构这项工具时,我们大可放松心态。考虑未来的同时,更着眼于当下,降低系统的复杂性,同时坦然迎接未来可能的修改。
  • 风格各异:没有统一的编码风格,也会导致代码出现坏味道。对于有争议的部分,不管是哪一方选择妥协,一定要拿出站得住脚的理由,达成一致,然后贯彻下去。
  • 无用代码:面对这种情况,推荐的做法是当机立断,直接删除。因为有版本控制工具的存在,我们不用担心代码丢失,也就没有必要舍不得。

以上并非代码崩坏的所有征兆,也很难给出一个完整的列表。但作为程序员,我们几乎都有这样的能力,既能够直接闻出代码的味道。就像一篇文章,一个故事,读了开头我们就大概知道其作者水平如何。

写在最后

选择“重构”这个庞大的主题来做文章,到现在仍旧是惴惴不安。毕竟要真正掌握它,需要大量的编码经验和项目设计经验。代码精进的道路路途遥远,对于我这个刚步入职场不到一年的菜鸟来说,恍若天堑。但千里之行始于足下,纵然步履维艰,也应上下求索。

这将是一个持续更新的系列。我会不断结合书中所学,输出自身对“重构”的理解和实践。最后,非常赞成 Kent Beck 在《重构》一书结尾写的话:

It's a little like walking along a narrow trail above a one-thousand-foot drop. As long as the light holds, you can step forward cautiously but with confidence. As soon as the sun sets, though, you'd better stop. You bed down for the night, sure the sun will rise again in the morning.

这有点像在悬崖峭壁上的小径行走,只要有光,你就可以前进,虽然谨慎却仍然自信。但是,一旦太阳下山,你就应该停止前进;夜晚你应该睡觉,并且相信明天早晨太阳仍旧升起。

重构剑法再现江湖,势必掀起一场血雨腥风,到底谁能笑到最后,取得那无上的武学造诣,还无人知晓。就让我们紧跟白衣少年的步伐,去探探隐藏在代码之界背后的秘密。

下一篇文章将从函数的重构技巧说起,不见不散!