重构:对软件内部结构的一种调整,目的是在不改变软件可观察行为的前提下,提高其可理解性,降低其修改成本。
重构:使用一系列重构手法,在不改变软件可观察行为的前提下,调整其结构。
- 如果你发现自己需要为程序添加一个特性,而代码结构使你无法很方便地达成目的,那就先重构那个程序,使特性的添加比较容易进行,然后再添加特性
- 重构的第一步:为即将修改的代码建立一组可靠的测试环境。重构之前,首先检查自己是否有一套可靠的测试机制。这些测试必须有自我检验能力
- 重构技术就是以微小的步伐修改程序。如果你发现错误,很容易便可发现它。
- 任何一个傻瓜都能写出计算机可以理解的代码。唯有写出人类容易理解的代码,才是优秀的程序员。更改变量名称,提高代码清晰度。代码应该表现自己的目的
- 尽量去除一些无意义的临时变量,临时变量往往引发问题,会导致大量参数被传来传去,很容易跟丢它们,尤其在长长的函数中
- 两顶帽子:重构时应该把时间分配给两种行为:添加新功能和重构。添加新功能不能修改既有代码,只添加新功能;重构不能再添加新功能,只管改进程序结构,此时也不应添加任何测试
- 代码结构的流失是累积性的。越难看出代码所代表的设计意图,就越难保护其中的设计
- 消除重复代码:代码越多,正确的修改就越困难
- "准确说出我想要的"
- 重构使代码更容易理解和阅读。重构可以帮助我们写出更强健的代码。重构帮助我们快速地开发程序
- 三次法则:事不过三,三则重构
- 第一次做某件事时只管去做
- 第二次做类似的事情就会产生反感,但无论如何还是可以去做
- 第三次再做类似的事情,就应该重构
- 重构时机:
- 添加功能时重构
- 修补错误时重构:如果收到一份错误报告,这就是重构的信号
- 复审代码时重构
- 难以阅读的代码,难以修改
- 逻辑重复的代码,难以修改
- 添加新行为时需要修改已有代码的程序,难以修改
- 带复杂条件逻辑的程序,难以修改
- 好程序的特性:
- 容易阅读
- 所有逻辑都只在唯一地点指定
- 新的改动不会危及现有行为
- 尽可能简单表达条件逻辑
- 计算机是这样一门科学:它相信所有问题都可以通过增加一个间接层来解决
- 间接层是一把双刃剑
- 允许逻辑共享
- 分开解释意图和实现
- 隔离变化
- 封装条件逻辑
- 间接层可能导致对象过多
- 重构的难题
- 数据库:在对象模型和数据库模型之间插入一个分隔层,隔离两个模型各自的变化
- 修改接口:如何面对那些必须修改"已发布"接口(找不到,即使找到也不能修改的接口)的重构手法?
- 让旧接口调用新接口。当你要修改某个函数名称时,请留下旧函数,让它调用新函数。千万不要复制函数实现,它会让你陷入重复代码的泥淖中难以自拔
- 除非真有必要,不要发布接口。改版代码所有权政策,让每个人都可以修改别人的代码,以适应接口的改动
- 不要过早发布接口。请修改你的代码所有权政策,使重构更顺畅
- 何时不应该重构
- 有时候重新编写代码比重构还简单时,就不应该重构。重写而非重构的一个清楚讯号就是:现有代码根本不能正常运作。重构之前,代码必须起码能够在大部分情况下正常运作
- 将"大块头"重构为良好的小组件,然后可以逐一对组件做出"重构或重建"的决定
- 项目已近最后期限,应该避免重构
- 重构可以带来更简单的设计,同时又不损失灵活性,这降低了设计过程的难度,减轻了设计压力
- 哪怕你完全了解系统,也请实际度量它的性能,不要臆测。臆测会让你学到一些东西,但十有八九是错的
- 编写快速软件的秘密就是:首先写出可调的软件,然后调整它以求获得足够速度
- 编写快速软件的方法:
- 时间预算法:给每个组件预先分配一定的资源:时间和执行轨迹
- 持续关注法:在任何时间做任何事情都要设法保持系统的高性能
- 利用统计数据:编写程序时不对性能投以特别的关注,直至进入性能优化阶段
- 代码的坏味道
- 重复代码
- Extract Method:提炼出重复代码
- Pull Up Method:将方法推入超类
- Form Template Method:构建模板方法
- Substitute Algorithm:替换算法
- Extract Class:提炼出类
- 过长函数
- 让小函数容易理解的关键是有一个好名字
- 每当感觉需要以注释来说明点什么的时候,我们就把需要说明的东西写进一个独立函数中,并以其用途(而非实现方法)命名
- 关键不在于函数的长度,而在于函数"做什么"和"如何做"之间的语义距离
- Extract Method
- Replace Temp With Query 消除临时元素
- Introduce Parameter Object 和 Preserve Whole Object 可以将过长的参数列变得简洁一些
- Replace Method With Method Object
- 寻找注释:如果代码前方有一行注释,就是在提醒你可以将这段代码替换为一个函数,而且可以在注释的基础上给这个函数命名。就算只有一行代码,如果需要以注释来说明,那也值得将它提炼到独立的函数中。
- 条件表达式和循环常常也是提炼的信号,可以使用 Decompose Conditional 处理条件表达式
- 循环应该将循环和其内部代码提炼到一个独立的函数中
- 过大的类
- Extract Class
- Extract Subclass
- 先确定客户端如何使用它们,然后运用 Extract Interface 为每一种使用方式提炼出一个接口
- Duplicate Observed Data
- 过长参数列
- 使用对象
- Replace Parameter with Method
- Preserve Whole Object:将来自同一个对象的一堆数据收集起来,并以该对象替换它们
- Introduce Parameter Object:构造参数对象
- 例外:有时候不希望造成"被调用对象"与"较大对象"间的某种依赖关系,这时候将数据从对象中拆解出来单独作为参数,也是合理的
- 如果参数列太长或变化太频繁,就需要重新考虑自己的依赖结构了
- 发散式变化
- 软件是需要能够容易被修改的,一旦需要修改,我们希望能够跳到系统的某一点,只在该处做修改。如果不能做到这一点,就发现了两种紧密相关的刺鼻味道中的一种了
- 如果某个类经常因为不同的原因在不同的方向上发生变化,Divergent Change(发散式变化)就出现了
- 针对某一外界变化的所有相应修改,都只应该发生在单一类中,而这个新类内的所有内容都应该反应此变化
- 应该找出某特定原因而造成的所有变化,然后运用 Extract Class 将它们提炼到另一个类中
- 散弹式修改
- 如果遇到某种变化,你都必须在许多不同的类内做许多小修改,面临的就是散弹式变化(Shotgun Surgery)
- 使用 Move Method 和 Move Field 把所有需要修改的代码放进同一个类,如果目前没有合适的类可以安置这些代码,就造一个
- 通常可以运用 Inline Class 把一系列相关行为放进同一个类
- 发散式变化是指一个类受到多种变化的影响
- 散弹式变化是指一种变化引发多个类做相应的修改
- 修改代码,使「外界变化」与「需要修改的类」趋于一一对应
- 依恋情结(Feature Envy)
- 函数对某个类的星期高过对自己所处的类的兴趣,这种孺慕之情最通常的焦点便是数据
- 使用 Move Method 把它移到它该去的地方
- 使用 Extract Method 把受影响的一部分提炼到独立的函数中,再使用 Move Method 把它移动到特定的类中
- 如果一个函数使用到了多个类的功能,判断哪个类拥有最多被此函数使用的数据,然后把这个函数和这些数据摆在一起
- 最根本的原则是:将总是一起变化的东西放在一块儿。
- 数据和引用这些数据的行为总是一起变化,但也有例外。如果例外出现,我们就搬移那些行为,保持变化只在一地发生
- 数据泥团(Data Clumps)
- 总是绑在一起出现的数据应该拥有自己的对象
- 首先找出这些数据已字段形式出现的地方,运用 Extract Class 将它们提炼到一个独立的对象中
- 然后将注意力转移到函数签名上,运用 Introduce Parameter Object 或 Preserve Whole Object 为它减肥
- 好处:将很多参数列缩短,简化函数调用
- 不必在意 Data Clumps 只用上新对象的一部分字段,只要以新对象取代两个(或更多)字段,就值回票价了
- 如果删除一个数据项,其他数据失去意义,则应该为它们产生一个新的对象。得到新对象后,还可以着手寻找依恋情结(Feature Envy),这可以帮你指出可以移至新类中的种种程序行为
- 基本类型偏执(Primitive Obsession)
- 大多数编程环境都有两种数据:
- 结构类型允许你将数据组织成有意义的形式
- 基本类型则是构成结构类型的积木块
- 对象存在的一个极大价值在于:它们模糊了横亘于基本数据和体积较大的类之间的界限。你可以轻松编写出一些与语言内置(基本)类型无异的小型类
- 你可以运用 Replace Data Value With Object 将原本单独存在的数据值替换为对象,从而走出传统的窟窿,进入炙手可热的对象世界
- 如果替换的数据值是类型码,而它并不影响行为,则可以运用 Replace Type Code With Class 将它替换掉
- 如果有与类型码相关的条件表达式,可以运用 Replace Type Code With Subclass 或 Replace Type Code With State/Strategy
- 如果你有一组应该总是被放在一起的字段,可以运用 Extract Class
- 如果在参数列中看到基本类型,可以使用 Introduce Parameter Object
- 如果发现自己正在从数组中挑选数据,可以运用 Replace Array With Object
- 大多数编程环境都有两种数据:
- Switch Statements
- 面向对象程序员的一个最明显特征就是:少用 Switch 语句
- 如果要为 Switch 添加一个新的 case 子句,就必须找到所有 Switch 语句并修改它们,面向对象的多态概念为此提供了优雅的解决办法
- 大多数时候,一看到 Switch 语句,就应该考虑以多态来替换它
- 使用 Extract Method 将 Switch 语句提炼到一个独立函数中,再使用 Move Method 将它搬移到需要多态性的那个类中。此时需要决定是否使用 Replace Type Code With Subclass 或 Replace Type Code With State/Strategy。一旦这样继承完成后,就开业运用 Replace Conditional With Polymorphism 了
- 如果只是在单一函数中有选择事例,并且不想改动它们,这种情况下 Replace Parameter With Explicit Methods 是个不错的选择。如果选择条件之一是 null,可以试试 Introduce Null Object
- 平行继承体系(Parallel Inheritance Hierarchies)
- 平行继承体系:当你为某个类增加一个子类,必须也为另一个类增加相应的子类;如果你发现某个继承体系的类名前缀和另一个集成体系的类名前缀完全相同
- 消除平行继承体系的策略是:让一个继承体系的实例引用另一个继承体系的实例;再使用 Move Method 和 Move Field 则可以将引用端的集成体系消弭于无形
- 冗赘类(Lazy Class)
- 如果一个类的所得不值其身价,它就应该消失
- 如果某些子类没有做足够的工作,试试 Collapse Hierarchy。对于几乎没有用的组件,应该以 Inline Class 来对付它们
- 夸夸其谈未来性(Speculative Generality)
- 如果企图以各式各样的钩子和特殊情况来处理一些非必要的事情,这种结果往往是造成系统的难以理解和维护
- 如果所有装置都会被用到,那就值得做,如果用不到,就不值得。用不上的装置会挡住你的路,所以,搬开它吧
- 如果有你的某个抽象类其实没有太大作用,请运用 Collapse Hierarchy
- 不必要的委托可以用 Inline Class 除掉
- 如果函数的某些参数未被用上,可对它实施 Remove Parameter
- 如果函数名称带有多余的抽象意味,应该对它实施 Rename Method,让它实现一些
- 如果函数或者类的唯一用户是测试用例,就会出现 Speculative Generality。如果发现这样的函数或者类,请把它们联通测试用例一起删掉。但如果它们的用途是帮助测试用例检测正当功能,则不应该删掉
- 令人迷惑的临时字段(Temporary Field)
- 有时你会看到这样的对象:其内某个实例变量仅仅是为某种特定请求而设,这样的代码让人不易理解
- 使用 Extract Method 为其抽象出一个类,然后把所有和这个变量相关的代码都放到这个类中。也许你还可以使用 Introduce Null Object 在"变量不合法"的情况下创造一个 Null 对象,从而避免写出条件式代码
- 如果类中有一个复杂的算法,需要好几个变量,往往就可能导致 Temporary Field 的出现,这些字段只有在使用该算法时才有效,其他情况下只会让人迷惑,这时候应该用 Extract Class 把这些变量和其相关函数提炼到一个单独的类中
- 过度耦合的消息链(Message Chains)
- 对象之间出现链式调用:A -> B -> C,这回导致一旦对象之间的关系发生任何变化,客户端就需要作出相应的修改
- 使用 Hide Delegate 进行重构
- 先观察消息链最终得到的对象是用来干什么的,看能否使用 Extract Method 把使用该对象的代码提炼到一个独立函数中,再运用 Move Method 把这个函数推入消息链
- 中间人(Middle Man)
- 对象的基本特征是封装:对外部世界隐藏其内部细节,封装往往伴随委托
- 过度运用委托:某个类接口有一半的函数都委托给其他类,这就是过度委托
- 这时应该运用 Remove Middle Man,直接和真正负责的对象打交道
- 如果这些函数只有少数几个,可以运用 Inline Method 将它们放进调用端
- 如果这些 Middle Man 还有其他行为,可以运用 Replace Delegation With Inheritance 把它变成实责对象的子类
- 狎昵关系(Inappropriate Intimacy)
- 两个类过于亲密,花费太多时间探究彼此的 private 成分
- 过分狎昵的类必须拆散
- 可以使用 Move Method 和 Move Field 帮它们划清界限,从而减少狎昵行径
- 也可以试试使用 Change Bidirectional Association to Unidirectional 让其中一个类对另一个类斩断情丝
- 如果两个类实在是情投意合,可以运用 Extract Class 把两者共同点提炼到一个安全地点
- 也可以尝试运用 Hide Delegate 让另一个类来为它们传递
- 继承往往造成过度亲密,因为子类对超类的了解总是超过后者的主观愿望,如果你认为子类该独自存在,请使用 Replace Inheritance With Delegation 让它离开继承体系
- 异曲同工的类(Alternative Classes With Different Interfaces)
- 如果两个函数做同一件事,却有不同的签名,请运用 Rename Method 根据它们的用途重新命名;请反复运用 Move Method 将某些行为移入类,直到两者协议一致;也可以运用 Extract SuperClass,提炼出超类
- 不完美的库类(Incomplete Library Class)
- 复用常被视为对象的终极目的,但是复用的意义被高估
- 如果想修改苦累的一两个函数,可以运用 Introduce Foreign Method
- 如果想添加一大堆额外行为,可以运用 Introduce Local Extension
- 纯稚的数据类(Data Class)
- Data Class 是指它们拥有一些字段,以及用于访问(读写)这些字段的函数,除此之外一无长处
- 运用 Encapsulate Field 或 Encapsulate Collection 将他们封装起来
- 对于那些不应该被其他类修改的字段,使用 Remove Setting Method
- 找出那些取值和设置值的函数被其他类运用的点,尝试以 Move Method 把那些调用行为搬移到 Data Class 来;如果无法搬移整个函数,就运用 Extract Method 产生一个可以被搬移的函数;然后就可以把这些取值和设置值的函数隐藏起来
- Data Class 作为一个起点很好,但是若要让它们像成熟的对象那样参与整个系统的工作,就必须承担一定责任
- 被拒绝的遗赠(Refused Bequest)
- 子类应该继承超类的函数和数据
- 如果子类不需要或者不想继承,就需要为整个子类新建一个兄弟类,再运用 Push Down Method 和 Push Down Field 把所有用不到的函数下推给那个兄弟,这样超类就只持有所有子类共享的东西
- 所有超类都应该是抽象的
- 即使你不愿意继承接口,也不要胡乱修改继承体系,应该运用 Replace Inheritance With Delegation 来达到目的
- Comments(过多的注释)
- 注释之所以存在是因为代码很糟糕
- Comments 可以带我们找到很多坏味道,找到坏味道后我们应该以各种重构手法把坏味道去除。完成之后我们常常会发现:注释已经变得多余了,因为代码已经清楚说明了一切
- 如果你需要注释来解释一块代码做什么,试试 Extract Method
- 如果函数已经提炼出来,但还是需要注释来解释其行为,试试 Rename Method
- 如果你需要注释来说明某些系统的需求规格,试试 Introduce Assertion
- 当你感觉需要撰写注释时,请先尝试重构,试着让所有注释都变得多余
- 如果你不知道该做什么,这才是注释的良好运用时机
- 除了用来记述将来的打算之外,注释还可以用来标记你并无十足把握的区域。你可以在注释里写下自己"为什么做某某事"。这类信息可以帮助将来的修改者
- 重复代码
如果想要进行重构,首要前提就是拥有一个可靠的测试环境。
类应该包含它们自己的测试代码。每个类都应该有一个测试函数,并且以它来测试自己这个类。
确保所有测试都完全自动化,让它们检查自己的测试结果
一套测试就是一个强大的 bug 侦测器,能够大大缩减查找 bug 所需的时间
撰写测试代码的最有用时机是在开始编程之前。当你需要添加新的特性时,先写相应的测试代码。编写测试代码其实就是在问自己:添加这个功能需要做些什么。编写测试代码还能使你把注意力 集中于接口而非实现。预先写好的测试代码还能为你的工作按上一个明确的结束标志:一旦测试代码正常运行,工作就可以结束了。
在重构前需要改造这些代码,使其能够自我测试。