Bad smells in code代码异味

1.Duplicated Code(重复的代码)

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

最单纯的 Duplicated Code 就是「同一个 class 内的两个函数含有相同表达式(expression)」。这时候你需要做的就是采用Extract Method(110)提炼出重复的代码,然后让这两个地点都调用被提炼出来的那段代码。

另一种常见情况就是「两个互为兄弟(sibling)的 subclasses 内含相同表达式」。
要避免这种情况,只需对两个 classes 都使用 Extract Method(110),然后再对被提炼出来的代码使用 Pull Up Method(332),将它推入 superclass 内。如果代码之间只是类似,并非完全相同,那么就得运用 Extract Method(110)将相似部分和差异部分割开,构成单独㆒个函数。然后你可能发现或许可以运用 Form Template Method(345)获得一个 Template Method 设计模式。如果有些函数以不同的算法做相同的事,你可以择定其中较清晰的那个,并使用 Substitute Algorithm(139)将其它函数的算法替换掉。
如果两个毫不相关的 classes 内出现 Duplicated Code,你应该考虑对其中一个使用 Extract Class(149),将重复代码提炼到一个独立 class 中,然后在另一个 class 内使用这个新 class。但是,重复代码所在的函数也可能的确只应该属于某个 class,另一个 class 只能调用它,或者这个函数可能属于第三个 class,而另两个 classes 应该引用这第三个 class。你必须决定这个函数放在哪儿最合适,并确保它被安置后就不会再在其它任何地方出现。

2.Long Method(过长函数)

拥有「短函数」(short methods)的对象会活得比较好、比较长。不熟悉面向对象技术的人,常常觉得对象程序中只有无穷无尽的 delegation(委托),根本没有进行任何计算。和此类程序共同生活数年之后,你才会知道,这些小小函数有多大价值。「间接层」所能带来的全部利益 — 解释能力、共享能力、选择能力 — 都是由小型函数支持的。

很久以前程序员就已认识到:程序愈长愈难理解。早期的编程语言中,「子程序调用动作」需要额外开销,这使得人们不太乐意使用 small method。现代 OO 语言几乎已经完全免除了进程(process)内的「函数调用动作额外开销」。不过代码阅读者还是得多费力气,因为他必须经常转换上下文去看看子程序做了什么。某些开发环境允许用户同时看到两个函数,这可以帮助你省去部分麻烦,但是让 small method 容易理解的真正关键在于起个好名字。如果你能给函数起个好名字,读者就可以通过名字了解函数的作用,根本不必去看其㆗写了些什么。

最终的效果是:你应该更积极进取地分解函数。我们遵循这样一条原则:每当感觉需要以注释来说明点什么的时候,我们就把需要说明的东西写进一个独立函数中,并以其用途(而非实现手法)命名。我们可以对一组或甚至短短一行代码做这件事。哪怕替换后的函数调用动作比函数自身还长,只要函数名称能够解释其用途,我们也该毫不犹豫地那么做。关键不在于函数的长度,而在于函数「做什么」和「如何做」之间的语义距离。

百分之九十九的场合里,要把函数变小,只需使用 Extract Method(110)。找到函数中适合集在一起的部分,将它们提炼出来形成一个新函数。

如果函数内有大量的参数和临时变量,它们会对你的函数提炼形成阻碍。如果你尝试运用 Extract Method(110),最终就会把许多这些参数和临时变量当做参数,传递给被提炼出来的新函数,导致可读性几乎没有任何提升。啊是的,你可以经常运用 Replace Temp with Query(120)来消除这些暂时元素。Introduce Parameter Object(295)和 Preserve Whole Object(288)则可以将过长的参数列变得更简洁一些。

如果你已经这么做了,仍然有太多临时变量和参数,那就应该使出我们的杀手翦:Replace Method with Method Object

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

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

3.Large Class(过大类)

如果想利用单一 class 做太多事情,其内往往就会出现太多 instance 变量。一旦如此,Duplicated Code 也就接踵而至了。

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

有时候 class 并非在所有时刻都使用所有 instance 变量。果真如此,你或许可以多次使用 Extract Class(149)或 Extract Subclass(330)。

