# 第3章:代码的坏味道

loading

# 一、Duplicated Code(重复代码)

如果在一个以上的地点看到相同的程序结构,那么可以肯定:设法将它们合二为一,程序会变的更好。

  • 同一个类的两个函数含有相同的表达式

这时候采用 Extract Method 需提炼出重复的代码,让这两个地点都调用被提炼出来的那一段代码。

  • 两个互为兄弟的子类内含有相同表达式

需对两个类都使用 Extract Method,再对被提炼出来的代码使用 Pull Up Method 将它推入超类内。如果代码之间只是类似,并非完全相同,那么就得运用 Extract Method 将相似部分和差一部分割开,够成单独一个函数,然后可以运用 Form Template Method 获得一个模板方法设计模式。如果有些函数以不同的算法做相同的事,可以选择其中一个较清晰的,使用 Substitute Algorithm 将其他函数的算法替换掉。

  • 两个毫不相关的类出现重复代码

考虑对其中一个使用 Extract Class,将重复代码提炼到一个独立类中,然后在另一个类内使用这个新类。但是重复代码所在的函数也可能的确只应该属于某个类,另一个类只能调用它,或者这个函数可能属于第三个类,而另外两个类应引用这第三个类。你必须决定这个函数放在哪最合适,并确保被安置后就不会再在其它地方出现。

# 二、Long Method(过长函数)

每当感觉医嘱是来说明点什么的时候,我们就把需要说明的东西写进一个独立函数中,并以其用途(而非实现手法)命名。

如果函数内有大量的参数和临时变量,这会对提炼函数形成阻碍,如果使用 Extract Method,最终会产生很多参数和临时变量,导致程序没有可读性。此时可以运用 Replace Temp with Query 来消除这些临时元素。Introduce Parameter ObjectPreserve Whole Object 则可以将过长的参数列变的更简洁一些。如果这么做还有太多临时变量和参数,那么就使用杀手锏 Replace Method with Method Object

如何确定该提炼哪一行代码呢?一个很好的技巧是:寻找注释。它们通常能指出代码用途和实现手法之间的语意距离。如果代码前方有一行注释,它就是在提醒你:可以将这段代码替换成一个函数,而且可以在注释的基础上给这个函数命名。就算只有一行代码,如果他需要以注释来说明,那也值得将它提炼到独立函数中去。

条件表达式和循环常常也是提炼的信号。可以使用 Decompose Conditional 处理条件表达式。至于循环,应该将循环和其内的代码提炼到一个独立函数中。

# 三、Large Class(过大的类)

可以运用 Extract Class 将几个变量一起提炼至新类内。提炼时应该选择类内彼此相关的变量,将它们放在一起。例如 depositAmountdepositCurrency 可能应该隶属于同一个类。通常如果类内的数个变量有着相同的前缀或字尾,这就意味着机会把它们提炼到某个组件内。如果这个组件适合作为一个子类,你会发现 Extract Subclass 往往比较简单。

这里有个技巧:先确定客户端如何使用它们,然后运用 Extract Interface 为每一种使用方式提炼出一个接口。者获取可以帮助你看清楚如何分解这个类。

# 四、Long Parameter List(过场参数列)

太长的参数列难以理解,太多参数会造成前后不一致、不易使用。而且一旦你需要更多数据,就不得不修改它。如果将对象传递给函数,大多数修改都讲没有必要,因为你很可鞥呢只需(在函数内)增加一两条请求,就能得到更多数据。

你可以运用 Preserve Whole Object 将来自同一对象的一堆数据收集起来,并以该对象替换它们。如果某些数据缺乏合理的对象归属,可以使用 Introduct Parameter Object 将它们制造出一个“参数对象”。

# 五、Divergent Change(发散式变化)

如果某个类经常因为不同的原因在不同的方向上变化,那么此时也许将这个对象分成两个会更好,这么一来每个对象就可以只因为一种变化而需要修改。针对某一外界变化所有的修改,都应该只发生在单一类中,而这个新提炼出来的类内的所有内容都应该反映此变化。你应该找出某些特定原因造成的所有变化,然后运用 Extract Class 将它们提炼到另一个类中。

# 六、Shotgun Surgery(霞弹式修改)

类似 Divergent Change,如果每遇到某种变化,都需要在不同的类内做出修改,此时面临的就是 Shotgun Surgery。因为需要修改的代码散布四处,很难找到它们,也很容易忘记某个重要的修改。

此时应该使用 Move MethodMove Field 把所有需要修改的代码放进同一个类。如果没有合适的类就创建一个。通常可以使用 Inline Class 把一些列相关行为放进同一个类。

# 七、Feature Envy(依恋情节)

经常某个函数为了计算某个值,从另一个对象那调用了几乎半打的取值函数,这时就应该使用 Move Method 把这个函数移至另一个地点。

一个函数往往会用到几个类的功能,那么原则是:判断哪个类拥有最多被此函数使用的数据,然后就把这个函数和那些数据摆在一起。如果先以 Extract Method 将这个函数分解为数个较小的函数并分别放置于不同地点,上述步骤也就容易完成了。

# 八、Data Clumps(数据泥团)

找出类中或者函数中相同参数的数据字段,运用 Extract Class 将它们提炼到一个独立对象中。

