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

145 阅读9分钟

何谓重构

  • 重构(名词):对软件内部结构的一种调整,目的是在不改变软件可观察行为的前提下,提高其可理解性,降低其修改成本。
  • 重构(动词):使用一系列重构手法,在不改变软件可观察行为的前提下,调整其结构。

两顶帽子

使用重构技术开发软件时,我把自己的时间分配给两种截然不同的行为:添加新功能和重构。

添加新功能时,我不应该修改既有代码,只管添加新功能。通过添加测试并让测试正常运行,我可以衡量自己的工作进度。

重构时我就不能再添加功能,只管调整代码的结构。此时我不应该添加任何测试(除非发现有先前遗漏的东西),只在绝对必要(用以处理接口变化)时才修改测试。

为何重构

  • 重构改进软件的设计
  • 重构使软件更容易理解
  • 重构帮助找到bug
  • 重构提高编程速度

何时重构

预备性重构:让添加新功能更容易

在添加新功能之前进行重构可以使代码结构更加清晰,减少重复代码,提高代码的可维护性和可扩展性。即通过重构来改善代码结构,降低同样bug再次出现的概率。

帮助理解的重构:使代码更易懂

通过重构可以使代码更加易读易懂,更好地表达意图,帮助理解和发现设计问题。 重构可以将对代码的理解植入代码本身,使得这份知识能够保存更久并且可以被其他人看到。 初步的重构可以帮助我们理解代码,发现设计问题,获得更高层面的理解。如果不重构代码,就无法看到隐藏在混乱中的机遇。

捡垃圾式重构

如果发现代码存在问题,可以根据具体情况决定是立即重构还是暂时记下来之后再处理;即使处理需要时间,也应该尽力清理垃圾,每次都让代码变得更好,积少成多最终达到清理的目的。

重构的好处在于每个小步骤都不会破坏代码,即使清理不完全也不会对代码产生负面影响。

有计划的重构和见机行事的重构

[!tip] 肮脏的代码必须重构,但漂亮的代码也需要很多重构。

重构是自然的编程流程的一部分:重构不是与编程割裂的行为,它应该是开发过程中的一部分。重构可以帮助当前的任务,并让未来的工作更轻松。

漂亮的代码也需要重构:重构不仅仅是为了弥补过去的错误或清理肮脏的代码,漂亮的代码也需要重构。在编写代码时,需要做出很多权衡取舍,而这些权衡可能会随着时间和需求的变化而改变。整洁的代码重构起来会更容易。

见机行事的重构是重要的,但有计划的重构也必要:虽然见机行事的重构是重要的,但有时团队需要花时间专门优化代码库,以便更容易添加新功能。有计划的重构应该很少,大部分重构应该是不起眼的、见机行事的。团队应该找到适合自己的工作方式,而分离重构提交并不是毋庸置疑的原则,只有当团队真的感到有益时,才值得这样做。

长期重构

大多数重构可以在几分钟到几小时内完成,但有些大型重构可能需要几个星期。 不建议让一支团队专门做重构,而是让整个团队达成共识,在未来的几周逐步解决问题。 重构不会破坏代码,每次小改动之后,整个系统仍然正常工作。可以通过引入一层新的抽象来逐步替换旧的库。

复审代码时重构

代码复审的重要性:代码复审可以改善开发状况,帮助传播知识,提高代码清晰度,让更多人有机会提出有用的建议。

重构在代码复审中的作用:重构可以帮助复审者更好地理解代码,提出更具体的建议,并立即实现一些建议。重构也可以让复审者获得更高层次的认识。

代码复审中加入重构的方式:在常见的 pull request 模式下,复审者独自浏览代码,此时进行重构效果并不好。最佳的方式是与原作者肩并肩坐在一起,一边浏览代码一边重构,这种工作方式很自然地导向结对编程。

怎么对经理说

如果是不懂技术的经理(客户),那么,不要告诉经理!

何时不应该重构

[!tip] 如果我看见一块凌乱的代码,但并不需要修改它,那么我就不需要重构它。如果丑陋的代码能被隐藏在一个API之下,我就可以容忍它继续保持丑陋。只有当我需要理解其工作原理时,对其进行重构才有价值。

如果重写比重构还容易,就不需要重构,而是直接重写。

重构的挑战

延缓新功能开发