和「太多 instance 变量」一样,class 内如果有太多代码,也是「代码重复、混乱、死亡」的绝佳滋生地点。最简单的解决方案(还记得吗,我们喜欢简单的解决方案)是把赘余的东西消弭于 class 内部。如果有五个「百行函数」,它们之间很多代码都相同,那么或许你可以把它们变成五个「十行函数」和十个提炼出来的「双行函数」。

和「拥有太多 instance 变量」一样,一个 class 如果拥有太多代码,往往也适合使用 Extract Class(149)和 Extract Subclass(330)。这里有个有用技巧:先确定客户端如何使用它们,然后运用 Extract Interface(341)为每㆒种使用方式提炼出一个接口。这或许可以帮助你看清楚如何分解这个 class。

如果你的 Large Class 是个 GUI class,你可能需要把数据和行为移到一个独立的领域对象(domain object)去。你可能需要两边各保留这些重复数据,并令这些数据同步(sync.)。Duplicate Observed Data(189)告诉你该怎么做。这种情况下,特别是如果你使用旧式 Abstract Windows Toolkit(AWT)组件,你可以采用这种方式去掉 GUI class 并代以 Swing 组件。

4.Long Parameter List(过长参数列)

刚开始学习编程的时候,老师教我们:把函数所需的所有东西都以参数传递进去。
这可以理解,因为除此之外就只能选择全局数据,而全局数据是邪恶的东西。对象技术改变了这一情况,因为如果你手上没有你所需要的东西,总可以叫另一个对象给你。因此,有了对象,你就不必把函数需要的所有东西都以参数传递给它了,你只需传给它足够的东西、让函数能从中获得自己需要的所有东西就行了。
函数需要的东西多半可以在函数的宿主类(host class)中找到。面向对象程序中的函数,其参数列通常比在传统程序要短得多。

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

如果「向既有对象发出一条请求」就可以取得原本位于参数列中的一份数据,那么你应该启动重构准则 Replace Parameter with Method(292)。上述的既有对象可能是函数所属 class 内的㆒个值域(field),也可能是另一个参数。你还可以运用 Preserve Whole Object(288)将来自同一对象的一堆数据收集起来,并以该对象替换它们。如果某些数据缺乏合理的对象归属,可使用 Introduce ParameterObject(295)为它们制造出一个「参数对象」。

此间存在一个重要的例外。有时候你明显不希望造成「被调用之对象」与「较大对象」间的某种依存关系。这时候将数据从对象中拆解出来单独作为参数,也很合情合理。但是请注意其所引发的代价。如果参数列太长或变化太频繁,你就需要重新考虑自己的依存结构(dependency structure)了。

5.Divergent Change(发散式变化)

我们希望软件能够更容易被修改——毕竟软件再怎么说本来就该是「软」的。一旦需要修改,我们希望能够跳到系统的某地点,只在该处作修改。如果不能做到这点,你就嗅出两种紧密相关的刺鼻味道中的一种了。

如果某个 class 经常因为不同的原因在不同的方向上发生变化,Divergent Change就出现了。当你看着一个 class 说:『呃,如果新加入一个数据库,我必须修改这三个函数;如果新出现一种金融工具,我必须修改这四个函数』,那么此时也许将这个对象分成两个会更好,这么一来每个对象就可以只因一种变化而需要修改。
当然,往往只有在加入新数据库或新金融工具后,你才能发现这一点。针对某一外界变化的所有相应修改,都只应该发生在单一 class 中,而这个新 class 内的所有内容都应该反应该外界变化。为此,你应该找出因着某特定原因而造成的所有变化,然后运用 Extract Class(149)将它们提炼到另一个 class 中。

6.Shotgun Surgery(霰弹式修改)

Shotgun Surgery 类似 Divergent Change,但恰恰相反。如果每遇到某种变化,你都必须在许多不同的 classes 内作出许多小修改以响应之,你所面临的坏味道就是 Shotgun Surgery。如果需要修改的代码散布㆕处,你不但很难找到它们,也
很容易忘记某个重要的修改。

这种情况下你应该使用 Move Method(142)和 Move Field(146)把所有需要修改的代码放进同一个 class。如果眼中没有合适的 class 可以安置这些代码,就创造一个。通常你可以运用 Inline Class(154)把一系列相关行为放进同一个 class。这可能会造成少量 Divergent Change,但你可以轻易处理它。

