0%

代码的坏味道

3.1 重复代码(Duplicated Code)

需要采用 Extract Method 提炼出重复的代码。

如果同一个继承体系下的两个兄弟子类有相同代码,则使用 Extract Method(提炼函数) 提炼重复代码,然后使用 Pull Up Method(函数上移) 将它推入超类中。

如果代码类似,但并非完全相同,则可以御用 Extract Method(提炼函数) 将想死部分和差异部分分开,构成单独一个函数。然后运用Form Template Method(塑造模板函数) 获得一个 Template Method(模板方法) 设计模式。

如果有些函数以不同的算法做相同的事,可以选择其中一个较清晰的,使用 Substitute Algorithm(替换算法) 将其他函数的算法替换掉。

如果两个毫不相关的类出现 Duplicated Code ,应该考虑使用 Extract Class(提炼类) 将重复代码提炼到一个独立类中。但是,重复代码所在函数也可能是属于某个类,另一个类智能调用它,或者这个函数属于第三个类。需要根据具体情况决定把类放在哪里。

3.2 过长函数(Long Method)

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

如果函数内有大量的参数和临时变量,灰度你的函数提炼形成阻碍。这个时候可以通过Introduce Parameter Object(引入参数对象)Preserve Whole Object(保持对象完整)将过长参数列表变得简洁一些。如果这么做之后还是有很多临时变量和参数,可以使用Replace Method with Method Object(以函数对象取代函数)

条件表达式和循环也是提炼的信号,可以使用Decompose Conditional(分解条件表达式)处理条件表达式。可以将循环和其内部代码提炼到独立函数中。

3.3 过大的类(Large Class)

如果类有太多实例,可以使用Extract Class(提炼类)将类内彼此相关的变量提炼至新类内。

有时候并非所有时刻都使用类的所有实例变量,这个时候,可以多次使用Extract Class(提炼类)Extract Subclass(提炼子类)

和拥有太多实例类似,如果一个类有太多代码,也可以使用Extract Class(提炼类)Extract Subclass(提炼子类)。可以确定客户端如何使用这个类,然后使用Extract Interface(提炼接口)为每一种使用方式提炼一个接口,或许可以看清楚如何分解这个类。

对于大的GUI类,你可能需要把数据和行为一到一个独立的领域对象去,需要两边各保留一些重复数据,并且保持同步。这个时候Duplicate Observed Data(复制"被监视数据")会告诉你该怎么做。

3.4 过长参数列(Long Parameter List)

使用 Replace Parameter with Method(以函数取代参数) 来向已有的对象发出一条请求取代一个参数;

使用 Preserve Whole Object(保持对象完整) 将来自同一个对象的一对数据收集起来,并以该对象替换它们;

如果某些数据缺乏合理的对象归属,可以使用Introduce Parameter Object(引入参数对象)为它们制造出一个”参数对象“。

3.5 发散式变化(Divergent Change)

指一个类受多种变化的影响。

如果某个类经常因为不同原因在不同的方向上发生变化,Divergent Change(发散式变化)就出现了。

针对某一外界变化的所有相应修改,都只应该发生在单一类,而这个类内的所有内容都应该反应次变化。为此,你应该找出某特定原因而造成的所有变化,然后运营Extract Class(提炼类)将他们提炼到一个类中。

3.6 霰弹式修改(Shotgun Surgery)

指一种变化引发多个类相应修改。

如果每遇到一种变化,你都必须在许多不同的类内做出许多小修改,你所面临的坏味道就是Shotgun Surgery(霰弹式修改)

可以使用Move Method(搬移函数)Move Field(搬移字段)把所有需要修改的代码放进同一个类,如果没有合适的类,则创建一个。可以使用Inline Class(将类内联化)把一系列相关行为放进同一个类里面。这可能会造成少量的Divergent Change(发散式变化),但很容易处理掉。

3.7 依恋情节(Feature Envy)

如果某个函数为了计算某个值,从另一个对象调用比较多的取值函数,这个时候,应该使用Move Method(搬移函数)把它移到它该去的地方。如果函数中只有部分受到这种依恋之苦,则先使用Extract Method(提炼函数)把这一部分提炼到独立函数中,然后再进行Move Method(搬移函数)

