《重构 改善既有代码的设计 第二版》重读笔记

154 阅读6分钟

前言

趁着国庆假期,把许久之前读过的《重构》又拾起来重读一番,很早之前读的是基于JAVA语言第一版,留在脑海里的知识已经所剩不多,这次读的是使用JavaScript的第二版。虽然语言不同,但核心思想仍然是一致的。

在这篇文章里,我尝试不再以流水账的方式复述书中的章节,而是根据自身的理解,在经验和认知的基础上,吸收书里所传达的知识。如有疏漏或表达含混(必然是存在的),敬请谅解指正。

理清概念

什么是重构

在做一件事之前,先要弄清楚这件事的定义是什么、边界在哪里。

所谓重构(refactoring)是这样一个过程:在不改变代码外在行为的前提下,对代码做出修改,以改进程序的内部结构。

重构定义中有两个关键点:

  • 一是不变:程序对外可观察到的行为是不发生变化的,不能因为重构导致模块以往的功能不可用,这是不能接受的。
  • 二是改变:改变的范畴是软件内部流程结构,目标是为了更好地进行维护,提升软件内部质量。

好的设计(有重构)与差的设计(无重构)之间对比

随着时间推移,向坏的设计上增加功能越来越难,好的设计则不然。

image.png

代码的腐化

熵增现象是客观世界不可避免的现实,一切事物从诞生之初起,就开始了自有序走向混乱的过程,一旦偏离有序状态太久,就必然需要进行拨乱反正。

对软件而言也是如此,功能开发之初通常伴随着完善的架构设计和编码规范,随之后续不断增加新功能以及打补丁,软件的实现路径逐渐偏离最初的设计,导致坏味道不断增多。

这时有人要问了,为什么不在第一版设计时候,就让架构支持后续扩展和新需求迭代,对修改关闭,对扩展开放。实际上,只有凤毛麟角的架构师能够在设计之初就考虑到未来所有的新需求,这不仅对技术有极高的要求,设计者还必须洞察业务的未来走向。对绝大多数公司而言,一线的开发者根本不具备这种能力(DDD领域驱动设计——这不是开发者的问题,而是公司的问题,其支付的低廉费用不足以聘用到具备足够能力的人员),他们日常繁重的业务需求开发压力也不允许他们对未来有更多的时间思考。更何况,消费者的喜好是频繁变化的,很多需求在开发完成一版、上线几个月后就被抛在脑后,成为新热点的垫脚石。这样的软件又谈何长期维护和发展?

何时重构

需要强调,重构并不是编程中的必要过程,要写出漂亮的代码也并非一定要经过重构。此外,即使一段代码写得糟糕无比,例如程序员们经常会遇到的屎山代码,只要它能正常运行,在没有新需求开发的前提下,仍然不必对它进行重构。

在笔者看来,重构发生的时机,仅限于以下两种:

  • 要在一座屎山代码上增加新的功能,不可避免对旧逻辑进行调整。
  • 模块虽然已经不再有新需求,但频繁报错,一而再、再而三投入人力修复。

所有的技术都是为业务需求服务的,脱离了业务实际需求的技术就是不切实际的空中楼阁、屠龙之术。

对业务而言,重构一方面可以控制开发新功能时的成本,另一方面能够降低在维护已有功能上所消耗的人力。

重构时的一些原则

在进行重构之前,需要明确几个原则,这些原则有助于重构过程更好的实施。

两顶帽子

就像一个戴帽子的人,一顶帽子叫做开发新需求,另一顶帽子叫做重构旧代码,你不能同时把这两顶戴在头上,不论何时都应当明确自己戴的帽子是哪一顶。

事不过三

第一次做某件事时只管去做;第二次做类似的事会产生反感,但无论如何还是可以去做;第三次再做类似的事,你就应该重构

测试与重构

书中尤其强调测试的重要性,对于需要长期维护的软件而言,如果有完备的单元测试,无疑是能够在最大程度保证代码质量。但正如前文所言,很多需求在开发时,连产品经理自己都不知道未来是否要长期维护,开发排期就更无法将单测作为任务排进去,就算排了也一定会收到需求方的challenge。因此,很多时候重构时要面临没有测试的场景。

然而,这也给重构提出了更高的要求,例如进行数据-视图拆分的重构时,对数据层是可以设计单元测试来保证重构过程的高效和质量的。

性能并不总是至上

为了维护成本降低和代码健壮性提升,是可以适当牺牲性能的,一个例子就是循环列表时进行多项操作,通过把操作抽出成为模板,增强代码灵活性,即便多循环几次,也可以接受。

典型重构技巧与手法

重构是为了优化软件内部实现,按照不同的颗粒度,可以从微观到宏观的三个层级来总结。

变量

  • 避免直接访问全局变量,而是通过get/set函数进行封装
  • 谨慎使用局部变量,在进行选择时,类变量的优先级更高
  • 在变量&表达式之间,选择更具表现力的那一个。通过内联(inline)可以将变量转化为表达式;通过提取(extract)将表达式转换为变量
  • 不要对输入参数赋值,而是进行变量拆分
  • 在传递引用对象时,只为每一个实体创建一次对象,并且建立一个仓库用于维护它们

函数

  • 函数的命名应当体现 做什么,而非 怎样做
  • 函数的参数个数超过6个,则抽出参数封装作为对象
  • 函数过长则抽子函数,尤其是在switchloop的场景中,这里还涉及到另一种手法——拆分循环
  • 对上一条的补充,如果判断条件的表达式过长,则extract
  • 以管道取代循环, map filter slice 等,Kotlin对此已经有完美支持
  • 卫语句(Guard Clauses)—— 在异常条件出现时快速退出,函数里只有一个在末尾的return并非好的编程习惯
  • 明确表现出 有副作用无副作用 两种函数的差异

  • 类的职责膨胀后,如果出现了不符合类自身设计初衷的功能,就要及时提炼出新的类
  • 类的职责萎缩后,应当进行内联
  • 函数和字段应当是流动的,会在继承体系里上移、下移;所谓 多态,是指兄弟之间的表现不同