Featured image of post 《重构》-重构的原则

《重构》-重构的原则

前言

本文是一篇关于Martin Fowler《重构:改善既有代码的设计》的第二章读书笔记与总结。

何谓重构

很多一线的实践者会非常随意的使用重构这个词,但是作者会非常严谨地使用重构。

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

重构的关键在于运用大量微且保持软件行为的步骤,一步步达成大规模的修改。 每个单独的重构呀么很小,要么由若干个小步骤组合而成。这样的好处是,即使重构没有完成,也可以在任何时刻停下来,代码很少进入到不可工作的状态。

重构与性能优化

二者有很多相似之处

  • 都需要修改代码
  • 都不会改变程序的整体功能

区别

  • 重构是为了让代码更容易理解,更易于修改。这可能会让程序运行的更快,也可能运行的更慢。
  • 性能优化时,只关心如何让代码程序运行得更快,最终得到的代码有可能更难理解和维护,对此需要有心理准备。

两顶帽子

Kent Beck提出了两顶帽子的比喻。使用重构技术进行软件开发是,可以把自己的时间分配给两种截然不同的行为:

  • 添加新功能:不应该修改既有的代码,只管添加新功能。同时需要添加测试并且让测试正常运行。
  • 重构:不再添加新的功能,只调整代码的结构。此时不应该添加任何测试,只有在绝对必要的时候才修改测试。

但是在实际的开发过程当中,我实际会经常变换帽子:当我开始添加新功能时候,突然意识到如果把程序结构修改一下,功能添加会容易很多。于是就换上了一顶重构帽子,开始了一会的重构工作。当程序结构调整好了之后,又戴上了原来的帽子。

总之,整个过程中,无论何时何地,都要清楚当前的自己戴的帽子究竟是哪一个,并且了解不同的帽子对编程状态提出的不同的要求

为何重构

重构绝对不是包治百病的灵丹妙药,更不是所谓的“银弹”,但是绝对算是一把银钳子,可以帮你始终良好的控制自己的代码。 重构是一个工具,它能够用于以下几个目的:

重构改进软件的设计

如果没有重构,程序的内部设计(架构)会逐渐的腐败变质。当人们只为了短期目的而修改代码时,经常没有完全理解架构的整体设计,于是代码就逐渐失去了自己的结构。(屎山代码就是这么来的,但是没有办法避免) 越难看出代码的设计意图,就越难保护其设计,腐败的也就越快。经常的重构有助于代码为支持自己该有的形态。 同时,消除重复代码,就可以确定所有事物和行为在代码中只表述一次,这才是优秀设计的根本。

重构使软件更容易理解

虽然代码是在计算机上运行的,编程的核心就是“准确的说出我想要的”,但是除了计算机之外,源码还有其他读者,几个月之后可能会有另外以为程序员尝试读懂我的代码并且进行一些修改。我们很容易就忘记了这个读者,“又不是不能用”,然后这个代码就这么写出来了。而后人可能会花费很多时间来理解和阅读我的代码。

因此,我们需要改变一下开发的节奏,让代码变得更加易于理解。重构可以让代码更加易读。

做一个懒惰的程序员,表现形式之一就是,总是记不住自己写过的代码。对于任何能够立刻查阅的东西,都可以故意不去记它,别把自己的脑袋塞爆了。尽量把需要记住的东西写在代码里。

重构帮助找到bug

重构的时候,能够更加深入的理解代码的所做所谓,并立即把新的理解反映在代码当中。在弄清楚程序结构的同时,必须要做一些假设去验证,因此想不把bug找出来都难。

Kent Beck:我不是一个特别好的程序员,我只是一个有一些特别好的习惯的还不错的程序员

重构提高编程速度

最后,前面的一切都归结到了这一点: 重构能够帮助我们更快的开发程序。

这虽然有点违反直觉,因为重构本来就是一件耗费精力的事情。但是如果一个代码全靠补丁来不断的修复,最后就是补丁补补丁,需要考古学家才能弄清楚这个代码到底是怎么个回事。以至于最后,这个系统的负担会不断拖慢新增功能的速度,到最后程序员恨不得重头开始写整个系统。