[!tip] 重构的唯一目的就是让我们开发更快,用更少的工作量创造更大的价 值。

重构的意义不在于把代码库打磨得闪闪发光,而是纯粹经济角度出发的考量。我们之所以重构,因为它能让我们更快——添加功能更快,修复bug更快。一定要随时记住这一点,与别人交流时也要不断强调这一点。重构应该总是由经济利益驱动。

代码所有权

推荐团队代码所有制,这样一支团队里的成员都可以修改这个团队拥有的代码,即便最初写代码的是别人。程序员可能各自分工负责系统的不同区域,但这种责任应该体现为监控自己责任区内发生的修改,而不是简单粗暴地禁止别人修改。

分支

特性分支的生命应该很短,采用的方法叫作持续集成(Continuous Integration,CI),也叫“基于主干开发”(Trunk-Based Development)。 在使用CI时,每个团队成员每天至少向主线集成一次。这个实践避免了任何分支彼此差异太大,从而极大地降低了合并的难度。 不过CI也有其代价:必须使用相关的实践以确保主线随时处于健康状态,必须学会将大功能拆分成小块,还必须使用特性开关(feature toggle,也叫特性旗标,feature flag)将尚未完成又无法拆小的功能隐藏掉。

测试

[!tip] 这里的关键就在于“快速发现错误”。要做到这一点,我的代码应该有一套完备的测试套件,并且运行速度要快,否则我会不愿意频繁运行它。也就是说,绝大多数情况下,如果想要重构,我得先有可以自测试的代码。

遗留代码

遗留代码往往很复杂,测试不足,需要进行重构以改善代码结构和可读性。 遗留系统多半缺乏测试,因此需要添加测试以确保重构的安全性。 重构需要运用重构手法创造出接缝,可以随时重构相关的代码,每次触碰一块代码时,尝试让它变得更好一点。

数据库

[!tip] 要改名一个字段,我的第一次提交会新添一个字段,但暂时不使用它。然后我会修改数据写入的逻辑,使其同时写入新旧两个字段。随后我就可以修改读取数据的地方,将它们逐个改为使用新字段。这步修改完成之后,我会暂停一小段时间,看看是否有bug冒出来。确定没有bug之后,我再删除已经没人使用的旧字段。这种修改数据库的方式是并行修改(Parallel Change,也叫扩展协议/expandcontract)的一个实例。

重构、架构和YAGNI

  • 应对未来变化的办法之一,就是在软件里植入灵活性机制。但过于考虑灵活性机制,反而会拖慢了响应变化的速度。
  • 与其猜测未来需要哪些灵活性、需要什么机制来提供灵活性,我更愿意只根据当前的需求来构造软件,同时把软件的设计质量做得很高。
  • 如果一种灵活性机制不会增加复杂度(比如添加几个命名良好的小函数),我可以很开心地引入它;但如果一种灵活性会增加软件复杂度,就必须先证明自己值得被引入。
  • 要判断是否应该为未来的变化添加灵活性,我会评估“如果以后再重构有多困难”,只有当未来重构会很困难时,我才考虑现在就添加灵活性机制。
  • YAGNI,“你不会需要它”(you arenʼt going to need it)

重构与软件开发过程

  • 自测试代码、持续集成、重构——彼此之间有着很强的协同效应

重构与性能

  • 时间预算法,这通常只用于性能要求极高的实时系统。这种方法高度重视性能,对于心律调节器一类的系统是必需的,因为在这样的系统中迟来的数据就是错误的数据。但对其他系统(例如我经常开发的企业信息系统)而言,如此追求高性能就有点儿过分了。

  • 持续关注法。这种方法要求任何程序员在任何时间做任何事时,都要设法保持系统的高性能。这种方式很常见,感觉上很有吸引力,但通常不会起太大作用。

  • 关于性能,一件很有趣的事情是:如果你对大多数程序进行分析,就会发现它把大半时间都耗费在一小半代码身上。如果你一视同仁地优化所有代码,90%的优化工作都是白费劲的,因为被你优化的代码大多很少被执行。你花时间做优化是为了让程序运行更快,但如果因为缺乏对程序的清楚认识而花费时间,那些时间就都被浪费掉了。

  • 编写构造良好的程序,不对性能投以特别的关注,直至进入性能优化阶段——那通常是在开发后期。