《重构:改善既有代码的设计》读书笔记(一)

4,973 阅读10分钟

各位程序老司机对重构肯定不会陌生,程序员的工作离不开重构。那么重构是已经“飞入寻常百姓家”的普通技术能力,还是看起来高大上的杀器?

对于一项技术你并不是天生就会而是需要持续学习的,很多人对重构的认识停留在 DevTools 自带的 Refactor 工具和搜索引擎出来的几篇文章,但是这样得到的知识并不够完善和系统。所以当指点江山、慷慨激昂的重构时,或许你还不会重构。

这个系列文章会记录关于这本书的读书笔记,自己的思考都会在段落前面备注(思考:)。

对代码要有敬畏,教条是教条,重要的是自己的思考、实践和从中收获(与代码的一种默契、与这种场景的合拍,找到一种知音的感觉)。

一、重构、第一个案例

定义:在不改变软件可观察行为的前提下改善其内部结构,提高其可理解性、降低其修改成本。本质上说重构就是在代码写好之后改进它的设计。

想真正让重构技术发挥威力,就必须做到“不需了解软件行为”――听起来很荒谬,但事实如此。如果一段代码能让你容易了解其行为,说明它还不是那么迫切需要被重构。

那些最需要重构的代码,你只能看到其中的“坏味道”,接着选择对应的重构手法来消除这些“坏味道”,然后才有可能理解它的行为。而这整个过程之所以可行,全赖你在脑子里记录着一份“坏味道”与重构手法的对应表。

1.1 起点

如果你发现自己需要为程序添加一个特性,而代码结构使你无法很方便地达成目的,那就先重构那个程序,使特性的添加比较容易进行,然后再添加特性。

思考:发现痛点果断重构。重构分大小,不要把重构想象成都是庞大、旷日持久的工程而不愿开始,每天甚至每个小时都可以完成一项小重构。

1.2 重构第一步

重构前,先检查自己是否有一套可靠的测试机制,这些测试必须有自我检验能力。

更改变量名是否值得?绝对值得,好的代码应该清楚表达出自己的功能,变量名称是代码清晰的关键。

一个任何一个傻瓜都能写出计算机可以理解的代码。唯有写出人类容易理解的代码,才是优秀的程序员。

重构与性能冲突:重构时不必担心性能,优化时才需要在意,但那时你已经处于有利的位置,有更多选择可以完成有效优化。

思考:

  1. 重构之前保证有可靠的测试机制:重构只改变软件内部而不改变软件可观察的行为,重构之后的功能要向重构之前对齐;
  2. 好的代码自己就是注释
  3. 代码更多是让人读而不是让机器执行的
  4. 重构阶段不能因为可能的性能问题降低了重构的积极性,完成重构可以让优化有更多选择以及更容易的开展
  5. 重构与性能优化出发点不同:重构为了更易于理解和修改,性能优化为了所需性能往往会使代码较难理解;

1.3 重构的节奏

这个例子给我们最大的启发是重构的节奏:测试、小修改、测试、小修改、测试、小修改……正是这种节奏让重构得以快速而安全地前进。

思考:重构的前提是不改变软件行为,意味着不能因为重构代码而引入 bug。相对于大刀阔斧的代码删改,“小步快跑,快速验证”的节奏明显更可控

二、重构原则

2.1、为何重构

代码结构的流失是累积性的,越难看出代码所代表的设计意图,就越难保护其中设计,于是该设计就腐败的越快。经常性的重构可以帮助代码维持自己该有的形态

改进设计的一个重要方向就是消除重复代码。 这个动作的重要性在于方便未来的修改。代码量减少并不会使系统运行更快。然而代码量减少将使未来可能的程序修改动作容易得多。代码越多,正确的修改就越困难,因为有更多代码需要理解。如果消除重复代码,你就可以确定所有事物和行为在代码中只表述一次,这正是优秀设计的根本。

所谓程序设计,很大程度上就是与计算机交谈:你编写代码告诉计算机做什么事,它的响应则是精确按照你的指示行动。你得及时填补“想要它做什么”和“告诉它做什么”之间的缝隙。这种编程模式的核心就是“准确说出我所要的”。除了计算机外,你的源码还有其他读者;几个月之后可能会有另一位程序员尝试读懂你的代码并做一些修改:我们很容易忘记这第二位读者,但他才是最重要的,计算机是否多花了几个小时来编译,又有什么关系呢?如果一个程序员花费一周时间来修改某段代码,那才要命呢――如果他理解了你的代码,这个修改原本只需一小时。

思考:重构让软件更容易理解、更容易交接、更容易被自己回忆、更容易被别人修改,可以更好的提升人效。