如果代码很清晰,引入bug的可能性就会很小,即使引入的bug,调试和排查也会容易很多。理想情况下,我的代码库会逐渐严骅成为一个平台,在其上可以容易的构造与其领域相关的新功能。

设计耐久性假说:通过投入精力改善内部设计,我们增加了软件的耐久性,从而可以更长时间的保持开发的快速。

何时重构

编程的每个小时,都应该去做重构。有几种方式可以把重构融入工作过程中。

三次法则

  • 第一次做某件事的时候尽管去做
  • 第二次做类似的事情会产生方案,但是无论如何还是可以去去做
  • 第三次再做类似的事情,就应该去重构

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

重构的最佳时机就是在添加新功能之前

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

需要理解代码做什么才能着手修改

  • 比如看到一段迷惑的代码,自问,能不能重构它,让它看起来一目了然?
  • 看到结果糟糕的条件判断,想修改,发现函数命名太糟糕了? 等等,都是重构的机会。哪怕只是修改函数的命名这类重构,只要能够更加清楚的表达出一图,都会是非常有意义的。这些初步的重构,就像是扫去窗上的灰尘,能够让我们看到窗外的风景。

捡垃圾式重构

可以把要重构的点记下来,把更紧急的事情处理之后,抽空来处理这些垃圾。积少成多

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

  • 肮脏的代码必须重构,但是漂亮的代码也需要很多重构(需求和情况是会不断改变的)
  • 每一次修改的时候,首先要令修改很容易(这件事有时候会很难),然后再进行这次容易的修改
  • 软件永远都不应该被视为“完成”
  • 重构常常和新功能紧密交织,不值得花功夫将它们分开。并且这样做也使得重构脱离上下文(除非真的有必要,感觉到真有的有益时,才值得这样做)

长期重构

重构可能会花费几分钟、几个小时、几个周、几个月,但是哪怕是大型的重构项目,也不愿意让一个团队专门来做重构。可以让团队达成共识,可以逐步解决这个问题。 小改动的好处是,不会破坏代码。例如想要换掉一个正在使用的库,可以引入一层新的抽象,兼容新旧两个库的接口,直到调用方已经完全改用了这层抽象。

复审代码时重构

重构可以帮助我复审别人的代码,同时还能帮助cr工作得到更加具体的结果。最好的情况,就是两个人肩并肩,一起cr,一起重构,这也就是所谓的结对编程了

怎么对经理说?(客户)

重构的时候不要告诉经理(客户),他们是进度驱动的,而我们发现重构就是完成工作最快的方式,那么就做。

  • 我认为最快的方式就是重构,所以我就重构

何时不应该重构

  • 如果丑陋的代码能够隐藏在一个api之下,不影响我使用,那就可以忍受它继续保持丑陋。只有需要了解其工作原理时,重构才有价值
  • 重写比重构还容易的时候,就别重构了,直接重写。不过如何判断,就需要丰富的经验了

重构的挑战

延缓新功能开发

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

  • 如果一个小功能,可以先上小功能,再做大规模的重构。这需要程序员专业的判断能力,无法量化决定的依据。
  • 重构应该总是以收益来驱动。而不是掉进“整洁的代码”“良好的工程实践”这类道德理由的陷阱里。纯粹考虑经济利益。都是上班,为何不用一些工作效率更高的方式来完成工作呢?

代码所有权

  • 不建议搞细粒度的强代码所有制
  • 鼓励开源的模型(比如一些开源协议)

分支

分支的合并是一个复杂的问题,随着特性分支的存在的时间加长,合并的难度会指数上升。减少特性分支的生命周期。 使用的方法就是CI(Continuous Integration),也叫作“基于主干开发”。在使用CI时,每个团队成员每天至少向主干分支集成一次。

  • 避免了任何分支之间差异太大
  • 降低了合并的难度
  • 必须保证主干的健康
  • 学会将大功能拆小,将未完成又无法拆小的功能隐藏掉

测试

想要重构,就必须要有可以自测试的代码(单元测试的重要性)自测试代码和继承测试紧密相关。我们依赖持续继承来及时捕获分支继承时的语义冲突。测试也是持续交付的关键环节

