大型重构
- 大型重构可能需要耗时数月甚至数年。在这个过程中,你应该根据需要安排自己的工作,只在需要添加新功能或修补错误时才进行重构。重构程度只要能满足其他任务的需要就行了,反正明天你还可以回来重构。
- 由于大型重构可能需要花费相当长的时间,因此他们并不像其他章节介绍的重构那样,能够立刻让人满意。你必须有那么一点小小的信仰:你每天都在使你自己的程序世界更安全。
- 进行大规模重构时,有必要为整个开发团队建立共识,这时小型重构所不需要的。
一. Tease Apart Inheritance(梳理并分解继承体系)
介绍
- 场景
某个继承体系同时承担两项责任。 - 手法
建立两个继承体系,并通过委托关系让其中一个可以调用另一个。
动机
- 继承是个好东西,它可以明显减少子类中的代码量。
- 继承很容易被误用,并且这种误用很容易在开发者之间蔓延。今天你为了一项小小任务而加入一个小小的子类,明天又为另一项任务在继承体系的另一个地方加入另一个子类。
- 混乱的继承体系是一个严重的问题,因为它会导致重复代码,并且使修改变得困难,因为特定问题的解决策略被分散到了整个继承体系。
- 如果继承体系中的某一特定层级上的所有类,其子类名称都以相同的形容词开始,那么这个体系很可能就是承担着两项不同的责任。
范例
一个混乱的继承体系

分割继承体系

显示风格之间的差异可以用变量来表现

