2022-05-27
第三章 代码的坏味道
本章主要讲何时应该进行代码重构,所谓代码的坏味道就是指代码中应该被重构的不好的代码
Duplicated Code 重复代码
重复代码常见情形
- 同一个类两个函数有相同的表达式
这种直接提炼出重复的代码,都去调用提炼出的函数即可 - 两个互为兄弟的子类内含有相同的表达式
提炼重复代码,将重复代码推到超类(父类)中,如果只是部分逻辑类似可以将相同部分提炼出来,有时可以使用Template Method设计模式 - 两个互不相关的类中有重复代码
将重复部分提炼出一个新的独立的类,然后在另一个类中使用这个类。
以上提炼都要考虑这个方法是否就应该在某个类中,不应该一味的将重复部分提炼出去,也要考虑提炼出的重复部分应该放在哪里更好。
Long Method过长的函数
程序越长越难理解,早起编程语言子程序的调用需要额外的开销,但,现代OO语言几乎已经完全免除了进程内的函数调用开销。不过太多的子程序调用可能降低可读性,读者要不断地转换上下文去看子程序做了什么。但是让小函数更容易理解的真正关键在于一个好名字,名字起得好,可以完全不去看子程序的逻辑,只看名字便知道它干了什么。
提炼小函数的信号
- 重复代码
- 当你想要写注释解释一段代码的时候
这是一个很明确的应该将代码提炼到一个独立函数的信号,你想要写的注释就是这个函数的函数名。只要函数名可以解释其用途我们应该毫不犹豫的去提炼出去,哪怕只有一行代码,只要这行代码需要注释其意图。哪怕最后函数调用比自身还要长也没关系。 - 条件表达式和循环
使用Decompose Conditional 的方法处理条件表达式,对于循环将其提炼到一个独立函数中
如果有大量的临时变量将阻碍提炼小函数,如果将临时变量作为参数传给小函数那么你会发现可读性没有任何提升此时可以应用Replace Temp with Query消除临时变量。
Introduce Parameter Object和Preserve Whole Object可以将过长的参数列变得简洁些,如果还是没有用,就用我们的杀手锏Replace Method with Method Object
Larg Class过大的类
如果一个类中做太多事情,就会有太多实例变量,和太多代码,这样就可能会出现重复代码。
可以使用Extract Class将几个变量提出去,将应该属于一个类的实例变量放在一起提炼成一个组件,如果适合作为子类可以使用Extract subClass。
Long Parameter List过长的参数列
太长的参数列难以理解,一旦需要更多数据,不得不修改参数列表。函数需要的东西多半可以在函数的宿主类中找到,如果向已有对象发起一个请求即可取代一个参数,应该使用Replace
Parameter with Method手法重构。还可以运用Preserve Whole Object将来自同一个对象的一堆数据收集起来,以该对象替换他们。如果某些数据缺乏合理对象归属,可以使用Introduce Parameter Object为他们制造出一个参数对象。
有些时候从对象中拆解出来单独作为参数是必要的,但是你的参数列表太长的话,就应该 考虑重新修改参数列表。
Divergent Change发散式变化
这个概念指“一个类受多种变化的影响”
每当需要修改时,我们希望能够调到系统的某一点,只在该处进行修改。针对某一个外界变化的所有修改都应该发生在单一类中。因此,你应该找出造成所有变化的原因,然后用Extract Class将它们提炼到一个类中。
Shotgun Surgery霰弹试修改
这个概念指“一个变化引发修改多个类”
如果每遇到一种变化,需要在许多不同类中进行修改,应该使用Move Method和Move Filed把需要修改的代码放到同一个类中,如果没有就创建一个
以上两种都是希望“外界变化”和“需要修改的类”趋于一一对应
Feature Envy依恋情节
“将数据和对数据的操作包装在一起。”
如果有函数对于某个类中的数据的依赖高于自己所处的类,就应该把他移到该类中去。当然,通常一个函数会用到几个类的功能,将函数与用到最多的那个类封装在一起。如果函数功能已经拆分的很细,将不同功能块放到不同类中显然会容易些。
Data Clumps数据泥团
在很多地方可以看到相同的几项数据项,这些总是绑在一起的数据项应该有属于他们自己的对象。可以使用Extract Class将他们提到一个独立对象中,然后运用Introduce Parameter Object和Preserve Whole Object为他减肥,这么做的好处是可以减少参数列个数。不必在意只用到这个独立函数的部分字段,只要新对象可以代替两个或以上的参数就折回票价了
Primitive Obsession基本类型偏执
将原本单独存在的基本类型替换为对象,例如:结合数值类型和币种的money类,有一个起始数值和结束数值组成的rang类
Switch Statement Switch惊悚现身
面向对象的最明显的特征是:少用Switch(或case)语句。本质上Switch就是重复,大多数情况,遇到Switch可以用多态替换他们。
先用Extract Method将Switch提出到独立函数中,然后Move Method将这个函数搬移到需要多态性的那个类中
用之前判断是否需要这么做,因为有时候这么做有点杀鸡用牛刀了
Parallel Inheritance Hierarchies平行继承体系
是Shotgun Surgery的特殊情况,每当为某个类增加一个子类时,也要为另一个类相应增加子类。消除这种重复性策略是:让一个继承体系实例引用另一个继承体系的实例
Lazy Class冗赘类
创建的每一个类都要费时费力去维护他。对于一些没有用的类,子类,果断的删除他
Speculative Generality夸夸其谈未来性
考虑将来可能会做的事,为此用一些特殊情况来处理一些非必要的事,这样往往造成系统更难理解和维护。如果将来确实会用到那是值得的,否则就不值。
Temporary Filed令人迷惑的临时字段
有些情况,某个实例变量只为某种特定情况而设。这种情况可以使用Extract Class将和这个变量有关的代码放到一起。如果类中有一个复杂算法需要用到很多变量,可以将这些变量和相关函数提炼到一个独立类中。
Message Chains过度耦合的消息链
看到用户向一个对象请求另一个对象,再向后者请求另一个对象,然后再请求。。。这就是消息链。一旦对象间关系发生变化,客户端就不得不修改。
这时可以用Hide Delegate,可以重构消息链任意一个对象,但这样可能把一系列对象变成middle man更好的选择是先观察消息链最终得到的对象是用来干什么的,看是否可以把用到该对象的代码提炼到一个独立函数中,然后再把这个函数推入消息链
Middle Man中间人
对象基本特征之一就是封装,向外部隐藏内部细节,这样少不了委托。例如:你询问主管是否有时间参加会议,主管将这个消息委托给自己的记事本,这样才能回答是否有时间。
但是可能会过度的使用委托,如果某个类接口有一半函数都委托给其他类这明显过度运用了。这是应该使用Remove Middle Man,直接和真正负责的对象打交道。如果这种不敢实事的函数只有少数几个,可以运用Inline Method把他们放进调用端。如果这些Middle Man还有其他行为可以运用Replace Delegation with Inheritance把他们变成实际负责对象的子类。
Inappropriate Intimacy狎昵关系(过度亲密关系)
有时两个类过度的亲密,必须花时间研究两个类的private成分,最好让他们划清界限。可以使用Move Method和Move Filed ,或者使用change Bidirectional Association to Unidirectional。如果两个类实在联系过于紧密,可以把两者的共同点提炼成一个独立的类,让他们使用这个类。
继承往往会导致这个问题,清运用Replace Inheritance with Delegation,让他离开继承体系
Alternative Classes with DIfferent Interface异曲同工的类
如果两个函数做同一件事,但是名称不一样,用Rename Method根据其功能重命名,这还不够还用不断使用MoveMethod将他们移到类中。如果这个过程需要重复而多余的移动代码,可以使用 Extract SuperClass减轻负担
Incomplete Library Class不完美的库类
库类往往不会让我们修改其中的类,使他完成我们希望的工作。有两个专门应付这种情况的工具。如果只想修改库类的一两个函数使用Introduce Foreign Method。如果想要添加一大堆额外行为运用Introduce Local Extension。
Data Class纯稚的数据类
Data Class指拥有一些字段,及用于访问(读写)这些字段的函数,除此之外没有别的功能。
他们可能被其他类过分琐碎的操控着。
对于这种类,应该找出这些取/设值函数被其他类运用的地点,尝试把这些调用移到Data Class中,如果无法移动整个函数,那就把可移动部分分离出来,放到Data Class中
Refused Bequest被拒绝的遗赠
子类继承超类的函数和数据,但有时子类只想使用一部分,一般来说这样是继承体系设计问题,可以通过增加一个兄弟类,将超类中,子类用不到的函数和数据放到这个兄弟类中。有这样一个忠告,所有超类都应该是抽象的。
不过并不是总是需要这样做,有时候,即便有这样的情况,也没有那么糟。不用大费周章做这些事情
Comments过多的注释
这并不以为着不要写注释,有时候你发现需要大量的注释去解释一段代码,前面说过,这是需要重构的信号,表示这段代码很糟糕。可以通过各种重构手法让代码变得优雅,之后会发现注释会少很多。