3 代码的坏味道
涵盖了大部分的bad smell以及解决之道。
3.1 重复代码
那么为什么重复的代码是一种坏味道?
最明显的就是重复的代码容易造成修改时的遗漏,修改遗漏导致一个问题需要修改多次才能才能确定最终修改完成。如果有一部分修改了,另外一部分没有修改且没有被发现,日后再遇到感觉类似,实则不同的代码会花费大量的时间确定业务上的需求,实现上应该如何处理。
重复代码这类坏味道产生的成本很低,但是带来的影响却是很大。
解决方法:
如果项目内多个地方看到了相同的代码, 那就想办法将其合为一
- 同一个类中两个函数含有相同的代码——>解决方法: extract method提炼重复代码
- 两个互为兄弟的子类内含相同表达式——>解决方法: extract method提炼重复代码。然后再对被提炼出来的代码使用Pull Up Method,将它推入超类内
- 两个子类中含有类似,并非完全相同的代码——>解决方法: extract method将相似部分和差异部分割开,构成单独一个函数。然后你可能发现可以运用Form Template Method 获得一个Template Method设计模式。
- 如果有些函数以不同的算法做相同的事,你可以选择其中较清晰的一个,并使用Substitute Algorithm(139)将其他函数的算法替换掉。
- 如果两个毫不相关的类出现Duplicated Code,你应该考虑对其中一个使用 Extract Class (149),将重复代码提炼到一个独立类中,然后在另一个类内使用这个新类。但是,重复代码所在的函数也可能的确只应该属于某个类,另一个类只能调用它,抑或这个函数可能属于第三个类,而另两个类应该引用这第三个类。你必须决定这个函数放在哪儿最合适,并确保它被安置后就不会再在其他任何地方出现。
Simple Design 为我们提供了参考参考原则:“通过测试,揭示意图,消除重复,最少元素”。
3.2 过长函数
过长的函数往往存在职责不够单一的情况,保持方法职责的单一有助于维护代码的可读性。
对大函数进行分解, 每当需要长注释的时候就应该将所需的分段进行包装了, 有时候替换后的函数只包含一行代码也没关系
同一个函数中的语句的表述应该在同一级别,比如创建订单 1. 检查用户是否登录,2. 检查库存 3. 下单
我们应该这么写
{
- 检查用户是否登录方法
- 检查库存方法
- 下单方法
}
不应该这么写
{
- 检查用户是否登录方法
- 检查库存逻辑代码
- 调用库存接口,查询库存数量
- 数量比对
- 下单方法
}
注释, 条件表达式和循环, 都是提炼代码的信号. 提炼的时候可能会产生过长的参数列表, 考虑如何将长参数包装为一个参数对象进行传递
为函数起一个好的名字,读者就可以通过名字了解函数的作用,根本不必去看其中写了些什么。
-
以其用途命名。
最终的效果是:你应该更积极地分解函数。
我们遵循这样一条原则:每当感觉需要以注释来说明点什么的时候,我们就把需要说明的东西写进一个独立函数中,并以其用途(而非实现手法)命名。我们可以对一组甚至短短一行代码做这件事。
哪怕替换后的函数调用动作比函数自身还长,只要函数名称能够解释其用途,我们也该毫不犹豫地那么做。关键不在于函数的长度,而在于函数“做什么”和“如何做”之间的语义距离。
以其用途命名。
百分之九十九的场合里,要把函数变小,只需使用Extract Method (110)。找到函数中适合集中在一起的部分,将它们提炼出来形成一个新函数。
过长函数通常的提炼方式
如果函数内有大量的参数和临时变量,它们会对你的函数提炼形成阻碍。如果你尝试运用Extract Method (110),最终就会把许多参数和临时变量当作参数,传递给被提炼出来的新函数,导致可读性几乎没有任何提升。此时,你可以经常运用 Replace Temp with Query(120)来消除这些临时元素。Introduce Parameter Object (295)和Preserve Whole Object (288)则可以将过长的参数列变得更简洁一些。
如果你已经这么做了,仍然有太多临时变量和参数,那就应该使出我们的杀手锏:Replace Method with Method Object (135)。
函数内有大量的参数和临时变量
条件表达式和循环常常也是提炼的信号。你可以使用Decompose Conditional (238)处理条件表达式。至于循环,你应该将循环和其内的代码提炼到一个独立函数中。
条件表达式和循环的提炼方法
如何确定该提炼哪一段代码呢?
一个很好的技巧是:寻找注释。它们通常能指出代码用途和实现手法之间的语义距离。如果代码前方有一行注释,就是在提醒你:可以将这段代码替换成一个函数,而且可以在注释的基础上给这个函数命名。就算只有一行代码,如果它需要以注释来说明,那也值得将它提炼到独立函数去。
那么一个函数应该多少行代码呢?
之前有个领导说(技术大牛):30行以内。在看文章的时候也有大牛说10行以内。其实不管有多少行,我感觉就是不要对人的心智有挑战就行。
3.3 过大的类
单个类做太多事情,其内往往就会出现太多实例变量。一旦如此,Duplicated Code也就接踵而至了。
如果对外提供服务发生了变动,过大的类不容易快速响应这样的变化。
如果大的类因为某个地方发生变化,很可能导致不相关的调用方的代码也会发生变化,这是一种耦合性的表现。
当过大的类被继承时很可能导致其他的坏味道,例如遗留的馈赠。
你可以运用Extract Class (149)将几个变量一起提炼至新类内。提炼时应该选择类内彼此相关的变量,将它们放在一起。例如depositAmount和depositCurrency可能应该隶属同一个类。通常如果类内的数个变量有着相同的前缀或字尾,这就意味有机会把它们提炼到某个组件内。如果这个组件适合作为一个子类,你会发现Extract Subclass (330)往往比较简单。
有时候类并非在所有时刻都使用所有实例变量。果真如此,你或许可以多次使用Extract Class (149)或Extract Subclass (330)。
和“太多实例变量”一样,类内如果有太多代码,也是代码重复、混乱并最终走向死亡的源头。最简单的解决方案(还记得吗,我们喜欢简单的解决方案)是把多余的东西消弭于类内部。如果有五个“百行函数”,它们之中很多代码都相同,那么或许你可以把它们变成五个“十行函数”和十个提炼出来的“双行函数”。
和“拥有太多实例变量”一样,一个类如果拥有太多代码,往往也适合使用Extract Class (149)和Extract Subclass (330)。这里有个技巧:先确定客户端如何使用它们,然后运用Extract Interface(341)为每一种使用方式提炼出一个接口。这或许可以帮助你看清楚如何分解这个类。
如果你的Large Class是个GUI 类,你可能需要把数据和行为移到一个独立的领域对象去。你可能需要两边各保留一些重复数据,并保持两边同步。Duplicate Observed Data (189)告诉你该怎么做。这种情况下,特别是如果你使用旧式的AWT组件,你可以采用这种方式去掉GUI 类并代以Swing组件。
和大函数一样, 当某个类负责了太多内容时就会产生冗余和混乱, 最好按照类所进行的工作为每个方法都提炼出接口, 然后慢慢分解
使用分治法。将过大的类,拆分成多个职责单一的小类
3.4 过长参数列
太长的参数列难以理解, 太多参数会造成前后不一致、不易使用, 且一旦需要更多数据就不得不修改它. 因此可以包装一个足够全面的参数类, 然后让目标函数自己从参数对象中获取自己需要的参数
-
长参数函数的可读性很差,不容易使用,且不容易维护。
-
当需要为长参数函数添加新的参数时,将会促使调用方发生变化,且新参数的位置也将让这个方法更加难以理解。
如何解决长参数的代码坏味道?
- 如果传递的几个参数都出自一个对象,那么可以选择使用 Preserve Whole Object(保持完整对象)直接传递该对象。
- 如果方法的参数来自不同的对象,可以选择使用 Introduce Parameter Object(引入参数对象)将多个参数放入一个新的类中,原来方法传递多个分开的参数,现在传递一个包含多个属性的一个对象。
- 如果调用者先计算调用 A 方法得到计算结果,然后将计算结果在传递给这个长参数函数,那么可以考虑去除这个参数,改为在长参数函数中直接调用 A 得到结果,从而消除传递的部分参数,这个重构过程可以参考 Replace Parameter With Method(使函数替换参数)。
需要的注意的是,有些情况下长参数的存在也是合理的,因为在一定程度上可以避免某些依赖关系的产生。可以通过观察长参数函数变化的频率,并采用“事不过三,三则重构“的原则,保持进行重构的准备。
尽量通过对象传递参数。
3.5 Divergent Change(发散式变化)
看到这个名字吓了一跳,表达的意思就是:一个类受多种变化的影响。
我们希望能够跳到系统的某一点,只在该处做修改。如果不能做到这点,你就嗅出两种紧密相关的刺鼻味道中的一种了。
如果某个类经常因为不同的原因在不同的方向上发生变化,Divergent Change就出现了。当你看着一个类说:“呃,如果新加入一个数据库,我必须修改这三个函数;如果新出现一种金融工具,我必须修改这四个函数。”那么此时也许将这个对象分成两个会更好,这么一来每个对象就可以只因一种变化而需要修改。当然,往往只有在加入新数据库或新金融工具后,你才能发现这一点。针对某一外界变化的所有相应修改,都只应该发生在单一类中,而这个新类内的所有内容都应该反应此变化。
为此,你应该找出某特定原因而造成的所有变化,然后运用Extract Class (149)将它们提炼到另一个类中。
软件一旦需要修改, 我们希望能够跳到系统的某一点, 只在该处做修改. 如果不能做到这点, 一个类由于外界发生对变化需要进行不同部分对修改时, 发散式变化发生了
这种时候我们应该将这个类进行拆分, 另外界某一功能的修改产生的变化只发生在一个类中
3.6 霰弹式修改(Shotgun Surgery)
一种变化引发多个类相应修改,需要将变化提炼出来。
和3.5类似, 但这是一个外界变化产生的修改发生在各处. 这种问题需要将所有需要修改的代码整合为一个类集中修改, 如果眼下没有合适的类那就创建一个
Shotgun Surgery类似Divergent Change,但恰恰相反。如果每遇到某种变化,你都必须在许多不同的类内做出许多小修改,你所面临的坏味道就是Shotgun Surgery。
如果需要修改的代码散布四处, 你不但很难找到它们, 也很容易忘记某个重要的修改。
这种情况下你应该使用Move Method (142)和Move Field (146)把所有需要修改的代码放进同一个类。
如果眼下没有合适的类可以安置这些代码,就创造一个。通常可以运用Inline Class (154)把一系列相关行为放进同一个类。这可能会造成少量Divergent Change,但你可以轻易处理它。
Divergent Change是指“一个类受多种变化的影响”,Shotgun Surgery则是指“一种变化引发多个类相应修改”。这两种情况下你都会希望整理代码,使“外界变化”与“需要修改的类”趋于一一对应。
3.7 依恋情结
充血模式函数对某个类的兴趣高过自己所处类的兴趣。
对象技术的全部要点在于:这是一种“将数据和对数据的操作行为包装在一起”的技术。
有一种经典气味是:函数对某个类的兴趣高过对自己所处类的兴趣。这种孺慕之情最通常的焦点便是数据。无数次经验里,我们看到某个函数为了计算某个值,从另一个对象那儿调用几乎半打的取值函数。疗法显而易见:把这个函数移至另一个地点。你应该使用Move Method (142)把它移到它该去的地方。有时候函数中只有一部分受这种依恋之苦,这时候你应该使用Extract Method (110)把这一部分提炼到独立函数中,再使用Move Method(142)带它去它的梦中家园。
当然,并非所有情况都这么简单。一个函数往往会用到几个类的功能,那么它究竟该被置于何处呢?我们的原则是:判断哪个类拥有最多被此函数使用的数据,然后就把这个函数和那些数据摆在一起。如果先以Extract Method (110)将这个函数分解为数个较小函数并分别置放于不同地点,上述步骤也就比较容易完成了。
有几个复杂精巧的模式破坏了这个规则。说起这个话题,GoF[Gangof Four]的Strategy和Visitor立刻跳入我的脑海,Kent Beck的Self Delegation[Beck]也在此列。使用这些模式是为了对抗坏味道Divergent Change。最根本的原则是:将总是一起变化的东西放在一块儿。数据和引用这些数据的行为总是一起变化的,但也有例外。如的东西放在一块儿。数据和引用这些数据的行为总是一起变化的,但也有例外。如的东西放在一块儿。数据和引用这些数据的行为总是一起变化的,但也有例外。如果例外出现,我们就搬移那些行为,保持变化只在一地发生。Strategy和Visitor使你得以轻松修改函数行为,因为它们将少量需被覆写的行为隔离开来——当然也付出了“多一层间接性”的代价。
如果一个函数高度依赖多个类的属性, 那么应该判断哪个类被这个函数使用得最多, 然后将函数放到这个类中
这也就是核心:总是将一起变化的东西放到一起, 保持变化只在一处发生
3.8 数据泥团(Data Clumps)
数据项就像小孩子,喜欢成群结队地待在一块儿。你常常可以在很多地方看到相同的三四项数据:两个类中相同的字段、许多函数签名中相同的参数。这些总是绑在一起出现的数据真应该拥有属于它们自己的对象。首先请找出这些数据以字段形式出现的地方,运用Extract Class (149)将它们提炼到一个独立对象中。然后将注意力转移到函数签名上,运用Introduce Parameter Object (295)或Preserve WholeObject (288)为它减肥。这么做的直接好处是可以将很多参数列缩短,简化函数调用。是的, 不必在意Data Clumps只用上新对象的一部分字段, 只要以新对象取代两个(或更多)字段,你就值回票价了。
一个好的评判办法是:删掉众多数据中的一项。这么做,其他数据有没有因而失去意义?如果它们不再有意义,这就是个明确信号:你应该为它们产生一个新对象。
减少字段和参数的个数,当然可以去除一些坏味道,但更重要的是:一旦拥有新对象,你就有机会让程序散发出一种芳香。得到新对象后,你就可以着手寻找Feature Envy,这可以帮你指出能够移至新类中的种种程序行为。不必太久,所有的类都将在它们的小小社会中充分发挥价值。
相同的若干项数据出现在不同地方,这些绑在一起出现的数据应提炼到独立的对象。
3.9 基本类型偏执( Primitive Obsession )
大多数编程环境都有两种数据:结构类型允许你将数据组织成有意义的形式;基本类型则是构成结构类型的积木块。结构总是会带来一定的额外开销。它们可能代表着数据库中的表,如果只为做一两件事而创建结构类型也可能显得太麻烦。
对象的一个极大的价值在于:它们模糊(甚至打破)了横亘于基本数据和体积较大的类之间的界限。你可以轻松编写出一些与语言内置(基本)类型无异的小型类。例如Java就以基本类型表示数值,而以类表示字符串和日期——这两个类型在其他许多编程环境中都以基本类型表现。
对象技术的新手通常不愿意在小任务上运用小对象——像是结合数值和币种的money类、由一个起始值和一个结束值组成的range类、电话号码或邮政编码(ZIP)等等的特殊字符串。你可以运用Replace Data Value with Object (175)将原本单独存在的数据值替换为对象,从而走出传统的洞窟,进入炙手可热的对象世界。如果想要替换的数据值是类型码,而它并不影响行为,则可以运用Replace Type Code with Class(218)将它换掉。如果你有与类型码相关的条件表达式, 可运用Replace Type Code with Subclass (213)或Replace Type Code with State/Strategy (227)加以处理。
如果你有一组应该总是被放在一起的字段,可运用Extract Class (149)。如果你在参数列中看到基本型数据,不妨试试Introduce Parameter Object (295)。如果你发现自己正从数组中挑选数据,可运用Replace Array with Object (186)。
将一些基本类型替换成包装类型
3.10 Switch惊悚现身
Switch 语句代表一类语句,比如 if...else, switch... case 语句都是 switch 语句。
并不是所有的 Switch 语句都是坏味道
带有坏味道的 Switch 语句指的是那些造成重复代码的 Switch语句。例如:根据某个状态来判断执行执行哪个动作。
public Order nextStep(...) {
if (state == 1) {
// do something
} else if (state == 2) {
// do something
} else if (state == 3) {
// do something
} else {
// do something
}
}
这种实现方法很多代码中都会出现,但是多数人使用这种方式添加代码,并不意味着这是一种好的代码。这样的实现方式很容易造成长函数,而且每次修改的位置要非常精准,需要在多个条件中逐个遍历找到最终需要的那个,再修改,可读性上无疑也是很差的。
面向对象中的多态概念可为此带来优雅的解决办法。大多数时候,一看到switch语句,你就应该考虑以多态来替换它。问题是多态该出现在哪儿?
switch语句常常根据类型码进行选择,你要的是“与该类型码相关的函数或类”,所以应该使用Extract Method (110)将switch语句提炼到一个独立函数中,再以Move Method (142)将它搬移到需要多态性的那个类里。此时你必须决定是否使用Replace Type Code with Subclasses (223)或Replace Type Code with State/Strategy (227)。一旦这样完成继承结构之后,你就可以运用Replace Conditional with Polymorphism (255)了。
如果你只是在单一函数中有些选择事例,且并不想改动它们,那么多态就有点杀鸡用牛刀了。这种情况下Replace Parameter with Explicit Methods (285)是个不错的选择。如果你的选择条件之一是null,可以试试Introduce Null Object。
如何处理 Switch 语句这种代码坏味道呢?
- 如果 swtich 语句是某个方法的一部分,那么不妨使用 Extract Method(提炼函数)将其先提炼出一个单独的方法,缩小上下文范围。
- 观察多个条件中的动作的关联关系,是否符合多态,如果是将符合多态的几个条件创建对应的类,并使用 Move Method (移动函数)移动到新创建的类中。
- 使用状态模式、枚举等多种实现手段消除其中的 swtich 语句。
3.11 平行继承体系
这是3.6的特殊情况。在这种情况下,每当你为某个类增加一个子类,必须也为另一个类相应增加一个子类。如果你发现某个继承体系的类名称前缀和另一个继承体系的类名称前缀完全相同,便是闻到了这种坏味道。
消除这种重复性的一般策略是:让一个继承体系的实例引用另一个继承体系的实例。如果再接再励运用Move Method (142)和Move Field (146),就可以将引用端的继承体系消弭于无形。
让一个继承体系的类的实例去引用另一个继承体系的实例,
3.12 冗赘类( Lazy Class )
项目中经常会出现这样的情况:某个类原本对得起自己的身价,但重构使它身形缩水,不再做那么多工作;或开发者事前规划了某些变化,并添加一个类来应付这些变化,但变化实际上没有发生。不论上述哪一种原因,请让这个类庄严赴义吧。如果某些子类没有做足够的工作,试试Collapse Hierarchy (344)。对于几乎没用的组件,你应该以Inline Class (154)对付它们。
尽管我们需要分解代码来保持逻辑的清晰, 但是一旦我们发现某些类的存在是不必要的, 徒增了理解的难度, 那就要及时将其删除
3.13 夸夸其谈未来性( Speculative Generality )
当有人说“噢,我想我们总有一天需要做这事”,并因而企图以各式各样的钩子和特殊情况来处理一些非必要的事情,这种坏味道就出现了。那么做的结果往往造成系统更难理解和维护。如果所有装置都会被用到,那就值得那么做;如果用不到,就不值得。用不上的装置只会挡你的路,所以,把它搬开吧。
带来的问题
未来意味着当下并不是必须的,过度的抽象和提升复杂性也会让系统难以理解和维护,同时也容易分散团队的注意力,如果用不到,那么就不值得做。
除非你在进行假设驱动开发,否则代码上总是谈未来容易绑架团队的思想,拿未来不确定的事情来解释事情的合理,会让那些务实者,关注投入产出比的抉择。并且容易让团队进入一个假象。
当业务上变动时,并不能及时的将代码进行变动,因为原来的代码中包含了一种对未来假设的实现,无形中增加了代码的复杂度,而且很容易增加团队沟通成本。
解决方法
如果你的某个抽象类其实没有太大作用,请运用Collapse Hierarchy (344)。不必要的委托可运用Inline Class (154)除掉。如果函数的某些参数未被用上,可对它实施Remove Parameter (277)。如果函数名称带有多余的抽象意味,应该对它实施Rename Method (273),让它现实一些。
如果函数或类的唯一用户是测试用例,这就飘出了坏味道Speculative Generality。
如果你发现这样的函数或类,请把它们连同其测试用例一并删掉。但如果它们的用途是帮助测试用例检测正当功能,当然必须刀下留人。
当发现为未来而写的代码时,可以:
- 删除那些觉的未来有用的参数、代码、方法调用。
- 修正方法名,使方法名揭示当下业务场景的意图,避免抽象的技术描述词。
通过上面两个过程将代码原本的要表达的意思还原回来。
不要过度设计, 绝大多数预先设计都是无用的. 如果我们发现某个设计除了在测试样例中外毫无作用, 那么我们应该讲这个设计连同这个测试样例一起删掉
工作中有两类未来性。一类是假设调用方可以怎么使用;一类是未来必然发生的业务功能。代码的坏味道更多的指的是第一种情况,第二种情况可以开发之前体现进行简单设计和拆分,从而避免过度设计,同时可以避免谈未来性,来让代码随着功能一起小步重构并演进。
3.14 令人迷惑的暂时字段( Temporary Field )
在一些场景下为了在实现上的临时方便性,有的开发者会直接在某个对象上添加一个属性,后续使用在需要的时候使用该属性。
一个类包含属性和方法,属性都是该类相关的。而临时向类中添加的字段,虽然临时有关联性,但是单独来看这个类中的属性时,却会让人觉得非常费解。有些接口的返回值就是也是类似原因导致的结果,每次为了方便像类中直接添加一些临时属性,满足了当时的需要,但是后续再使用的时候却并不能区分哪些属性时必须的,哪些是不必须的,以及哪些被添加的字段的上下文分别是什么。
请使用Extract Class (149)给这个可怜的孤儿创造一个家,然后把所有和这个变量相关的代码都放进这个新家。也许你还可以使用Introduce Null Object (260)在“变量不合法”的情况下创建一个Null对象,从而避免写出条件式代码。
如果类中有一个复杂算法,需要好几个变量, 往往就可能导致坏味道TemporaryField的出现。由于实现者不希望传递一长串参数(想想为什么),所以他把这些参数都放进字段中。但是这些字段只在使用该算法时才有效,其他情况下只会让入迷惑。这时候你可以利用Extract Class (149)把这些变量和其相关函数提炼到一个独立类中。提炼后的新对象将是一个函数对象[Beck]。
如果一个类中有一个复杂算法, 需要好几个临时变量的协作, 那么我们应该将这些操作和变量拆分提取到一个独立类中, 提炼后称为函数对象. 由这个类去维护那些临时变量, 也方便我们传递这些变量
3.15 过度耦合的消息链
如果你看到用户向一个对象请求另一个对象,然后再向后者请求另一个对象,然后再请求另一个对象……这就是消息链。
实际代码中你看到的可能是一长串getThis()或一长串临时变量。采取这种方式,意味客户代码将与查找过程中的导航结构紧密耦合。一旦对象间的关系发生任何变化,客户端就不得不做出相应修改。
这时候你应该使用Hide Delegate(157)。你可以在消息链的不同位置进行这种重构手法。理论上可以重构消息链上的任何一个对象,但这么做往往会把一系列对象(intermediate object)都变成Middle Man。通常更好的选择是:先观察消息链最终得到的对象是用来干什么的,看看能否以Extract Method (110)把使用该对象的代码提炼到一个独立函数中,再运用Move Method (142)把这个函数推入消息链。如果这条链上的某个对象有多位客户打算航行此航线的剩余部分,就加一个函数来做这件事。
有些人把任何函数链都视为坏东西,我们不这样想。呵呵,我们的冷静镇定是出了名的,起码在这件事上是这样。
长长的消息链并不一定是坏事, 有时候是被逼无奈的. 重要的是防止消息链过度耦合, 使得一个小小的修改影响了整个链的运作. 我们应该将消息链尽量提取和拆分, 提炼一些小函数作为链条中间的接口, 当用户可以从链的任何节点开始运行时, 解耦就做得差不多了
3.16 中间人( Middle Man )
对象的基本特征之一就是封装——对外部世界隐藏其内部细节。封装往往伴随委托。
比如说你问主管是否有时间参加一个会议,他就把这个消息“委托”给他的记事簿,然后才能回答你。很好,你没必要知道这位主管到底使用传统记事簿或电子记事簿亦或秘书来记录自己的会议。
但是人们可能过度运用委托。你也许会看到某个类接口有一半的函数都委托给其他类,这样就是过度运用。这时应该使用Remove Middle Man (160),直接和真正负责的对象打交道。如果这样“不干实事”的函数只有少数几个,可以运用InlineMethod (117)把它们放进调用端。如果这些Middle Man还有其他行为,可以运用Replace Delegation with Inheritance(355)把它变成实责对象的子类,这样你既可以扩展原对象的行为,又不必负担那么多的委托动作。
避免太多的委托和中间人的设计, 如果发现某个类和另一个类的交流中有一半以上的接口都由中间人来负责的话, 不如将中间人的相应实现提回对话的两边, 然后消除这个无用的中间人. 中间人应该只负责一点点粘合工作, 两个类间如果可以的话尽量不要中间人
3.17 狎昵关系( Inappropriate Intimacy )
如果两个类过于亲密,花费太多时间去探究彼此的private成分。你可以采用Move Method (142)和Move Field (146)帮它们划清界线,从而减少狎昵行径。你也可以看看是否可以运用Change Bidirectional Association to Unidirectional (200)让其中一个类对另一个斩断情丝。如果两个类实在是情投意合,可以运用Extract Class (149)把两者共同点提炼到一个安全地点,让它们坦荡地使用这个新类。或者也可以尝试运用Hide Delegate (157)让另一个类来为它们传递相思情。
继承往往造成过度亲密,因为子类对超类的了解总是超过后者的主观愿望。如果你觉得该让这个孩子独自生活了,请运用Replace Inheritance with Delegation (352)让它离开继承体系。
不要让两个类过于亲密, 大量的private都能互相访问, 我们应该将这种耦合的类的相关方法尽量提取到新的类中, 然后让这两个类一起使用中间类来交互.
相似的, 如果子类对父类在继承中有了过多的了解, 也应该用委托来减少过多的试探, 和3.16似乎有冲突/取舍, 3.17这里主要是针对访问private的问题
3.18 异曲同工的类
如果两个函数做同一件事,却有着不同的签名,请运用Rename Method (273)根据它们的用途重新命名。但这往往不够,请反复运用Move Method (142)将某些行为移入类,直到两者的协议一致为止。如果你必须重复而赘余地移入代码才能完成这些,或许可运用Extract Superclass (336)为自己赎点罪。
如果出现两个函数做着一样的事情但是名称和接口不太一样, 应该尽量将其整合为一个函数, 或者用一个类来包装
3.19 不完美的库类( Incomplete Library Class )
复用常被视为对象的终极目的。不过我们认为,复用的意义经常被高估——大多数对象只要够用就好。但是无可否认,许多编程技术都建立在程序库的基础上,没人敢说是不是我们都把排序算法忘得一干二净了。
库类构筑者没有未卜先知的能力,我们不能因此责怪他们。毕竟我们自己也几乎总是在系统快要构筑完成的时候才能弄清楚它的设计,所以库作者的任务真地很艰巨。麻烦的是库往往构造得不够好,而且往往不可能让我们修改其中的类使它完成我们希望完成的工作。这是否意味那些经过实践检验的战术, 如Move Method (142)等,如今都派不上用场了?
幸好我们有两个专门应付这种情况的工具。如果你只想修改库类的一两个函数,可以运用Introduce Foreign Method (162);如果想要添加一大堆额外行为,就得运用Introduce Local Extension (164)。
库类的设计有时候也有一些不好的问题, 如果只想修改库类的一两个函数, 可以运用后面的Introduce Foreign Method方法;如果想要添加一大堆额外行为, 就得运用Introduce Local Extension.
3.20 纯稚的数据类(Data Class)
所谓Data Class是指:它们拥有一些字段,以及用于访问(读写)这些字段的函数,除此之外一无长物。
这样的类只是一种不会说话的数据容器,它们几乎一定被其他类过份细琐地操控着。这些类早期可能拥有public字段, 果真如此你应该在别人注意到它们之前,立刻运用Encapsulate Field (206)将它们封装起来。如果这些类内含容器类的字段,你应该检查它们是不是得到了恰当的封装;如果没有,就运用Encapsulate Collection (208)把它们封装起来。对于那些不该被其他类修改的字段,请运用Remove Setting Method (300)。
然后,找出这些取值/设值函数被其他类运用的地点。尝试以Move Method (142)把那些调用行为搬移到Data Class来。如果无法搬移整个函数,就运用Extract Method(110)产生一个可被搬移的函数。不久之后你就可以运用Hide Method (303)把这些取值/设值函数隐藏起来了。
Data Class就像小孩子。作为一个起点很好,但若要让它们像成熟的对象那样参与整个系统的工作,它们就必须承担一定责任。
用来管理字段的数据类很好, 但是我们要注意对其进行良好的封装, 尽量少暴露其内部的接口, 并为其设计一些非常常用的功能以物尽其用
3.21 被拒绝的馈赠( Refused Bequest )
子类应该继承超类的函数和数据。但如果它们不想或不需要继承,又该怎么办呢?它们得到所有礼物,却只从中挑选几样来玩!
按传统说法,这就意味着继承体系设计错误。你需要为这个子类新建一个兄弟类,再运用Push Down Method (328)和Push Down Field (329)把所有用不到的函数下推给那个兄弟。这样一来,超类就只持有所有子类共享的东西。你常常会听到这样的建议:所有超类都应该是抽象(abstract)的。
既然使用“传统说法”这个略带贬义的词,你就可以猜到,我们不建议你这么做,起码不建议你每次都这么做。我们经常利用继承来复用一些行为,并发现这可以很好地应用于日常工作。这也是一种坏味道,我们不否认,但气味通常并不强烈。所以我们说:如果Refused Bequest引起困惑和问题,请遵循传统忠告。但不必认为你每次都得那么做。十有八九这种坏味道很淡,不值得理睬。如果子类复用了超类的行为(实现),却又不愿意支持超类的接口,Refused Bequest的坏味道就会变得浓烈。拒绝继承超类的实现,这一点我们不介意;但如果拒绝继承超类的接口,我们不以为然。
不过即使你不愿意继承接口,也不要胡乱修改继承体系,应该运用Replace Inheritance with Delegation (352)来达到目的。
传统思想中常常告诉我们超类应该是抽象的, 但是这并不是很大的问题, 我们常常需要继承来服用父类的方法, 所以没必要保持抽象. 而且如果遇到了我们需要继承一个类, 可是这个类中有些接口我们不想继承, 不要立刻为父类创建兄弟类, 我们只需拒绝继承超类的实现即可, 其实接口的多余继承并不重要
3.22 过多的注释( Comments )
别担心,我们并不是说你不该写注释。从嗅觉上说, Comments不是一种坏味道,事实上它们还是一种香味呢。我们之所以要在这里提到Comments,是因为人们常把它当作除臭剂来使用。
常常会有这样的情况:你看到一段代码有着长长的注释,然后发现,这些注释之所以存在乃是因为代码很糟糕。这种情况的发生次数之多,实在令人吃惊。
Comments可以带我们找到本章先前提到的各种坏味道。找到坏味道后,我们首先应该以各种重构手法把坏味道去除。完成之后我们常常会发现:注释已经变得多余了,因为代码已经清楚说明了一切。
如果你需要注释来解释一块代码做了什么,试试Extract Method (110);如果函数已经提炼出来,但还是需要注释来解释其行为,试试Rename Method (273);如果你需要注释说明某些系统的需求规格,试试Introduce Assertion (267)。
当你感觉需要撰写注释时,请先尝试重构,试着让所有注释都变得多余。
如果你不知道该做什么,这才是注释的良好运用时机。除了用来记述将来的打算之外, 注释还可以用来标记你并无十足把握的区域。你可以在注释里写下自己“为什么做某某事”。这类信息可以帮助将来的修改者,尤其是那些健忘 的家伙。
在 《Clean Code》 中罗列了一些注释的坏味道:
-
喃喃自语
-
多余的注释
-
误导性注释
-
循规方注释
-
日志式注释
-
废话注释
-
用注释来解释变量意思
-
用来标记位置的注释
-
类的归属的注释
-
注释掉的代码
注释是一种好习惯, 但是当感觉需要撰写注释时, 先尝试重构试着让所有注释都变得多余, 通过提炼函数, 重命名各种函数和变量, 用断言替代注释中的规格声明. 注释的良好运用时机是记述将来的打算和自己都没有十足把握的区域. 这类信息可以帮助将来的修改者, 尤其是将来的自己