遗留代码

对于别人写的代码,留下来的遗产,没有测试我们就加测试,听起来简单,做起来可不容易。再有了充分的测试之后再进行重构。 这很难,但是于是频繁使用的代码,改善其可理解性的努力就能获得更丰厚的回报。

数据库

将代码和数据库的变更,通过脚本代码的方式来进行同步。和常规的重构不同,数据库重构最好是分散到多次生产发布来完成,这样即使出现问题,也比较容易回滚。

  • 同时写入新旧两个字段,然后再把读的地方逐个换成新的字段,看有没有bug,确定没有问题之后,在进行修改,删除无人使用的旧字段。

重构、架构和YAGNI

在任何时候,代码开始写之前就应该完成软件的设计和架构,一旦代码写了出来,代码就开始腐败了

重构改变了这种观点,重构可以改善现有代码的设计。这种观点最大的问题,就是大多数时候,软件的需求都很难被预先充分理解。只有在真正使用的时候,人们才会知道自己真正想要的是什么(就像人生很多事情,在经历的时候,才会知道自己真正想要的是什么) 应对未来变化的办法之一,就是在软件植入灵活性机制。在编写一个函数的时候就要思考,如何将这个代码变得更有通用性。如果一个灵活性会增加软件复杂度,就必须证明它值得被引入。

这种设计方法叫做简单设计、增量式设计或者YAGNI:You aren’t going to need it。 YAGNI并不是不做架构性思考的意思,而是应该融为一体,将重构作为基础。

重构和软件开发过程

大部分的敏捷项目都只是徒有其名,真正的敏捷方式的运作,团队成员必须要在重构上有能力、有激情,采用的开发过程必须与常规的、持续的重构相匹配。

  • 自测试代码
  • 持续继承
  • 重构 有了这些核心实践打下的基础,才能让项目始终处于可发布的状态,做到一天多次发布,同时还增加了软件的可靠性、减少耗费在bug上的时间

重构和性能

并不赞同为了提高设计的纯洁性而忽视性能。我们应该先写出可调优的软件,然后调优它以获得足够的速度。

  • 时间预算法:用于性能要求极高的实时系统,在设计师就做好预算,给每个组件做好预算,分配好资源
  • 持续关注法:要求程序员在任何时间做任何事情,都要设法保持系统的高性能。但是通常不会有太大的作用。通常任何修改如果只是为了提高性能,通常会使得程序难以维护。
  • 统计优化法:使用工具去监控程序,看看90%的资源和时间都被什么地方的代码消耗了,着重优化那些地方,找出性能热点。这样,较少的工作量也能有不错的成果。

重构起源于何处

作者找了找,但是没找到。优秀的程序员肯定会花时间来清理自己的代码。整洁的代码肯定比杂乱无章的代码容易修改,而且他们知道,几乎是无法一开始就写出整洁的代码的。 重构对于提高生产力非常重要。 巴拉巴拉,是作者的一些经历和对重构这个概念和技术的推广,然后逐渐被行业广泛接收。(也有负面的效果,Java世界里,重构被滥用,其实只是不严谨的结构调整)

自动化重构

主要靠着IDE实现。比如JetBrains的Intelli IDEA。后面IBM还出了VisualAge,然后是Eclipse等等。现在,只要是正常主流的工具或者编译器,都支持重构。

  • 文本操作,查找替换
  • 提取变量
  • 代码导航
  • 静态检查
  • 理解、修改、处理语法树 这背后的实现,应该是很有挑战的事情,修改和重构之后,还需将结果写回编辑器视图等等。尽管现在的工具已经很强大了,但是也不能掉以轻心,因为某些情况,比如反射等等,编译器并不能很好的处理,依然需要我们强大的测试来保证重构。

延展阅读

  • Josh Kerievsky《重构与模式》
  • Ambler & Sadalage《数据库重构》
  • Harold《重构HTML》
  • Michael Feather 《修改代码的艺术》
  • Jay Fields & Shane Harvey 写了关于Ruby的《重构》
  • 关于本书refactoring.com
Owner Rainbowly
Total [] user views [] site views [] page views
Built with Hugo
主题 StackJimmy 设计