《重构改善既有代码的设计》Tips

《重构改善既有代码的设计》Tips
  • 如果你要给程序添加一个特性, 但发现代码因缺乏良好的结构而不易于进行更改, 那就先重构那个程序, 使其比较容易添加该特性, 然后再添加该特性.
  • 重构前, 先检查自己是否有一套可靠的测试集。 这些测试必须有自我检验能力。
  • 重构技术就是以微小的步伐修改程序。 如果你犯下错误, 很容易便可发现它。
    论每次重构多么简单, 养成重构后即运行测试的习惯非常重要。 犯错误是很容易的——至少我知道我是很容易犯错的。 做完一次修改就运行测试, 这样在我真的犯了错时, 只需要考虑一个很小的改动范围, 这使得查错与修复问题易如 反掌。 这就是重构过程的精髓所在: 小步修改, 每次修改后就运行测试。 如果我改动了太多东西, 犯错时就可能陷入麻烦的调试, 并为此耗费大把时间。 小步修 改, 以及它带来的频繁反馈, 正是防止混乱的关键
  • 傻瓜都能写出计算机可以理解的代码。 唯有能写出人类容易理解的代码的, 才是优秀的程序员。
  • 因此对于重构过程的性能问题, 我总体的建议是: 大多数情况下可以忽略它。 如果重构引入了性能损耗, 先完成重构, 再做性能优化。
  • 编程时, 需要遵循营地法则: 保证你离开时的代码库一定比来时更健康。
  • 好代码的检验标准就是人们是否能轻而易举地修改它。(我不完全认同)
  • 如果有人说他们的代码在重构过程中有一两天时间不可用, 基本上可以确定, 他们在做的事不是重构。 (个人认为重构应该是用一系列手法在不改变程序本身功能的前提下提高代码的可维护性和可阅读性, 而重构的过程采用小步进应该)
  • 不过就用户应该关心的行为而言, 不应该有任何改变。 如果我在重构过程中发现了任何 bug, 重构完成后同样的bug应该仍然存在(不过, 如果潜在的bug还没有被任何人发现, 也可以当即把它改掉) 。
  • 重构与性能优化有很多相似之处: 两者都需要修改代码, 并且两者都不会改变程序的整体功能。 两者的差别在于其目的: 重构是为了让代码“更容易理解,更易于修改”。 这可能使程序运行得更快, 也可能使程序运行得更慢。 在性能优化时, 我只关心让程序运行得更快, 最终得到的代码有可能更难理解和维护, 对此我有心理准备。
  • 果没有重构, 程序的内部设计(或者叫架构) 会逐渐腐败变质。 当人们只为短期目的而修改代码时, 他们经常没有完全理解架构的整体设计, 于是代码逐渐失去了自己的结构。 程序员越来越难通过阅读源码来理解原来的设计。 代码结构的流失有累积效应。 越难看出代码所代表的设计意图, 就越难保护其设计, 于是设计就腐败得越快。 经常性的重构有助于代码维持自己该有的形态。 完成同样一件事, 设计欠佳的程序往往需要更多代码, 这常常是因为代码在不同的地方使用完全相同的语句做同样的事, 因此改进设计的一个重要方向就是消除重复代码。
  • 我们应该改变一下开发节奏, 让代码变得更易于理解。 重构可以帮我让代码更易读。 开始进行重构前, 代码可以正常运行, 但结构不够理想。 在重构上花一点点时间, 就可以让代码更好地表达自己的意图——更清晰地说出我想要做的。
  • 对代码的理解, 可以帮我找到bug。
  • 重构帮我更快速地开发程序。需要添加新功能时, 内部质量良好的软件让我可以很容易找到在哪里修改、 如何修改。 良好的模块划分使我只需要理解代码库的一小部分, 就可以做出修改。 如果代码很清晰, 我引入bug的可能性就会变小, 即使引入了bug, 调试也会容易得多。 理想情况下, 我的代码库会逐步演化成一个平台, 在其上可以很容易地构造与其领域相关的新功能。
  • “设计耐久性假说”: 通过投入精力改善内部设计, 我们增加了软件的耐久性, 从而可以更长时间地保持开发的快速。 我还无法科学地证明这个理论, 所以我说它是一个“假说”。
  • 重构不是与编程割裂的行为。 你不会专门安排时间重构, 正如你不会专门安排时间写if语句。 我的项目计划上没有专门留给重构的时间, 绝大多数重构都在我做其他事的过程中自然发生。 不过, 说了这么多, 并不表示有计划的重构总是错的。 如果团队过去忽视了重构, 那么常常会需要专门花一些时间来优化代码库, 以便更容易添加新功能。在重构上花一个星期的时间, 会在未来几个月里发挥价值。 有时, 即便团队做了 日常的重构, 还是会有问题在某个区域逐渐累积长大, 最终需要专门花些时间来解决。 但这种有计划的重构应该很少, 大部分重构应该是不起眼的、 见机行事的。
  • 还有一种常见的误解认为, 重构就是人们弥补过去的错误或者清理肮脏的代码。 当然, 如果遇上了肮脏的代码, 你必须重构, 但漂亮的代码也需要很多重构。
  • 大多数重构可以在几分钟——最多几小时——内完成。 但有一些大型的重构可能要花上几个星期, 即便在这样的情况下, 我仍然不愿让一支团队专门做重构。 可以让整个团队达成共识, 在未来几周时间里逐步解决这个问题, 这经常是一个有效的策略。 每当有人靠近“重构区”的代码, 就把它朝想要改进的方向推动一点。 这个策略的好处在于, 重构不会破坏代码——每次小改动之后, 整个系统仍然照常工作。 (这个策略叫作Branch By Abstraction[mf-bba]。)
  • 代码复审的过程中代码的原作者在旁边会好很多,因为作者能提供关于代码的上下文信息, 并且充分认同复审者进行修改的意图。 对我个人而言, 与原作者肩并肩坐在一起, 一边浏览代码一边重构, 体验是最佳的。 这种工作方式很自然地导向结对编程: 在编程的过程中持续不断地进行代码复审
  • 怎么对经理说? 如果这位经理懂技术, 能理解“设计耐久性假说”, 那么向他说明重构的意义应该不会很困难。 如果他们不理解代码库的健康对生产率的影响。 这种情况下我会给团队一个较有争议的建议: 不要告诉经理! 这是在搞破坏吗? 我不这样想。 软件开发者都是专业人士。 我们的工作就是尽可能快速创造出高效软件。 我的经验告诉我, 对于快速创造软件, 重构可带来巨大帮助。 如果需要添加新功能, 而原本设计却又使我无法方便地修改, 我发现先重构再添加新功能会更快些。 如果要修补错误, 就得先理解软件的工作方式,而我发现重构是理解软件的最快方式。 受进度驱动的经理要我尽可能快速完成任务, 至于怎么完成, 那就是我的事了。 我领这份工资, 是因为我擅长快速实现新功能; 我认为最快的方式就是重构, 所以我就重构。 (这条的争议可能会比较大,但是如果把重构当成开发的一部分就想的过去了)
  • 何时不应该重构? 1)如果我看见一块凌乱的代码, 但并不需要修改它; 2)如果重写比重构还容易, 就别重构了;
  • 我推荐团队代码所有制, 这样一支团队里的成员都可以修改这个团队拥有的代码, 即便最初写代码的是别人。 程序员可能各自分工负责系统的不同区域, 但这种责任应该体现为监控自己责任区内发生的修改, 而不是简单粗暴地禁止别人修改
  • 分支合并本来就是一个复杂的问题, 随着特性分支存在的时间加长, 合并的难度会指数上升。 所以很多人认为, 应该尽量缩短特性分支的生存周期, 比如只有一两天。 还有一些人(比如我本人) 认为特性分支的生命还应该更短, 我们采用的方法叫作持续集成(Continuous Integration, CI) , 也叫“基于主干开发”(Trunk-Based Development) 在使用CI时, 每个团队成员每天至少向主线集成一次。 不过CI也有其代价: 你必须使用相关的实践以确保主线随时处于健康状态, 必须学会将大功能拆分成小块, 还必须使用特性开关(feature toggle, 也叫特性旗标, feature flag) 将尚未完成又无法拆小的功能隐藏掉。
  • 不会改变程序可观察的行为, 这是重构的一个重要特征。如果每个重构都是很小的修改, 即便真的造成了破坏, 我也只需要检查最后一步的小修改——就算找不到出错的原因, 只要回滚到版本控制中最后一个可用的版本就行了。这里的关键就在于“快速发现错误”。 要做到这一点, 我的代码应该有一套完备的测试套件, 并且运行速度要快, 否则我会不愿意频繁运行它。 (文中还提到了另外保障重构质量的方法:1. 重构工具;2. 只使用验证过的重构方法;)
  • 对于没有自测的遗留代码,秉持的原则是 “没测试就加测试”。而加测试的时候主要在于去找到代码的衔接处,如果没有衔接的地方则可能需要使用重构手法来创建衔接处。 但此时的重构是非常危险的,需要格外小心。鉴于此种情况,如果能在代码最开始的时候编写测试程序是极好的。 不过就算有了测试, 我也不建议你尝试一鼓作气把复杂而混乱的遗留代码重构成漂亮的代码。 我更愿意随时重构相关的代码: 每次触碰一块代码时, 我会尝试把它变好一点点——至少要让营地比我到达时更干净。 如果是一个大系统, 越是频繁使用的代码, 改善其可理解性的努力就能得到越丰厚的回报。
  • 与常规的重构不同, 很多时候, 数据库重构最好是分散到多次生产发布来完成, 这样即便某次修改在生产数据库上造成了问题, 也比较容易回滚。 比如, 要改名一个字段, 我的第一次提交会新添一个字段, 但暂时不使用它。 然后我会修改数据写入的逻辑, 使其同时写入新旧两个字段。 随后我就可以修改读取数据的地方, 将它们逐个改为使用新字段。 这步修改完成之后, 我会暂停一小段时间,看看是否有bug冒出来。 确定没有bug之后, 我再删除已经没人使用的旧字段。 这种修改数据库的方式是并行修改(Parallel Change, 也叫扩展协议/expandcontract) [mf-pc]的一个实例。
  • 要真正以敏捷的方式运作项目, 团队成员必须在重构上有能力、 有热情, 他们采用的开发过程必须与常规的、 持续的重构相匹配。
  • 重构的第一块基石是自测试代码。 我应该有一套自动化的测试, 我可以频繁地运行它们, 并且我有信心: 如果我在编程过程中犯了任何错误, 会有测试失败。 这块基石如此重要, 我会专门用一章篇幅来讨论它。
  • 自测试代码、 持续集成、 重构——彼此之间有着很强的协同效应。有这三大核心实践打下的基础, 才谈得上运用敏捷思想的其他部分。 不管采用什么方法, 软件开发都是一件复杂而微妙的事, 涉及人与人之间、 人与机器之间的复杂交互。我在这里描述的方法已经被证明可以应对这些复杂性, 但——就跟其他所有方法一样——对使用者的实践和技能有要求。
  • 今天的大多数重构功能都依附于强大的IDE, 因为这些IDE原本就在语法树上实现了代码导航、 静态检查等功能,自然也可以用于重构。 不仅能处理文本, 还能处理语法树, 这是IDE相比于文本编辑器更先进的地方.
  • 坏代码的指标有:神秘命名,重复代码,过长函数,过长参数列表,全局数据,可变数据,发散式变化,霰弹式修改,
  • 重构手法:函数调用代替函数名称,多态代替分支,循环拆分,参数对象取代冗长的参数列表,单例只包含只读或者不存储状态的函数,
  • 编写优良的测试程序, 可以极大提高我的编程速度, 即使不进行重构也一样如此。
  • 测试驱动开发的编程方式依赖于下面这个短循环: 先编写一个(失败的) 测试, 编写代码使测试通过, 然后进行重构以保证代码整洁。 这个“测试、 编码、 重构”的循环应该在每个小时内都完成很多次。 这种良好的节奏感可使编程工作以更加高效、 有条不紊的方式开展。
  • 一旦业务逻辑的部分开始变复杂, 我就会把它与UI分离开, 以便能更好地理解和测试它。
  • 当我为类似的既有代码编写测试时, 发现一切正常工作固然是好, 但我天然持怀疑精神。 特别是有很多测试在运行时, 我总会担心测试没有按我期望的方式检查结果, 从而没法在实际出错的时候抓到bug。 因此编写测试时, 我想看到每个测试都至少失败一遍。 我最爱的方式莫过于在代码中暂时引入一个错误,总是确保测试不该通过时真的会失败。
  • 测试的重点应该是那些我最担心出错的部分, 这样就能从测试工作中得到最大利。
  • 测试的阶段:配置-检查-验证(setup-exercise-verify),given-when-then,准备-行为-断言(arrange-act-assert). 除此之外还有一个拆除阶段,即此阶段可将测试夹具移除, 以确保不同测试之间不会产生交互。 因为有时创建缓慢等原因, 我们会在不同的测试间共享测试夹具, 此时, 显式地声明一个拆除操作就是很重要的。
  • 测试覆盖率的分析只能识别出那些未被测试覆盖到的代码, 而不能用来衡量一个测试集的质量高低。
  • 单元测试的缺失不仅会意味着较低的工程质量,而且意味着重构的难以进行,一个有单元测试的项目尚且不能够保证重构前后的逻辑完全相同,一个没有单元测试的项目很可能本身的项目质量就堪忧,更不用说如何在不丢失业务逻辑的情况下进行重构了。
原创文章,版权所有,转载请获得作者本人允许并注明出处
我是留白;我是留白;我是留白;(重要的事情说三遍)
posted @ 2021-09-23 22:22  Mojies  阅读(93)  评论(0编辑  收藏  举报