特例:策略模式(Strategy),访问者模型(Visitor),这两者是为了对抗发散式变化(Divergent Change)坏味道。一般数据和对应的行为是一起变化的,如果不是,则搬移那些行为,保持在一地发生。这两种模式使得你可以轻松修改函数行为,因为它们将少量需要被覆写的行为隔离开来,代价是:多了一层间接性。

3.8 数据泥团(Data Clumps)

常常可以在很多地方看到相同的三四项数据:两个类中相同的字段、许多函数签名中相同的参数。可以运用Extract Class(提炼类)将它们提炼到一个独立对象中。针对函数参数,可以运用Introduce Parameter Object(引入参数对象)或者Preserve Whole Object(保持对象完整性)为它们减肥。

3.9 基本类型偏执(Primitive Obsession)

你可以运用Replace Data Value with Object(以对象取代数据值)将原本单独存在的数据值替换为对象,从而走出传统的洞窟,进入炙手可热的对象世界。如果是类型码,可以使用Replace Type Code with Class(以类取代类型码)将它替换掉。进一步,如果有类型码相关的条件表达式,可以使用Replace Type Code with Subclass(以子类取代类型码)Replace Type Code with State/Strategy(以State/Strategy取代类型码)加以处理。

如果你有一组总放一起的字段,可以使用Extract Class(提炼类)。如果在参数列表看到基本数据类型,可以试试Introduce Parameter Object(引入参数对象)。针对数据,也可以运用Replace Array with Object(以对象取代数组)

3.10 switch惊悚现身(Switch Statements)

一看到switch语句,就可以考虑用多态来替换它(switch常常根据类型码进行选择,你需要的是”与该类型码相关的函数或类“):

  • 使用Extract Method(提炼函数)将switch语句提炼到一个独立函数中;
  • Move Method(搬移函数)将它搬到需要多态性的那个类里;
  • 然后决定是否使用Replace Type Code with Subclasses(以子类取代类型码)Replace Type Code with State/Strategy(以State/Strategy取代类型码)
  • 接下来,就可以运用Replace Conditional with Polymorphism(以多态取代条件表达式)了。

如果只是在单一函数中有选择事例,并且不想改动他们,多态就有点大材小用了,针对这种情况,可以使用Replace Parameter with Explicit Methods(以明确函数取代参数)。如果选择条件是null,可以试试Introduce Null Object(引入Null对象)

3.11 平行继承体系(Parallel Inheritance Hierarchies)

Parallel Inheritance Hierarchies(平行继承体系)Shotgun Surgery(霰弹式修改)的特殊情况:每当你为某个类增加一个子类的时候,必须也为另一个类增加相应的子类,如果你发现某一个继承提醒的类前缀和另一个继承体系的类名称前缀完全相同,便是闻到了这种坏味道。

消除策略:让一个继承体系的实例引用另一个继承体系的实例。进一步的,运用Move Method(搬移函数)Move Field(搬移字段)将引用端继承体系消弥于无形。

3.12 冗赘类(Lazy Class)

如果某些子类没有做足够的工作,试试Collapse Hierarchy(折叠继承体系)。对于几乎没用的组件,应该以Inline Class(将类内联化)对付他们。

3.13 夸夸其谈未来性(Speculative Generality)

  • 如果你的某个抽象类其实没有太大作用,请运用Collapse Hierarchy(折叠继承体系)
  • 不必要的委托可以运用Inline Class(将类内联化)除掉;
  • 函数的某些参数未被用上,可以实施Remove Parameter(移除参数)
  • 如果函数名有多余的抽象意味,可以实施Rename Method(函数改名)

3.14 令人迷惑的暂时字段(Temporary Field)

坏味道描述:某个对象内部某个实例变量仅为某种特例情况而设。

可以使用Extract Class(提炼类)给这个实例变量创造一个家,然后把变量相关的代码都放到这个新家中。还可以使用Introduce Null Object(引入Null对象)在变量不合法的情况下创建Null对象,从而避免写出条件代码。

3.15 过渡耦合的消息链(Message Chains)

即过长的函数调用链。可以使用Hide Delegate(隐藏委托关系)做恰当的处理(理论上可以重构消息链上的任何一个对象,但是会把一系列对象都变为中间人)。