Divergent Change 是指「一个 class 受多种变化的影响」,Shotgun Surgery 则是指「一种变化引发多个 classes 相应修改」。这两种情况㆘你都会希望整理代码,取得「外界变化」与「待改类」呈现一对一关系的理想境地。

7.Feature Envy(依恋情结)

对象技术的全部要点在于:这是一种「将数据和加诸其㆖的操作行为包装在一起」的技术。有一种经典气味是:函数对某个 class 的兴趣高过对自己所处之 host class 的兴趣。这种孺慕之情最通常的焦点便是数据。无数次经验里,我们看到某个函数为了计算某值,从另一个对象那儿调用几乎半打的取值函数(getting method)。
疗法显而易见:把这个函数移至另一个地点。你应该使用 Move Method(142)把它移到它该去的㆞方。有时候函数中只有一部分受这种依恋之苦,这时候你应该使用 Extract Method(110)把这一部分提炼到独立函数中,再使用 Move Method(142)带它去它的梦中家园。

当然,并非所有情况都这么简单。一个函数往往会用上数个 classes 特性,那么它究竟该被置于何处呢?我们的原则是:判断哪个 class 拥有最多「被此函数使用」的数据,然后就把这个函数和那些数据摆在一起儿。如果先以 Extract Method(110)将这个函数分解为数个较小函数并分别置放于不同地点,上述步骤也就比较容易完成了。

有数个复杂精巧的模式(patterns)破坏了这个规则。说起这个话题,「四巨头」 [Gang of Four] 的 Strategy 和 Visitor 立刻跳入我的脑海,Kent Beck 的 Self Delegation [Beck] 也在此列。使用这些模式是为了对抗坏味道 Divergent Change。最根本的原则是:将总是一起变化的东西放在一块儿。「数据」和「引用这些数据」的行为总是一起变化的,但也有例外。如果例外出现,我们就搬移那些行为,保持「变化只在一地发生」。Strategy 和 Visitor 使你得以轻松修改函数行为,因为它们将
少量需被覆写(overridden)的行为隔离开来 — 当然也付出了「多一层间接性」的代价。

8.Data Clumps(数据泥团)

数据项(data items)就像小孩子:喜欢成群结队地待在一块儿。你常常可以在很多地方看到相同的三或四笔数据项:两个 classes 内的相同值域(field)、许多函数签名式(signature)中的相同参数。这些「总是绑在一起出现的数据」真应该放进属于它们自己的对象中。首先请找出这些数据的值域形式(field)出现点,运用 Extract Class(149)将它们提炼到㆒个独立对象㆗。然后将注意力转移到函数签名式(signature)上头,运用 Introduce Parameter Object(295)或 Preserve Whole Object(288)为它减肥。这么做的直接好处是可以将很多参数列缩短,简化函数调用动作。是的,不必因为 Data Clumps 只用㆖新对象的㆒部分值域而在意,只要你以新对象取代两个(或更多)值域,你就值回票价了。

一个好的评断办法是:删掉众多数据中的一笔。其它数据有没有因而失去意义?
如果它们不再有意义,这就是个明确信号:你应该为它们产生一个新对象。
缩短值域个数和参数个数,当然可以去除一些坏味道,但更重要的是:一旦拥有新对象,你就有机会让程序散发出一种芳香。得到新对象后,你就可以着手寻找Feature Envy,这可以帮你指出「可移至新 class」中的种种程序行为。不必太久,所有 classes 都将在它们的小小社会㆗充分发挥自己的生产力。

9.Primitive Obsession(基本型 别偏执)

大多数编程环境都有两种数据:结构型别(record types)允许你将数据组织成有意义的形式;基本型别(primitive types)则是构成结构型别的积木块。结构总是会带来一定的额外开销。它们有点像数据库中的表格,或是那些得不偿失(只为
做一两件事而创建,却付出太大额外开销)的东西。

对象的一个极具价值的东西是:它们模糊(甚至打破)了横亘于基本数据和体积较大的 classes 之间的界限。你可以轻松编写出一些与语言内置(基本)型别无异的小型 classes。例如 Java 就以基本型别表示数值,而以 class 表示字符串和日期——这两个型别在其它许多编程环境中都以基本型别表现。