我绝对相信:良好的设计是快速开发的根本――事实上,拥有良好设计才可能做到快速开发。如果没有良好设计,或许某一段时间内你的进展迅速,但恶劣的设计很快就让你的速度慢下来。 你会把时间花在调试上面,无法添加新功能。修改时间越来越长,因为你必须花越来越多的时间去理解系统、寻找重复代码。随着你给最初程序打上一个又一个的补丁,新特性需要更多代码才能实现。真是个恶性循环。

思考:良好设计是维持软件开发速度的根本。重构可以帮助你更快速地持续开发软件,因为它阻止系统腐败变质、防止代码变得难以理解、防止变得难以添加新功能,它甚至还可以提高设计质量。

2.2、何时重构

事不过三,三则重构。第三次做同样的事、遇到同样的麻烦,就是需要重构的时候了。

添加功能时重构,代码的设计无法帮助我轻松添加我所需要的特性:我看着设计,然后对自己说:“如果用某种方式来设计,添加特性会简单得多。”这种情况下我不会因为自己过去的错误而懊恼――我用重构弥补它。之所以这么做,部分原因是为了让未来增加新特性时能够更轻松一些,但最主要的原因还是:我发现这是最快捷的途径。重构是一个快速流畅的过程,一旦完成重构,新特性的添加就会再快速、再流畅。

思考:添加功能时重构既是弥补的一次机会,同时同样的时间消耗却多了额外的产出:更容易维护的设计。

复审代码时重构

  • 复审代码有助于知识的传播,有利于代码被编写者之外的人理解;
  • 复审代码可以得到别人角度的建议,然后动手实现,避免主观误判;
  • 复审团队需要精炼,就一个审查者和一个原作者。较大的项目可以通过UML图去展示代码的逻辑;

对于代码我们的希望:

  • 容易阅读;
  • 所有逻辑都只有唯一地点指定;
  • 新的改动不会危及现有行为;
  • 尽可能简单表达逻辑;

2.3 怎么对经理说

  • 不要告诉经理:经理是进度驱动,就是要求开发者尽快完成任务。而对于我来说最快完成任务的方式就是先重构。
  • 很多时候重构都为程序引入间接层。把大型对象拆分成小对象,把大型函数拆分为小型函数。
    • 允许逻辑共享:一个函数在不同地点被调用。子类共享超类的方法;
    • 分开解释意图和实现:通过类名和函数名解释自己的意图;
    • 隔离变化:在不同地方使用同一个对象,需要修改一处逻辑,那么可以做出子类,并在需要的时候修改这个子类;
    • 封装条件逻辑:运用多态。将条件逻辑转化为消息模式;
  • 减少间接层:当间接层只在一处使用,那么需要将其消除。

2.4 重构的难题

  • 修改接口
    • 对于已经发布的接口需要可能需要维护旧接口和新接口,用 deprecated 修饰旧接口;
    • 不发布新接口,在旧接口中调用新接口;
    • 假如新接口抛出编译时异常,那么可以在旧接口中调用新接口并将编译时异常转化为运行时异常;

不要过早发布接口,请修改你的 api 所有权,使重构更顺畅。

  • 何时不重构
    • 重构之前,代码必须能够在大部分情况下正常运行,不然就不应该重构,而应该是重写;
    • 到了 Deadline,应该避免重构。通常意味着你错过了早该进行重构的时间;

重写的一个清楚讯号是:现有代码根本不能正常运作。重构与重写的区别是:重构之前,代码在大多数情况下正常运作。

2.5 重构与设计

  • 重构与设计是彼此互补的;
  • 预先设计是必须,预先设计不可能做到完全正确,随着对问题的逐渐深入,通过重构可以改善程序的质量;
  • 重构减轻了设计的难度和压力,在程序不断修改的过程逐步完善程序的设计;

思考:重构与设计二者不是互斥的关系,而是不同的项目阶段不同的选择、重点。

2.6 重构与性能

  • 重构是有可能导致程序运行变慢的;
  • 除了对实时有严格要求的程序,编写快速软件的秘诀是:首先写出可调的程序,然后调整它以达到足够的速度
  • 经过分析大部分程序的大半部分时间是运行在一小半代码上,所以对所有代码一视同仁是错误的;
  • 性能优化放在开发的后期,通过分析工具找出消耗大量时间空间的地方,然后集中精力优化这些地方;

思考:重构与性能在某些场景下是冲突的:代码清晰明了是为了让程序员(人)来看的,减少的是程序员的时间成本;而性能则是表现在机器上。人容易看的代码一些场景下性能可能有瓶颈(想一想你写过的为了实现某项性能指标而改过的代码),那么孰重孰轻?首先写出可调的程序,然后调整它以达到足够的速度。