可以观察下消息链最终得到的对象是干嘛的,能否用Extract Method(提炼函数)把使用该对象的代码提炼到一个独立函数中,再用Move Method(搬移函数)把这个函数推入消息链(如果链上的某个对象有多位客户打算航线此航线的剩余部分,就加一个函数来做这件事)。

3.16 中间人(Middle Man)

为了隐藏对象内部层次关系细节,往往会使用委托,即中间人。

过渡使用委托:某个类接口有一半的函数都委托给其他类,这既是过度运用了。这个时候可以使用Remove Middle Man(移除中间人),直接和负责的对象打交道。

如果这样的委托函数只有几个,可以使用Inline Method(内联函数)把他们放入调用端。

如果中间人还有其他的行为,为何使用Replace Delegation with Inheritance(以继承取代委托)把它变成实责对象的子类呢。这样就既可以扩展元对象的行为,又不必负担那么多的委托动作。

3.17 狎昵关系(Inappropriate Intimacy)

若果两个类花太多时间去太久彼此的private成分,就说明有这种坏味道。

为了拆解这种关系,可以使用Move Method(搬移函数)Move Field(搬移字段)帮他们划清界限,减少这种关联关系。

也可以看看是否可以运用Change Bidirectional Association to Unidirectional(将双向关联改为单向关联)让其中一个类对另一个斩断情丝。

如果无法拆解,则可以吧两者共同点提炼到一个安全地点,让他们使用这个新类。也可以尝试使用Hide Delegate(隐藏委托关系)让另一个类来传递这种关联关系。

继承往往会造成过度亲密,子类对超类的了解总是超过超类的主观愿望,如果有必要,可以运用Replace Inheritance with Delegation(以委托取代继承)让子类离开继承体系。

3.18 异曲同工的类(Alternative Classes with Different Interfaces)

如果两个函数做同一件事情,却有不同的签名,请运用Rename Method(函数改名)根据用途重新命名。

也可以使用Extract Superclass(提炼超类)把子类相同的函数放入超类。

3.19 不完美的库类(InComplete Library Class)

假设类库中没有我们想要的方法或者功能,这也是很常见的现象,毕竟类库构筑者没有未卜先知的能力。

为此,你可以运营Introduce Foreign Method(引入外加函数)进行修改类库的一两个函数;如果需要添加一大堆额外行为,可以运用Introduce Local Extension(引入本地扩展)

3.20 纯粹的数据类(Data Class)

对于纯粹的数据类,属性不能为public字段,如果有,需要用Encapsulate Field(封装字段)封装起来。对于里面的容器类也类型,如果没有封装,尝试用Encapsulate Collection(封装集合)把它们封装起来。如果属性不允许被修改,请使用Remove Setting Method(移除设值函数)

找出取值/设值函数被调用的地方,尝试以Move Method(搬移函数),把那些调用行为搬移到Data Class来。如果无法搬移,则用Extract Method(提炼函数)产生一个可被搬移的函数。不久之后就可以用Hide Method(隐藏函数)把这些取值/设值函数隐藏起来了。

Data Class就像一个小孩,作为一个面向对象的起点很好,当腰让它们想成熟的对象那样参与整个系统,就必须承担一定责任。

3.21 被拒绝的遗赠(Refused Bequest)

正常来说,超类支持有所有子类共享的东西,所有超类都应该是抽象的。如果子类不想要继承超类的函数和方法,就会产生坏味道,但这种坏味道不是太强烈。

为了修复这种继承提醒错误,需要为子类新建一个兄弟类,再运用Push Down Method(函数下移)Push Down Field(字段下移)把所有用不到的函数下推给那个兄弟。

拒绝继承超类的实现,这一点我们不介意,但是如果拒绝继承超类的接口,我们不以为然。这个时候应该应用Replace Inheritance with Delegation(以委托取代继承)来达到目的。

3.22 过多的注释(Comments)

  • 如果需要注释来解释一段代码块做什么,试试Extract Method(提炼方法)
  • 如果函数已经提炼出来,但还需要注释,试试Rename Method(函数改名)
  • 如果需要注释说明某些系统的需求规格,试试Introduce Assertion(引入断言)

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

注释的用处:

  • 当你不知道该做什么时,可以写注释;
  • 注释还可以用来标记你并无十足把握的区域。

欢迎关注我的其它发布渠道