对象技术的新手通常不愿意在小任务中运用小对象 — 像是结合数值和币别的 money class、含一个起始值和一个结束值的 range class、电话号码或邮政编码(ZIP)等等的特殊 strings。你可以运用 Replace Data Value with Object(175)将原本单独存在的数据值替换为对象,从而走出传统的洞窟,进入炙手可热的对象世界。
如果欲替换之数据值是 type code(型别码),而它并不影响行为,你可以运用 Replace Type Code with Class(218)将它换掉。如果你有相依于此 type code 的条件式,可运用 Replace Type Code with Subclass (213)或 Replace Type Code with State/Strategy(227)加以处理。

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

10.Switch Statements(switch 惊悚现身)

面向对象程序的一个最明显特征就是:少用 switch(或 case)语句。从本质中说,switch 语句的问题在于重复(duplication)。你常会发现同样的 switch 语句散布于不同地点。如果要为它添加一个新的 case 子句,你必须找到所有 switch 语句并修改它们。面向对象中的多态(polymorphism)概念可为此带来优雅的解决办法。

大多数时候,一看到 switch 语句你就应该考虑以「多态」来替换它。问题是多态该出现在哪儿?switch 语句常常根据 type code(型别码)进行选择,你要的是「与该 type code 相关的函数或 class」。所以你应该使用 Extract Method(110)将 switch 语句提炼到一个独立函数中,再以 Move Method(142)将它搬移到需要多态性的那个 class 里头。此时你必须决定是否使用 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(260)。

11. Parallel Inheritance Hierarchies(平行继承体系)

Parallel Inheritance Hierarchies 其实是 Shotgun Surgery 的特殊情况。在这种情况下,每当你为某个 class 增加一个 subclass,必须也为另一个 class 相应增加一个 subclass。如果你发现某个继承体系的 class 名称前缀和另一个继承体系的 class
名称前缀完全相同,便是闻到了这种坏味道。
消除这种重复性的一般策略是:让一个继承体系的实体(instances)指涉(参考、引用、refer to)另一个继承体系的实体(instances)。如果再接再厉运用 Move Method(142)和 Move Field(146),就可以将指涉端(referring class)的继承体系消弭于无形。

12. Lazy Class(冗赘类)

你所创建的每一个 class,都得有人去理解它、维护它,这些工作都是要花钱的。
如果一个 class 的所得不值其身价,它就应该消失。项目中经常会出现这样的情况:某个 class 原本对得起自己的身价,但重构使它身形缩水,不再做那么多工作;或开发者事前规划了某些变化,并添加一个 class 来应付这些变化,但变化实际上没有发生。不论上述哪一种原因,请让这个 class 庄严赴义吧。如果某些 subclass 没有做满足够工作,试试 Collapse Hierarchy(344)。对于几乎没用的组件,你应该以 Inline Class(154)对付它们。

13.Speculative Generality(夸夸其谈未来性)

这个令我们十分敏感的坏味道,命名者是 Brian Foote。当有人说『噢,我想我们总有一天需要做这事』并因而企图以各式各样的挂勾(hooks)和特殊情况来处理一些非必要的事情,这种坏味道就出现了。那么做的结果往往造成系统更难理解和维护。如果所有装置都会被用到,那就值得那么做;如果用不到,就不值得。
用不到的装置只会挡你的路,所以,把它搬开吧。

如果你的某个 abstract class 其实没有太大作用,请运用 Collapse Hierarchy(344)。
非必要之 delegation(委托)可运用 Inline Class(154)除掉。如果函数的某些参数未被用到,可对它实施 Remove Parameter(277)。如果函数名称带有多余的抽象意味,应该对它实施Rename Method(273)让它现实一些。

如果函数或 class 的惟㆒用户是 test cases(测试用例),这就飘出了坏味道Speculative Generality。如果你发现这样的函数或 class,请把它们连同其 test cases都删掉。但如果它们的用途是帮助 test cases 检测正当功能,当然必须刀下留人。

14. Temporary Field(令人迷惑的暂时值域)

有时你会看到这样的对象:其内某个 instance 变量仅为某种特定情势而设。这样的代码让人不易理解,因为你通常认为对象在所有时候都需要它的所有变量。在变量未被使用的情况下猜测当初其设置目的,会让你发疯。

请使用 Extract Class(149)给这个可怜的孤儿创造一个家,然后把所有和这个变量相关的代码都放进这个新家。也许你还可以使用 Introduce Null Object(260)在「变量不合法」的情况下创建一个 Null 对象,从而避免写出「条件式代码」。

