第三章、代码的坏味道
培养出自己的判断力——能够清晰地表达自己的观点
- 目的:清晰、容易理解、易于修改的代码
3.1 重复代码(Duplicated Code)
方法:
- 提炼函数(Extract Method)P110
- 函数上移(Pull Up Method)P322
- 塑造模板函数(From Template Method)P345
- 替换算法(Substitute Algorithm)P139
- 提炼类(Extract Class)P149
含义:
- 你有一段代码可以被组织再一起并独立起来——将这段代码放进一个独立函数中,并让函数名称解释该函数的作用
- 有些函数再各个子类中产生完全相同的结果——将该函数移至超类
- 你有一些子类,期中相应的某些函数以相同的顺序执行类似的操作,但各个操作再细节上有所不同——将这些操作分别放进独立函数中,并保持他们有相同的签名,于是原函数也就变得相同了。然后将原函数移至超类
- 你想要把某个算法替换为另一个更清晰的算法——将函数本体替换为另一个算法
- 某个类做了应该由两个类做的事——建立一个新类,将相关字段和函数从旧类搬移到新类
阅读:
- 提炼函数(Extract Method)
- 将两个函数相同的部分提取出来(相同的部分可以是:代码、流程等)
- 函数上移(Pull Up Method)
- 将子类中做相同事情的函数,提升为超类中的方法
- 塑造模板函数(From Template Method)
- 也是对于子类中大部分相同的方法,提取出共有流程,然后不同的细节由子类的提取方法来完成
- 替换算法(Substitute Algorithm)
- 做相同的事情的代码一般上都会更简单的算法来实现
- 提炼类(Extract Class)
- 一个类应该是明确的抽象,处理一些明确的责任。是改善并发的常用技术,因为可以为两个类分别加锁
问题 | 解决 | 备注 |
---|---|---|
同一个类的两个函数含有相同的表达式 | 1 | |
两个子类含有相同的表达式 | 1,2 | |
两个子类含有相似的表达,逻辑过程相同 | 1,2,3 | |
函数以不同的算法,做相同的事情 | 4 | |
毫不相关的类出现Duplicated Code | 5 | 或第三方类(util.class) |
3.2 长函数(Long Method)
函数式编程、利用所有的代码段组合,来完成大型的特定功能
解释:
- “间接层”所能带来的全部利益——解释能力、共享能力、选择能力——都是由小型函数支持的P61
- 让小函数容易理解的真正关键在于一个好的名字,这样就可以免去返回查看其他函数做了什么
- 最终效果:应该积极的分解函数
- 原则:每当感觉需要注释来说明点什么东西,就吧需要说明的东西移动到独立函数里,并以其用途(而非实现手法)命名
- 注释的对象:一行、一组代码,哪怕替换后的函数调用动作比自身函数还长,只要函数名称能够解释其用途即可。关键不在于函数的长度,而在于函数“做什么”和“如何做”之间的语义距离
- 99%的场合里,只要提炼方法即可
- 如果函数里面有大量的参数和临时变量,
方法:
- 提炼函数(Extract Method)P110
- 以查询代替临时变量值(Replace temp with Query)P120
- 引入参数对象(Introduce Parameter Object)P295
- 保持对象完整(Preserve Whole Object)P288
- 以函数对象取代函数(Replace Method with Method Object)P135
- 分解条件表达式(Decompose Conditional)P238
含义:
- 你有一段代码可以被组织再一起并独立起来——将这段代码放进一个独立函数中,并让函数名称解释该函数的作用
- 你的程序以一个临时变量保存某一表达式的运算结果——将这个表达式提炼到一个独立函数中。将这个临时变量的所有引用点替换为对新函数的调用,此后,新函数就可以被其他函数使用
- 某些参数总是很自然地同时出现——以一个对象取代这些参数
- 你从某个对象中取出若干值,将他们作为某一次函数调用时的参数——改为传递整个对象
- 你有一个大型函数,其中对局部变量的使用使你无法采用Extract Method——将这个函数放进一个单独对象中,如此一来局部变量就成了对象内的字段。然后你可以再同一个对象中将这个大型函数分解为多个小型函数
- 你有一个复杂的条件(if-then-else)语句——从if、then、else三个段落分别提炼出独立函数
阅读:
- 提炼函数(Extract Method)
- 将两个函数相同的部分提取出来(相同的部分可以是:代码、流程等)
- 以查询代替临时变量值(Replace temp with Query)
- 临时变量的问题在于它是局部的,所以会让你写出更长的函数
- 如果把它替换为一个查询函数,那没在该类所有的方法都可以访问到该变量信息。
- 但有时难以提炼(临时变量被多次赋值等),可以尝试用分解临时变量(Split Temporary Variable)或将查询函数和修改函数分离(Sperate Qurey form Modifer)使情况变得简单一些,然后再替换临时变量
- 引入参数对象(Introduce Parameter Object)
- 对于数据泥团(Data Clumps),可以运用对象包装这些数据。
- 然后也可以发现可以移动到新类中的通用方法(常常是对包装的数据处理代码段),这样就进一步地降低了代码的理解和修改难度
- 保持对象完整(Preserve Whole Object)
- 原因在于:万一函数将来需要新的数据项,就需要修改所有的调用处
- 而对象则可以简单的增加信息,函数就能获取到新的数据项,而不修改方法参数。如果这样会使得你的代码结构恶化,那就不要使用该方法
- 对于函数只传递对象的期中一项数据,那么只传递数值还是传递对象?
- 也许传递对象会有一点的性能问题,但是更重要的在于对象之间的依赖关系
- 如果被调用的函数使用了来自另一个对象的很多项数据,这可能意味着该函数应该被定义在那些数据所属的对象中,即需要考虑搬移函数(Move Method)
- 以函数对象取代函数(Replace Method with Method Object)
- 对于大量存在的局部变量,会导致难以分解一个大型函数
- 建立一个新类,根据待处理函数的用途,为这个类命名,字段为那些临时变量
- 创建构造函数,comput()用于处理需提取的函数,然后引用该函数的调用对象,用于访问调用对象的的值
- 分解条件表达式(Decompose Conditional)
- 复杂的条件逻辑是最常导致复杂度上升的地点之一
- 对于嵌套的条件表达式:以卫语句取代嵌套条件表达式(Replalce Nested Conditional with Guard Clauses)
- 同样也是再拉近代码和语义解释之间的距离
问题 | 解决 | 备注 |
---|---|---|
每当需要通过注释来解释说明的时候 | 1 | |
函数中有单一且唯一赋值的临时变量 | 2 | |
调用函数的参数过多或临时变量很多 | 3,4 | |
调用函数的参数和临时变量都很多 | 5 | |
函数里碰到复杂的条件表达式和循环 | 6 |
3.3 过大的类(Larger Class)
解释:
- 利用单个类做太多的事情,导致出现太多的实例变量
- 同样会出现许多的重复函数
方法:
- 提炼类(Extract Class)P149
- 提炼子类(Extract Subclass)P330
- 提炼接口(Extract Interface)P341
含义:
- 某个类做了应该由两个类做的事——建立一个新类,将相关字段和函数从旧类搬移到新类
- 类中的某些特性只被某些(并非全部)实例用到——新建一个子类,将上诉的那一部分特性移到子类中
- 若干客户使用类接口中的同一子集,或者两个类的接口有部分相同——将相同的子集提炼到一个独立的接口中
阅读:
- 提炼类(Extract Class)
- 一个类应该是明确的抽象,处理一些明确的责任。是改善并发的常用技术,因为可以为两个类分别加锁
- 提炼子类(Extract Subclass)
- 与提炼类(Extract Class)相比,先处理函数再处理数据会更简单一点
- 提炼接口(Extract Interface)
- 对于使用到类中的一部分功能,可以考虑将其划分到接口中
- 这样可以使得程序调用更加地清晰,以及将来可以接入更多的功能,只有实现了该接口即可
- 和提炼超类(Extract Superclass)有相似之处,他只能提炼公共接口,不能提供通用代码
- 可能会导致重复代码,可以使用提炼类(Extract Class)将通用行为,放到组件里面,然后委托该类执行
- 还有一种用法是,用接口来表示外部服务
3.4 过长参数列(Long Parameter List)
解释:
- 导致难以理解
- 后期添加新的数据,会导致需要修改太多地方
方法:
- 用函数代替参数(Replace Parameter with Method)P292
- 保持对象完整(Preserve Whole Object)P288
- 引入参数对象(Introduce Parameter Object)P295
含义:
- 对象调用某个函数,并将所得的结果作为参数,传递给另一个函数,而接受该参数的函数本身也能调用前一个函数——让参数接收者去除该项参数,并直接调用前一个函数
- 你从某个对象中取出若干值,将他们作为某一次函数调用时的参数——改为传递整个对象
- 某些参数总是很自然地同时出现——以一个对象取代这些参数
阅读:
- 用函数代替参数(Replace Parameter with Method)
- 办法一:看看接收端是否能和调用端采用相同的计算来获取到参数值
- 以明确的函数取代参数(Replace Parameter with Explicit Methods) P285
- 以上如,工程方法的调用根据不同的类型创建不同的对象
- 为了以后接口的灵活,而预留的多余参数:可以预算一下修改的成本 & 是否需要降低各个部位之间的依赖
- 保持对象完整(Preserve Whole Object)
- 原因在于:万一函数将来需要新的数据项,就需要修改所有的调用处
- 而对象则可以简单的增加信息,函数就能获取到新的数据项,而不修改方法参数。如果这样会使得你的代码结构恶化,那就不要使用该方法
- 对于函数只传递对象的期中一项数据,那么只传递数值还是传递对象?
- 也许传递对象会有一点的性能问题,但是更重要的在于对象之间的依赖关系
- 如果被调用的函数使用了来自另一个对象的很多项数据,这可能意味着该函数应该被定义在那些数据所属的对象中,即需要考虑搬移函数(Move Method)
- 引入参数对象(Introduce Parameter Object)
- 对于数据泥团(Data Clumps),可以运用对象包装这些数据。
- 然后也可以发现可以移动到新类中的通用方法(常常是对包装的数据处理代码段),这样就进一步地降低了代码的理解和修改难度
3.5 发散式变化(Divergent Change)
解释:
- “一个类受多种变化的影响”
- 我们希望的是,一个类受到的影响尽量只有一种
方法:
- 提炼类(Extract Class)P149
含义:
- 类中的某些特性只被某些(并非全部)实例用到——新建一个子类,将上诉的那一部分特性移到子类中
阅读:
- 提炼类(Extract Class)
- 一个类应该是明确的抽象,处理一些明确的责任。是改善并发的常用技术,因为可以为两个类分别加锁
3.6 散弹式修改(Shotgun Surgery)
解释:
- 类似Divergent Change,但也可以看做类的联系较为耦合,修改时需要修改这些因为外部状态改变下的方法
- “表示一种变化引发多个类相应修改”
- 常常我们希望“外界变化”与“需要修改的类”趋于一一对应
方法:
- 搬移函数(Move Method)P142
- 搬移字段(Move Field)P146
- 将类内联化(Inline Class)P154
含义:
- 你的程序中,有个函数与其所在的类之外的类进行更多的交流(往往是因为另一个类的数据):调用后者,或者被前者调用——在该函数最常引用的类中建立一个与该方法行为类似的新函数。将旧函数编程一个单纯的委托函数,或者完全移除旧函数
- 再你的程序中,某个字段被其所在的类之外的类更多的使用——再目标类新建一个字段,修改源字段的所有用户,令它们改用新字段
- 某个类没有做太多事情——将这个类的所有特性搬移到另一个类中,然后移除原类
阅读:
- 搬移函数(Move Method)
- “搬移函数”是重构理论的支柱
- 对于该函数如果引用了源类的函数,需要考虑将源类作为参数传递给方法
- 搬移字段(Move Field)
- 再类之间移动状态和行为,是重构之中不可缺少的部分
- 再使用Extract Class时,通常先搬移字段,后搬移函数
- 对于该类中的许多函数都用到了该字段,采用Self Encapsulate Field,这样只有修改访问函数即可
- 将类内联化(Inline Class)
- 正好与Extract Class相反,如果一个类不在承担足够的责任,不在有单独存在的理由
3.7 依恋情节(Feature Envy)
解释:
- 对象技术的要点:“将数据和对数据的操作行为包装在一起”
- 对于一个函数用到多个类的情况下,判断哪个类拥有最多被此函数使用的数据,就搬移到那个类中
- 对于上诉的函数,先采用Extract Class会比较容易一些
方法:
- 提炼函数(Extract Method)P110
- 搬移函数(Move Method)P142
含义:
- 你有一段代码可以被组织再一起并独立起来——将这段代码放进一个独立函数中,并让函数名称解释该函数的作用
- 你的程序中,有个函数与其所在的类之外的类进行更多的交流(往往是因为另一个类的数据):调用后者,或者被前者调用——在该函数最常引用的类中建立一个与该方法行为类似的新函数。将旧函数编程一个单纯的委托函数,或者完全移除旧函数
阅读:
- 提炼函数(Extract Method)
- 将两个函数相同的部分提取出来(相同的部分可以是:代码、流程等)
- 搬移函数(Move Method)
- “搬移函数”是重构理论的支柱
- 对于该函数如果引用了源类的函数,需要考虑将源类作为参数传递给方法
3.8 数据泥团(Data Clump)
解释:
- 两个类中相同的字段、函数的相同参数
- 这些总是出现再一起的数据应该拥有他们自己的对象
- 这样做可以缩短参数列表,简化函数的调用
方法:
- 提炼类(Extract Class)P149
- 引入参数对象(Introduce Parameter Object)P295
- 保持对象完整(Preserve Whole Object)P288
含义:
类中的某些特性只被某些(并非全部)实例用到——新建一个子类,将上诉的那一部分特性移到子类中
某些参数总是很自然地同时出现——以一个对象取代这些参数
你从某个对象中取出若干值,将他们作为某一次函数调用时的参数——改为传递整个对象
阅读:
- 提炼类(Extract Class)
- 一个类应该是明确的抽象,处理一些明确的责任。是改善并发的常用技术,因为可以为两个类分别加锁
- 引入参数对象(Introduce Parameter Object)
- 对于数据泥团(Data Clumps),可以运用对象包装这些数据。
- 然后也可以发现可以移动到新类中的通用方法(常常是对包装的数据处理代码段),这样就进一步地降低了代码的理解和修改难度
- 保持对象完整(Preserve Whole Object)
- 原因在于:万一函数将来需要新的数据项,就需要修改所有的调用处
- 而对象则可以简单的增加信息,函数就能获取到新的数据项,而不修改方法参数。如果这样会使得你的代码结构恶化,那就不要使用该方法
- 对于函数只传递对象的期中一项数据,那么只传递数值还是传递对象?
- 也许传递对象会有一点的性能问题,但是更重要的在于对象之间的依赖关系
- 如果被调用的函数使用了来自另一个对象的很多项数据,这可能意味着该函数应该被定义在那些数据所属的对象中,即需要考虑搬移函数(Move Method)
3.9 基本类型偏执(Primitive Obsession)
解释:
- 大多数编程环境都允许将数据组织成有意义的形式,结构体、类等
- 对象的一个极大价值在于:他们模糊了基本数据类型和体积较大的类之间的界限?
- 例如:以类String、Date表示字符串和日期类,进而进入对象编程的世界
方法:
- 以对象取代数据项(Replace Data Value with Object)P175
- 以类取代类型码(Replace Type Code with Class)P218
- 以子类取代类型码(Replace Type Code with Subclasses)P223
- 以State/Strategy取代类型码(Replace Type Code with State/Strategy)P227
- 提炼类(Extract Class)P149
- 引入参数对象(Introduce Parameter Object)P295
- 以对象取代数组(Replace Array with Object )P186
含义:
- 你有一个数据项,需要与其他数据和行动为一起使用才有意义——将数据项编程对象
- 类之中有一个数值类型码,但它并不影响类的行为——以一个新的类替换该数值类型码
- 你有一个不可变的类型码,它会影响子类的行为——以子类取代这个类型码
- 你有一个类型码,它会影响类的行为,但你无法通过继承手法消除它——以状态对象取代类型码
- 类中的某些特性只被某些(并非全部)实例用到——新建一个子类,将上诉的那一部分特性移到子类中
- 某些参数总是很自然地同时出现——以一个对象取代这些参数
- 你有一个数组,其中的元素各自代表不同的东西——以对象代替数组。对于数组中的每个元素,以一个字段来表示
阅读:
- 以对象取代数据项(Replace Data Value with Object)
- 开发初期,使用简单的数据来表示简单的情况。后期可能发现需要改变的东西比较多,情况变得复杂起来
- 以类取代类型码(Replace Type Code with Class)
- 提高代码的可读性,能够提供运行期检测
- 为类型类提供工厂函数,这样九可以确保正确的和法的对象呗创建出来
- 只有当类型码是纯粹数据时(就是类型码不会在switch语句中引起行为变化),你才能以类取代他
- 还需注意类型码不会因其数值不同而引起行为上的差异,宿主中的某些行为还是可以移动到类型中的
- 以子类取代类型码(Replace Type Code with Subclasses)
- 以类型码的宿主为基类,针对每一种类型建立相应的子类
- 其主要作用是为了让Replace Conditional with Polymorphism得以实现,否则使用Replace Type Code with Class较为合适,风险也更将低
- 还有一个原因是为了,将宿主类中的只于特定类型有关的属性或者方法移动到合适的子类中去
- 这对未来新添加的行为更容易
- 以State/Strategy取代类型码(Replace Type Code with State/Strategy)
- 和Replace Type Code with Subclasses很相似,但是如果类型码的值在对象的生命周期里发生了变化或者其他原因使得宿主类不能被继承
- 类型码的值在对象的生命周期里发生了变化:这里可以引入一个中间层来完成转换,使它成为一个属性
- 本重构使用State或Strategy模式,这两个模式非常相似,无论选择哪一个重构的过程都是相同的
- 如果打算完成本项重构后使用Replace Conditional with Polymorphism,那么使用Strategy模式比较合适;如果打算搬移与状态相关的数据,而且把新建的对象视为一种变迁状态,那么使用State模式
- 提炼类(Extract Class)
- 一个类应该是明确的抽象,处理一些明确的责任。是改善并发的常用技术,因为可以为两个类分别加锁
- 引入参数对象(Introduce Parameter Object)
- 对于数据泥团(Data Clumps),可以运用对象包装这些数据。
- 然后也可以发现可以移动到新类中的通用方法(常常是对包装的数据处理代码段),这样就进一步地降低了代码的理解和修改难度
- 以对象取代数组(Replace Array with Object )
- 数组常用于容纳一组相似的对象,当用于存放不同的对象时,会导致难以记住顺序
- 通过设置类来完成意义化语义
问题 | 解决 | 备注 |
---|---|---|
小任务上的对象(类似money、range、zipcode) | 1 | |
不影响行为的类型码 | 2 | |
与类型码相关的条件表达式 | 3、4 | |
一组应该总是被放在一起的字段 | 5 | |
再参数列表中看到基本类型 | 6 | |
发现自己正从数组中挑选数据 | 7 |
3.10 switch惊悚现身(Switch Statements)
解释:
- 面向对象程序的一个最明显特征:少用switch或(case)语句,从本质上说,switch语句的问题在于重复,经常会发现同样的switch语句散部在不同的地点,如果要添加新的case语句就要找到所有的switch语句并修改它们
- 大多数时候,一看到switch语句,就应该使用多态来代替他,但是对象单一函数中有些选择事例,且并不想改动它们,那么使用多态就显得杀鸡用牛刀,这时候使用Replace parameter with Explicit Methods 就不错
- 当然也可以使用再if-else语句中
方法:
- 提炼函数(Extract Method)P110
- 搬移函数(Move Method)P142
- 以子类取代类型码(Replace Type Code with Subclasses)P223
- 以State/Strategy取代类型码(Replace Type Code with State/Strategy)P227
- 以多态取代条件表达式(Replace Conditional with Polymorphism)P255
- 以明确函数取代参数(Replace Parameter with Explicit Methods) P285
- 引入 Null 对象(Introduce Null Object)P260
含义:
- 你有一段代码可以被组织再一起并独立起来——将这段代码放进一个独立函数中,并让函数名称解释该函数的作用
- 你的程序中,有个函数与其所在的类之外的类进行更多的交流(往往是因为另一个类的数据):调用后者,或者被前者调用——在该函数最常引用的类中建立一个与该方法行为类似的新函数。将旧函数编程一个单纯的委托函数,或者完全移除旧函数
- 你有一个不可变的类型码,它会影响子类的行为——以子类取代这个类型码
- 你有一个类型码,它会影响类的行为,但你无法通过继承手法消除它——以状态对象取代类型码
- 你手上有个条件表达式,它根据对象类型的不同而选择不同的行为——将这个条件表达式的每个分支放进一个子类内的覆写函数中,然后将原始函数声明稳抽象函数
- 你有一个函数,期中完全取决于参数值而采取不同行为——针对该参数的每一个可能值,建立一个独立函数
- 你需要再三检查某对象是否为null——将null值替换为null对象
阅读:
提炼函数(Extract Method)
- 将两个函数相同的部分提取出来(相同的部分可以是:代码、流程等)
搬移函数(Move Method)
- “搬移函数”是重构理论的支柱
- 对于该函数如果引用了源类的函数,需要考虑将源类作为参数传递给方法
以子类取代类型码(Replace Type Code with Subclasses)
- 以类型码的宿主为基类,针对每一种类型建立相应的子类
- 其主要作用是为了让Replace Conditional with Polymorphism得以实现,否则使用Replace Type Code with Class较为合适,风险也更将低
- 还有一个原因是为了,将宿主类中的只于特定类型有关的属性或者方法移动到合适的子类中去
- 这对未来新添加的行为更容易
以State/Strategy取代类型码(Replace Type Code with State/Strategy)
- 和Replace Type Code with Subclasses很相似,但是如果类型码的值在对象的生命周期里发生了变化或者其他原因使得宿主类不能被继承
- 类型码的值在对象的生命周期里发生了变化:这里可以引入一个中间层来完成转换,使它成为一个属性
- 本重构使用State或Strategy模式,这两个模式非常相似,无论选择哪一个重构的过程都是相同的
- 如果打算完成本项重构后使用Replace Conditional with Polymorphism,那么使用Strategy模式比较合适;如果打算搬移与状态相关的数据,而且把新建的对象视为一种变迁状态,那么使用State模式
以多态取代条件表达式(Replace Conditional with Polymorphism)
- 如果需要改用多态,只需建立一个新的子类,并再其中提供适当的函数
- 类的用户不需要了解这个子类,这就大大降低了系统各部分之间的依赖,使系统升级更加容易
- 也就是把对应类型case语句中的操作移动到子类当中去
以明确函数取代参数(Replace Parameter with Explicit Methods)
- 可以使得接口变得更加清晰,和获得编译期检查
- 也避免了检查所给的参数是否合格
引入 Null 对象(Introduce Null Object)
- 为了减少重复判断某个数值是否为空,往往需要大量的过程代码
- 使用空对象可以带来:减少重复的过程代码、可以像正常对象一样显示、不会破坏系统(设置和普通对象一样的方法&特殊的返回值)
- 但是存在一个问题是:有时会造成问题的侦查和查找上的困难
- 重要的一点:空对象一定是一个常量,他们的任何成分都不应该发生变化,所以采用单例模式比较合适
- 需要的是大多数客户代码都对空对象做出了相同的相应,其他不同的依然可以使用
isNull()
函数 - 经常会看到空对象返回了其他的空对象,对于空对象调用了其他类的方法,同样可以制造空的对象(通过继承来完成)
- 可以通过建立一个接口,来使得不能修改的类使用null对象
- Special Cas:特例的价值,可以降低你错误处理的开销
问题 | 方法 | 备注 |
---|---|---|
switch语句 | 1、2、3、4、5 | 按顺序 |
单一函数中的switch语句 | 6 | |
选择条件之一是null | 7 |
3.11 平行继承体系(Parallel Inheritance Hierarchies)
解释:
- parallel inheritance hierarchies 是 Shotgun Surgery的特殊情况
- 在这种情况下,每当你为一个类添加一个新的子类,就必须要为另一个类添加一个子类
- 如果看到某个继承体系的类名称前缀和另一个继承体系的完全相同,便是这种问题
- 消除这种问题的一般策略是:让另一个继承体系的实例引用另一个继承体系的实例
方法:
- 搬移函数(Move Method)P142
- 搬移字段(Move Field)P146
含义:
- 你的程序中,有个函数与其所在的类之外的类进行更多的交流(往往是因为另一个类的数据):调用后者,或者被前者调用——在该函数最常引用的类中建立一个与该方法行为类似的新函数。将旧函数编程一个单纯的委托函数,或者完全移除旧函数
- 在你的程序中,某个字段被其所在的类之外的类更多的使用——再目标类新建一个字段,修改源字段的所有用户,令它们改用新字段
阅读:
- 搬移函数(Move Method)
- “搬移函数”是重构理论的支柱
- 对于该函数如果引用了源类的函数,需要考虑将源类作为参数传递给方法
- 搬移字段(Move Field)
- 再类之间移动状态和行为,是重构之中不可缺少的部分
- 再使用Extract Class时,通常先搬移字段,后搬移函数
- 对于该类中的许多函数都用到了该字段,采用Self Encapsulate Field,这样只有修改访问函数即可
3.12 冗赘类(Lazy Class)
解释:
- 每一个类都需要人,来维护,来理解
- 对于哪些做工作较少,和不在使用的类,把他们从代码中删除
方法:
- 折叠继承体系(Collapse Hierarchy)P344
- 将类内联化(Inline Class)P154
含义:
- 超类和子类之间无太大的区别——将他们合为一体
- 再你的程序中,某个字段被其所在的类之外的类更多的使用——再目标类新建一个字段,修改源字段的所有用户,令它们改用新字段
阅读:
- 折叠继承体系(Collapse Hierarchy)
- 对于没有带来价值的类,把它和子类合并起来
- 将类内联化(Inline Class)
- 正好与Extract Class相反,如果一个类不在承担足够的责任,不在有单独存在的理由
3.13 夸夸其谈未来性(Speculative Generality )
解释:
- 对未来可能出现的事物,事先完成
- 或者某个函数或者类的唯一用户是测试用例,那么就把他们连同测试用例一起删除
- 如果他们的用途是帮助用力检测正当的功能,可以留下
方法:
- 折叠继承体系(Collapse Hierarchy)P344
- 将类内联化(Inline Class)P154
- 移除参数(Remove Parameter )P277
- 函数改名(Rename Method)P273
含义:
- 超类和子类之间无太大的区别——将他们合为一体
- 再你的程序中,某个字段被其所在的类之外的类更多的使用——再目标类新建一个字段,修改源字段的所有用户,令它们改用新字段
- 函数本体不在需要某个参数——将该参数去除
- 函数名称未能揭示函数的用途——修改函数名称
阅读:
- 折叠继承体系(Collapse Hierarchy)
- 对于没有带来价值的类,把它和子类合并起来
- 将类内联化(Inline Class)
- 正好与Extract Class相反,如果一个类不在承担足够的责任,不在有单独存在的理由
- 移除参数(Remove Parameter )
- 对于多态,就需特殊考虑了。因为某个参数可能需要再另一个状态里使用
- 函数改名(Rename Method)
- 将复杂的处理过程分解成小函数,所以需要设置好小函数的名称,代码首先是为了人写的,其次才是计算机
- 可以先暂时使用新函数调用旧函数,来保证小步前进
3.14 令人迷惑的暂时字段(Temporary Field)
解释:
- 对象内的某个实例变量仅为某种特定情况而设,这样的代码会让人不易理解
- 在一个类中有一个复杂的算法,需要使用到好几个变量来完成,为了避免使用过长的参数列表,则使用字段来表示。但是这些变量只在这个算法里使用,所以使用Extract Class比较好,使之变成一个函数对象
方法:
- 提炼类(Extract Class)P149
含义:
- 某个类做了应该由两个类做的事——建立一个新类,将相关字段和函数从旧类搬移到新类
阅读:
- 提炼类(Extract Class)
- 一个类应该是明确的抽象,处理一些明确的责任。是改善并发的常用技术,因为可以为两个类分别加锁
3.15 过度耦合的消息链(Message Chains)
解释:
- 用户向一个对象请求另一个对象,然后后者又请求另一个对象,然后再请求另一个对象…….这就是消息链
- 这样的耦合度代码,使得一旦对象发生了任何关系上的变化,都会导致客户端不得不做出修改
- 并不是所有的函数链都是不好的,要根据实际情况分析
方法:
- 隐藏委托关系(Hide Delegate)P157
含义:
- 客户通过一个委托类来调用另一个对象——再服务类上建立客户所需要的所有函数,用以隐藏委托关系
阅读:
- 隐藏委托关系(Hide Delegate)
- 封装是对象的最关键特征之一,意味着每个对象都应该尽可能少了解系统的其他部分,这样一来,一旦发生变化,需要了解这一变化的对象就比较少
- 可以随时取消这一层委托(Client->Server->Delegate)
3.16 中间人(Middle Man)
解释:
- 对象的基本特征之一就是封装——对外部世界隐藏其内部细节
- 人们也许会过度使用委托,可能看到某个类的接口有一半的函数都委托了给其他类,可以采用Inline Method
- 如果中间人还有更多的行为,可以使用Replace Delegate with Inheritance
方法:
- 移除中间人(Remove Middle Man)P160
- 内联函数(Inline Method)P117
- 以继承取代委托(Replace Delegate with Inheritance)P355
含义:
- 某个类做了过多的简单委托动作——让客户直接调用受托类
- 一个函数的本体与名称一样清楚易懂——在函数调用点插入函数本体,然后移除该函数
- 你在两个类之间使用委托关系,并经常为整个接口编写许多极简单的委托函数——让委托类继承受托类
阅读:
- 移除中间人(Remove Middle Man)
- 这个是Hide Delegate的相反过程,临界点在于是否过度委托,当然把握这个临界点是比较难说的
- 但也不用在意临界点,只要不断重构就好了
- 内联函数(Inline Method)
- 这和Extract Method是一个相反的过程,核心在于程序是否清晰易读,是否重复等
- 使用该方法的情况二:你手上有一群组织不合理的函数,你可以把它们都内联到一个大型函数中,再从中提取组织合理的小函数
- 使用该方法的情况三:对于使用了太多间接层,使得系统中的函数几乎都是对另一个函数的调用
- 以继承取代委托(Replace Delegate with Inheritance)
- 和Replace Inheritance with Delegate 刚好相反,对于的是编写简单的方法(类似调用等)
- 注意两点:一是如果没有使用受托类的全部方法,就不应该执行此重构,因为子类总是遵循超类的接口;二是受托被对象不止一个其他对象共享,而且受托对象是可变的,因为这样就无法在共享数据了。数据共享必须是委托关系承当的一种责任,你无法把它转给继承关系。
3.17 狎昵关系(Inappropriate Intimacy)
解释:
- 两个类过分地探究互相的私有成分
方法:
- 搬移函数(Move Method)P142
- 搬移字段(Move Field)P146
- 将双向关联改为单向关联(Change Bidirectional Association to Unidirectional)P200
- 提炼类(Extract Class)P149
含义:
- 你的程序中,有个函数与其所在的类之外的类进行更多的交流(往往是因为另一个类的数据):调用后者,或者被前者调用——在该函数最常引用的类中建立一个与该方法行为类似的新函数。将旧函数编程一个单纯的委托函数,或者完全移除旧函数
- 在你的程序中,某个字段被其所在的类之外的类更多的使用——再目标类新建一个字段,修改源字段的所有用户,令它们改用新字段
- 两个类之间有双向关联,但期中一个类如今不在需要另一个类的特性——去除不必要的关联
- 某个类做了应该由两个类做的事——建立一个新类,将相关字段和函数从旧类搬移到新类
阅读:
- 搬移函数(Move Method)
- “搬移函数”是重构理论的支柱
- 对于该函数如果引用了源类的函数,需要考虑将源类作为参数传递给方法
- 搬移字段(Move Field)
- 再类之间移动状态和行为,是重构之中不可缺少的部分
- 再使用Extract Class时,通常先搬移字段,后搬移函数
- 对于该类中的许多函数都用到了该字段,采用Self Encapsulate Field,这样只有修改访问函数即可
- 将双向关联改为单向关联(Change Bidirectional Association to Unidirectional)
- 容易导致循环引用
- 导致系统之间过于耦合,任何一个改动可能导致牵一发而动全身
- 提炼类(Extract Class)
- 一个类应该是明确的抽象,处理一些明确的责任。是改善并发的常用技术,因为可以为两个类分别加锁
3.18异曲同工的类(Alternative Classes with Different Interface)
解释:
- 两个函数做同一件事,却有着不同的函数签名,根据用途使用Rename Method
- 可以反复使用Move Method将某些行为移入类,直到两者协议一致
方法:
- 搬移函数(Move Method)P142
- 提炼超类(Extract Superclass)P336
含义:
- 你的程序中,有个函数与其所在的类之外的类进行更多的交流(往往是因为另一个类的数据):调用后者,或者被前者调用——在该函数最常引用的类中建立一个与该方法行为类似的新函数。将旧函数编程一个单纯的委托函数,或者完全移除旧函数
- 两个类有相似的特性——为这两个类建立一个超类,将相同的特性移至超类
阅读:
- 搬移函数(Move Method)
- “搬移函数”是重构理论的支柱
- 对于该函数如果引用了源类的函数,需要考虑将源类作为参数传递给方法
- 提炼超类(Extract Superclass)
- 减少重复代码,在发现两个类的通性就可以考虑提炼超类
3.19 不完美的库类(Incomplete Library Class)
解释:
- 复用常被视为对象的终极目的,不过我们认为,复用的意义经常被高估
- 所以对于想修改库类的一些方法,或者添加一些方法(ios中的类别)
方法:
- 引入外加函数(Introduce Foreign Method)P162
- 引入本地扩展(Introduce Local Extension)P164
含义:
- 你需要为你提供服务的类增加一个函数,但你无法修改这个类——在客户类中建立一个函数,并以第一参数形式传入一个服务类实例
- 你需要为服务类提供一些额外函数,但你无法修改这个类——建立一个新类,使它包含这些额外函数。让这个扩展品成为源类的子类或包装类
阅读:
- 引入外加函数(Introduce Foreign Method)
- 相当于扩展工具类的方法,相当于在进行了一次封装
- 引入本地扩展(Introduce Local Extension)
- 和Introdect Foreign Method类似,只是引入的方法数量大于等于两个或者需要大量引用该函数
- 本地扩展包是一个独立的类(类别)
- 可以选择子类hi或者包装类来完成工作,一般上推荐使用子类,因为可以向上兼容
3.20 幼稚的数据类(Data Class)
解释:
- 就像数据容器,只有数据和数据的访问方法
- 所以可能会看到该类的被其他类控制着
- 对于集合的数据,使用Encapsulate Collection
- 不想让其他类修改的字段,使用Remove Seting Method
- 对于使用了这些取值函数或者设值函数,可以使用Move Method,将方法移动到该类中
方法:
- 封装字段(Encapsulate Field)P206
- 封装集合(Encapsulate Collection)P208
- 移除设值函数(Remove Seting Method)P300
- 隐藏函数(Hide Method)P303
含义:
- 你的类中存在一个Public字段——将它声明为private,并提供相应的访问函数
- 有个函数返回一个集合——让这个函数返回该集合的一个只读副本,并在这个类中提供添加、移除集合元素的函数
- 类中的某个字段应该在对象创建的时候被赋值,然后就不再改变——去掉该字段的所有设值函数
- 有一个函数没有被其他类用到——将这个函数修改为privete
阅读:
- 封装字段(Encapsulate Field)
- 面向对象的首要的原则之一就是封装,或者成为“数据隐藏”
- 封装集合(Encapsulate Collection)
- 一个类提供了集合(array/list/set等),采用和不同数据类型的封装方法
- 比如:push, pop等,避免用户修改整个集合的值
- 移除设值函数(Remove Seting Method)
- 将不可变字段设置为final
- 隐藏函数(Hide Method)
- 重构往往促使你修改函数的可见度
3.21 被拒绝的遗赠(Refused Bequest)
解释:
- 对于继承来说,如果有不想要继承的数据或者方法,可以设计一个兄弟类,把用不到的方法和数据通过Push Down Method和Push Down Field移动到该类中
- 常常可以听到,所有的超类都应该是抽象的
- 如果子类复用超类的行为(实现),却不愿意支持超类的接口,就应该使用Replace Inheritance with Delegation
方法:
- 函数下移(Push Down Method)P328
- 字段下移(Push Down Field)P329
- 以委托取代继承(Replace Inheritance with Delegation)P352
含义:
- 超类中的某个函数只与部分(而非全部)子类有关——将这个函数移到相关的那些子类去
- 超类中的某个字段只被部分(而非全部)子类用到——将这个字段移到需要它的那些子类去
- 某个子类只使用超类接口中的一部分,或是根本不需要继承而来的数据——在子类中新建一个字段用以保存超类,调整子类函数,令它改而委托超类;然后渠道两者之间的继承关系
阅读:
- 函数下移(Push Down Method)
- 把某些行为从超类移至特定的子类
- 字段下移(Push Down Field)
- 把某些字段从超类移至特定的子类
- 以委托取代继承(Replace Inheritance with Delegation)
- 需要在该类宅保存委托类的对象(iOS, delegaet, weak)
3.22 过多的注释(Comments)
解释:
- 如果你感觉需要注释的时候,请先尝试重构,试着让代码来解释
方法:
含义:
阅读:
最后更新: 2018年07月11日 14:04
原始链接: https://ilifexiao.github.io/2018/05/02/重构改善既有代码的设计/代码的坏味道/