二. Convert Procedural Design to Objects(将过程化设计转化为对象设计)
介绍
- 场景
你手上有一些传统过程化风格的代码。 - 手法
将数据记录变成对象,将大块的行为分成小块,并将行为移入相关对象之中。
动机
- 你往往会面对一些过程化风格的代码所带来的问题,并因此希望他们变得更面向对象一些。
- 典型的情况是:类中有着长长的过程化函数和极少的数据,旁边则是一堆哑数据对象——除了数据访问函数外没有其他任何函数。如果你要转换的是一个纯粹的过程化程序,可能连这些东西都没有。
- 并不是说绝对不应该出现只有行为而几乎没有数据的对象。在策略模式中,我们常使用策略对象来改变宿主对象的行为,这些小型的策略对象就只有行为而没有数据。但这样的对象通常比较小,而且只有在我们特别需要灵活性的时候,才会使用它们。
范例
重构前
第1章售票范例
重构后
分解并重组statement()
三. Separate Domain from Presentation(将领域和表述/显示分离)
介绍
- 场景
某些GUI
类之中包含了领域逻辑。 - 手法
将领域逻辑分离出来,为他们建立独立的领域类。
动机
- 提到面向对象,就不能不提
MVC
(模型-视图-控制器)模式。MVC
模式最核心的价值在于:它将用户界面代码(即:视图;亦即现今常说的“展现层”)和领域逻辑(即模型)分离了。 - 展现类只含用以处理用户界面的逻辑:领域类不含任何与程序外观相关的代码,只含业务逻辑相关的代码。
- 将程序中这两块复杂的部分加以分离,程序未来的修改将变得更加容易,同时也使同一业务逻辑的多种展现方式成为可能。
- 大多数人并没有在设计中采用这种方式来处理
GUI
。大多数客户端/服务器结构的GUI
应用都采用双层逻辑设计:数据保存在数据库中,业务逻辑放在展现类中。
四. Extract Hierarchy(提炼继承体系)
介绍
- 场景
你有某个类做了太多工作,其中一部分工作是以大量条件表达式完成的。 - 手法
建立继承体系,以一个子类表示一种特殊情况。
动机
- 在渐进式设计过程中,常常会有这样的情况:一开始设计者只想以一个类实现一个概念;但随着设计方案的演化,最后却可能一个类实现了两个、三个乃至十个不同的概念。
- 当你遇到这种瑞士军刀般的类——不但能够开瓶开罐、砍小树枝、还能在演示会上打出激光强调重点——你就需要一个好策略(亦即本项重构),将它的各个功能梳理并分开。
重构,复用与现实
- 在很多系统的开发中,大部分成本并不是花在最初版本上,而是花在其后对系统不断的修改和调整上。
- 你可以重写整个程序,依赖自己的设计经验来纠正程序中存在的错误。你可以复制、修改现有系统的一部分,以扩展它的功能。重构是上述两个极端的中庸之道。通过重新组织软件结构,重构使得设计思路更详尽明确。
重构工具
- 重构的最大障碍之一就是:几乎没有工具对它提供支持。
- 那些把重构作为文化成分之一的语言(例如
Smalltalk
)通常都提供了强大的开发环境,其中对代码重构的众多必要特性都提供了支持。但即使是这样的环境,到目前为止,也只是对重构过程提供了部分支持,绝大部分工作仍然得靠手工完成。 - 如果能够把重构变得像调整代码格式那么简单,程序员自然也会乐意像整理代码外观那样去整理系统的设计。而这样的整理对代码的可维护性、可复用性和可理解性,都能够带来深远的正面影响。
- 随着重构成本的降低,设计错误也不再像从前那样带来昂贵代价了。由于弥补设计错误所需的成本降低了,需要预先做的设计也就更少了。预先设计是一项带有预测性质的工作,因为项目激活之时,需求往往还不明朗。
- 拥有自动化重构工具的辅助之后,所需测试少多了,因为很多重构都可以自动进行,无需再做测试。
- 重构工具的技术标准
① 程序数据库
可以搜索程序元素的交叉引用。
例如:找到某个特定函数的所有调用点,找到读/写某个特定实例变量的所有函数。
② 解析树
能够处理函数层面下的一部分系统,通常是对被修改程序元素的引用。
例如:如果某个实例变量被改名,那么其所属类及其子类中对该实例变量的所有引用都必须更新。将某个函数的一部分提炼为一个独立函数。对于函数任何修改都必须能够处理函数结构,因此我们需要解析树的帮助。
③ 准确性
由于工具实现的重构,必须合理保持程序原有行为。
对于绝大多数程序来说,重构可以相当准确。只要可能破坏重构准确性的因素都被识别出来,重构技术员就可以避免在不适当时候进行重构,也可以避免对于重构工具无法修补的程序错误地进行手工修补。 - 重构工具的实用标准
① 速度
重构前的分析和必要调整,可能会耗费较多时间,因为他们有可能非常复杂。如果重构前需要大量准备工作,程序员就不会使用自动化重构工具,他们宁可手工进行重构。
② 撤销
撤销功能可以让我们放心尝试,不会遭受任何惩罚,因为我们总是可以回到原先的任何一个版本。
③ 与其他工具集成
集成开发环境(IDE
)已经成为绝大多数开发项目的核心工具。重构工具集成于集成开发环境有利于更多开发者使用它。
总结
- 你已经了解了重构的基础,但还不知道何时应该使用它们、何时不应该使用;何时开始、何时停止;何时前进、何时等待。使重构能够成功的,不是前面各自独立的技术,而是这种节奏。
- 当你开始冷静下来的时候,对自己的重构技艺感到绝对自信——无论别人留下的代码多么杂乱无章,你都可以将它变好,好到足够进行后续的开发——那时你就知道,自己已经“得道”了。
- 只要有光,你就可以前进,虽然谨慎却仍然自信。但是,一旦太阳下山,你就应该停止前进;夜晚你应该睡觉,并且相信明天早晨太阳仍然升起。
- 重构是一种可以学习的技术,如何学习?
① 虽然挑一个目标
某个地方的代码开始发臭了,你就应该将问题解决掉。你应该朝着目标前进,达成目标后就停止。你之所以重构,不是为了探索真善美(至少不全是),而是为了让你的系统更容易被人理解,为了防止程序变得散乱。
② 没把握就停下来
朝目标前进的过程中,可能会有这样的时候:你无法再自信满满的进行下一步,无法证明自己所做的一切能够保持程序原本的语义。此时你就应该停下来。如果代码已经改善了一些,就发布你的成果;如果没有,就撤销所有修改。
③ 经常原路返回
重构的原则不好学,而且很容易遗失准头。每次重构之后一定要运行所有测试。如果出错了,应该回到最近一个没有出错的状态,而不是试图让他们再次正常运行。
④ 二重奏
和别人一起重构,可以收到更好的效果。两人结对时,对于任何一种软件开发都有很多好处,对于重构也不例外。与搭档交谈时,你必须把刚刚做的事情讲出来,交谈过程中有助于你更清楚了解如何让个别的重构项适应整个重构节奏。 - 即使你已经在你的重构目标(代码)中工作了好几年,一丝一缕了然于胸,但只要发现其中的坏味道,以及消除坏味道的重构手法,你就有可能看到程序的另一种可能。
- 没有一位经理愿意听到他的开发人员说“我们要停工三个月来清理以前的代码”。而且开发人员本来也就不应该这样做。大规模的重构只会带来灾难。
- 你前面的代码也行看起来混乱极了,不要着急,一点一点慢慢地解决这些问题。当你想要添加新功能时,用上几分钟时间把代码整理一下。重构可以使你更好理解代码的作用和工作方式,这使得新功能的添加更容易,而且重构之后代码的质量也会大大提高。
- 永远不要忘记“两顶帽子”:添加新功能,以及重构。添加新功能时,你不应该修改既有代码。重构时,应该保持代码功能完全不变。
网友评论