如果 class 中有一个复杂算法,需要好几个变量,往往就可能导致坏味道 Temporary Field 的出现。由于实现者不希望传递一长串参数(想想为什么),所以他把这些参数都放进值域(fields)中。但是这些值域只在使用该算法时才有效,其它情况下只会让人迷惑。这时候你可以利用 Extract Class(149)把这些变量和其相关函数提炼到一个独立 class 中。提炼后的新对象将是一个 method object [Beck] (译注:其存在只是为了提供调用函数的途径,class 本身并无抽象意味)。

15. Message Chains(过度耦合的消息链)

如果你看到用户向一个对象索求(request)另一个对象,然后再向后者索求另一个对象,然后再索求另一个对象……这就是 Message Chain。实际代码中你看到的可能是一长串 getThis()或一长串临时变量。实行这种方式,意味客户将与查找过程中的航行结构(structure of navigation)紧密耦合。一旦对象间的关系发生任何变化,客户端就不得不作出相应修改。

这时候你应该使用 Hide Delegate(157)。你可以在 Message Chain 的不同位置进行这种重构手法。理论上你可以重构 Message Chain 中的任何一个对象,但这么做往往会把所有中介对象(intermediate object)都变成 Middle Man。通常更好的选择是:先观察 Message Chain 最终得到的对象是用来干什么的,看看能否以Extract Method(110)把使用该对象的代码提炼到一个独立函数中,再运用 Move Method(142)把这个函数推入 Message Chain。如果这条链㆖的某个对象有多位客户打算航行此航线的剩余部分,就加一个函数来做这件事。

有些人把任何函数链(method chain。译注:就是 Message Chain;面向对象领域中所谓「发送消息」就是「调用函数」)都视为坏东西,我们不这样想。呵呵,我们的冷静镇定是出了名的,起码在这件事情上是这样。

16 Middle Man(中间转手人)

对象的基本特征之一就是封装(encapsulation)— 对外部世界隐藏其内部细节。封装往往伴随 delegation(委托)。比如说你问主管是否有时间参加一个会议,他就把这个消息委托给他的记事簿,然后才能回答你。很好,你没必要知道这位主管到底使用传统记事簿或电子记事簿抑或秘书来记录自己的约会。

但是人们可能过度运用 delegation。你也许会看到某个 class 接口有一半的函数都委托给其它 class,这样就是过度运用。这时你应该使用 Remove Middle Man(160),
直接和实责对象打交道。如果这样「不干实事」的函数只有少数几个,可以运用 InlineMethod(117)把它们 “inlining”,放进调用端。如果这些 Middle Man 还有其它行为,你可以运用 Replace Delegation with Inheritance(355)把它变成实责对象的 subclass,这样你既可以扩展原对象的行为,又不必负担那么多的委托动作。

17 Inappropriate Intimacy(狎昵关系)

有时你会看到两个 classes 过于亲密,花费太多时间去探究彼此的 private 成分。如果这发生在两个「人」之间,我们不必做卫道之士;但对于 classes,我们希望它们严守清规。

就像古代恋人一样,过份狎昵的 classes 必须拆散。你可以采用 Move Method(142)和 Move Field(146)帮它们划清界线,从而减少狎昵行径。你也可以看看是否运用 Change Bidirectional Association to Unidirectional(200)让其中一个 class 对另一个斩断情思。如果两个 classes 实在是情投意合,可以运用 Extract Class(149)把两者共同点提炼到一个安全地点,让它们坦荡地使用这个新 class。或者也可以尝试运用 Hide Delegate(157)让另一个 class 来为它们传递相思情。

继承(inheritance)往往造成过度亲密,因为 subclass 对 superclass 的了解总是超过 superclass 的主观愿望。如果你觉得该让这个孩子独自生活了,请运用 Replace Inheritance with Delegation(352)让它离开继承体系。

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

如果两个函数做同㆒件事,却有着不同的签名式(signature),请运用 Rename Method(273)根据它们的用途重新命名。但这往往不够,请反复运用 Move Method(142)将某些行为移入 classes,直到两者的协议(protocols)㆒致为止。如果你必须重复而赘余地移入代码才能完成这些,或许可运用 Extract Superclass(336)为自己赎点罪。

19. Incomplete Library Class(不完美的程序库类)