一个好的评判办法是:删掉众多数据中的一项。这么做,其他数据有没有因而失去意义?如果它们不再有意义,这就是个明确信号:你应该为它们产生一个新对象。

# 九、Primitive Obsession(基本类型偏执)

你可以运用 Replace Data Value with Object 将原本单独存在的数据值替换为对象,从而走出传统的洞窟,进入炙手可热的对象世界。如果想要替换的数据值是类型码,而它并不影响行为,则可以运用 Replace Type Code with Class 将它替换掉。如果你又与类型码相关的条件表达式,可运用 Replace Type Code with SubclassReplace Type Code with State/Strategy 加以处理。

如果你有一组应该总是被放在一起的字段,可运用 Extract Class。如果在参数列中看到基本类型,不妨试试 Introduce Parameter Object。如果发现自己正从数组中挑选数据,可运用 Replace Array with Object

# 十、Switch Statements(Switch惊悚现身)

应该使用 Extract MethodSwitch 语句提炼到一个独立函数中,再以 Move Method 将它搬移到需要多态性的那个类里。此时你必须决定是否使用 Replace Type Code with SubclassesReplace Type Code with State/Stratery。一旦这样完成继承结构之后,就可以运用 Replace Conditional with Polymorphism 了。

如果你只是在单一函数中有些选择事例,且并不想改动它们,那么多态就有点杀鸡用牛刀了。这种情况下 Replace Parameter with Explicit Methods 是个不错的选择。如果你的选择条件之一是 null,可以试试 Introduce Null Object

# 十一、Parallel Inheritance Hierarchies(平行继承体系)

Parallel Inheritance Hierarchies 其实是 Shotgun Surgery 的特殊情况。这种情况下,每当你为某个类增加一个子类,必须也为另一个类相应增加一个子类。

消除这种重复性的一般策略是:让一个继承体系的实例引用另一个继承体系的实例。如果再接再厉运用 Move MethodMove Field 就可以将引用端的继承体消弭儿于无形。

# 十二、Lazy Class(冗赘类)

如果一个类的所得不值其身价,它就应该消失。

# 十四、Temporary Field(令人迷惑的暂时字段)

某个对象内的某个实例变量近卫某种特定情况而设,请使用 Extract Class 给这些孤儿创造一个家。

如果类中有一个复杂算法,需要好几个参数,但是里面部分参数只在使用该算法时才有效,其他情况下这会让人迷惑。这时候你可以使用 Extract Class 把这些变量和其相关函数提炼到一个独立类中。提炼后的新对象僵尸一个函数对象。

# 十五、Message Chains(过度耦合的消息链)

向一个对象A请求对象B,向对象B请求对象C,以此类推,这就是消息链。先观察消息链最终得到的对象是用来干什么的,看看能否以 Extract Method 把使用该对象的嗲吗提炼到一个独立函数中,再运用 Move Method 把这个函数兑入消息链。

# 十六、Middle Man(中间人)

你也许会看到某个类接口有一半的函数都委托给其他类,这样就是过度运用。应该使用 Remove Middle Man,直接和真正负责的对象打交道。如果这样的函数只有少数几个,可以运用 InlineMethod 把它们放进调用端。如果这些 Middle Man 还有其他行为,可以运用 Replace Delegation with Inheritance 把它变成实责对象的子类,这样你既可以扩展原对象的行为,又不必负担那么多的委托动作。

# 十七、Inappropriate Intimacy(狎昵关系)

可以看看是否可以运用 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 是指拥有一些字段,以及用于访问(读写)这些字段的函数,除此之外一无长物的类。这些类早起可能拥有 public 字段,果真如此你应该在别人注意到它们之前,立刻运用 Encapsulate Field 将它们封装起来。如果这些类内含容器类的字段,你应该检查它们是不是得到了恰当的封装:如果没有,就运用 Encapsulate Collection 把它们封装起来,对于那些不该被其他类修改的字段,请运用 Remove Setting Method

然后找出这些取值/设值函数被其他类运用的地点。尝试以 Move Method 把那些调用行为搬移到 Data Class 来。如果无法搬移整个函数,就运用 Extract Method 产生一个可以被搬移的函数。不久之后你就可以运用 Hide Method 把这些取值/设值函数隐藏起来了。

# 二十一、Refused Bequest(被拒绝的遗赠)

你常常会听到这样的建议:所有超类都应该是抽象的。

如果子类复用了超类的行为(实现)缺又不愿意支持超类的接口,如果不愿意继承接口的话,应该运用 Replace Inheritance with Delegation 来达到目的。

# 二十二、Conmments(过多的注释)

如果你需要注释来解释一块代码做了什么,试试 Extrace Method,如果函数已经提炼出来,但还是需要注释来解释其行为,试试 Rename Method;如果你需要注释说明某些系统的需求规格,试试 Introduce Assertion

当你感觉需要撰写注释时,请先尝试重构,试着让所有注释都变得多余

如果你不知道该做什么,这才是注释的良好运用时机。除了 用来记述将来的打算之外,注释还可以用来标记你并无十足把握的区域,你可以在注释里写下自己“为什么做某某事”。这类信息可以帮助将来的修改者,尤其是那些健忘的家伙。


上次更新: 2020-08-21 09:02:51(10 小时前)