复用(reuse)常被视为对象的终极目的。我们认为这实在是过度估计了(我们只是使用而已)。但是无可否认,许多编程技术都建立在 library classes(程序库类)的基础中,没人敢说是不是我们都把排序算法忘得一干二净了。

library classes 构筑者没有未卜先知的能力,我们不能因此责怪他们。毕竟我们自己也几乎总是在系统快要构筑完成的时候才能弄清楚它的设计,所以 library 构筑者的任务真的很艰巨。麻烦的是 library 的形式(form)往往不够好,往往不可能让我们修改其中的 classes 使它完成我们希望完成的工作。这是否意味那些经过实践检验的战术如 Move Method(142)等等,如今都派不上用场了?

幸好我们有两个专门应付这种情况的工具。如果你只想修改 library classes 内的中两个函数,可以运用 Introduce Foreign Method(162);如果想要添加一大堆额外行为,就得运用 Introduce Local Extension(164)。

20. Data Class (纯稚的数据类)

所谓 Data Class 是指:它们拥有一些值域(fields),以及用于访问(读写)这些值域的函数,除此之外一无长物。这样的 classes 只是一种「不会说话的数据容器」,
它们几乎一定被其它 classes 过份细琐地操控着。这些 classes 早期可能拥有 public 值域,果真如此你应该在别㆟注意到它们之前,立刻运用 Encapsulate Field(206)将它们封装起来。如果这些 classes 内含容器类的值域(collection fields),你应该检查它们是不是得到了恰当的封装;如果没有,就运用 Encapsulate Collection(208)把它们封装起来。对于那些不该被其它 classes 修改的值域,请运用 Remove Setting Method(300)。

然后,找出这些「取值/设值」函数(getting and setting methods)被其它 classes 运用的地点。尝试以 Move Method(142)把那些调用行为搬移到 Data Class 来。如果无法搬移整个函数,就运用 Extract Method(110)产生一个可被搬移的函数。
不久之后你就可以运用 Hide Method(303)把这些「取值/设值」函数隐藏起来了。

Data Class 就像小孩子。作为一个起点很好,但若要让它们像「成年(成熟)」的对象那样参与整个系统的工作,它们就必须承担一定责任。

21 Refused Bequest(被拒绝的遗赠)

subclasses 应该继承 superclass 的函数和数据。但如果它们不想或不需要继承,又该怎么办呢?它们得到所有礼物,却只从中挑选几样来玩!

按传统说法,这就意味继承体系设计错误。你需要为这个 subclass 新建一个兄弟(sibling class),再运用 Push Down Method(328)和 Push Down Field(329)把所有用不到的函数下推给那兄弟。这样一来 superclass 就只持有所有 subclasses 共
享的东西。常常你会听到这样的建议:所有 superclasses 都应该是抽象的(abstract)。

既然使用「传统说法」这个略带贬义的词,你就可以猜到,我们不建议你这么做,起码不建议你每次都这么做。我们经常利用 subclassing 手法来复用一些行为,并发现这可以很好㆞应用于日常工作。这也是一种坏味道,我们不否认,但气味通常并不强烈。所以我们说:如果 Refused Bequest 引起困惑和问题,请遵循传统忠告。但不必认为你每次都得那么做。十有八九这种坏味道很淡,不值得理睬。

如果 subclass 复用了 superclass 的行为(实现),却又不愿意支持 superclass 的接口,Refused Bequest 的坏味道就会变得浓烈。拒绝继承 superclass 的实现,这一点我们不介意;但如果拒绝继承 superclass 的接口,我们不以为然。不过即使你不愿意继承接口,也不要胡乱修改继承体系,你应该运用 Replace Inheritance with Delegation(352)来达到目的。

22 Comments(过多的注释)

别担心,我们并不是说你不该写注释。从嗅觉上说,Comments 不是一种坏味道;
事实上它们还是一种香味呢。我们之所以要在这里提到 Comments,因为人们常把它当作除臭剂来使用。常常会有这样的情况:你看到㆒段代码有着长长的注释,然后发现,这些注释之所以存在乃是因为代码很糟糕。这种情况的发生次数之多,实在令人吃惊。

Comments 可以带我们找到本章先前提到的各种坏味道。找到坏味道后,我们首先应该以各种重构手法把坏味道去除。完成之后我们常常会发现:注释已经变得多余了,因为代码已经清楚说明了一切。

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

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