大家接手的祖传代码都是屎山,那么何时重构、何时停止?
如何删除一个实例变量、如何产生一个继承体系,相信你也都会,因为本就简单,但要你解释何时做这些动作呢?你好像不那么轻松应答了呢。 编程不仅是一门艺术,也是需要科学的说服力。
我们看过很多很多代码,观察这些代码时,我们学会了从中找寻某些特定结构,这些结构指出了重构的可能性。谁都无法给你一个何时必须重构的精确衡量标准。 但经验看来,没有任何规矩比经验丰富大佬的直觉。我只会告诉你一些迹象,它会指出“这里有一个可以用重构解决的问题”。你必须培养自己的判断力:
- 判断一个类内有多少实例变量算是太大
- 一个函数内有多少行代码才算太长
如果你无法确定该采用哪一种重构手法,请阅读本系列文章吧!下面来看各种让人难受的坏味道吧!
1 神秘命名(Mysterious Name)
整洁代码最重要的一环就是好的名字,我们要深思熟虑如何给函数、模块、变量和类命名,使之清晰表明功能和用法。 然而,命名是编程中最难的两件事之一。正因如此,改名可能是最常用的重构手法,包括:
- 改变函数声明(用于给函数改名)
- 变量改名
- 字段改名
很多人经常不愿意给程序元素改名,觉得不值得,但好的名字能节省未来开发者的大把猜谜时间。
改名不仅是修改名字,若你想不出一个好名字,说明背后很可能潜藏着更深的设计问题。为一个恼人的
名字所付出的纠结,常常能推动我们对代码进行精简。
2 重复代码(Duplicated Code)
如果你在一个以上的地点看到相同的代码结构,那么可以肯定:设法将它们合而为一,程序会变得更好。
一旦有重复代码存在,阅读这些重复的代码时你就必须加倍仔细,留 意其间细微的差异。如果要修改重复代码,你必须找出所有的副本去修改。
-
最单纯的重复代码就是同一个类的两个函数含有相同表达式
这时,你需要采用提炼函数:提炼出重复代码,然后让这两个地点都调用被提炼出来的那段代码
-
若重复代码只是相似,而非完全相同
首先尝试用【移动语句】重组代码顺序,把相似部分放在一起以便提炼
-
若重复的代码段位于同一个父类的不同子类,可使用【函数上移】避免在两个子类之间互相调用
3 过长函数(Long Function)
据经验,活最长、最好的程序,其中的函数都较短。初次接触到这种代码库的程序员常常会觉得“计算都没有发生”——程序里满是无穷无尽的委托调用。但和这样的程序共处几年后,你会明白这些小函数的意义:间接带来更好的阐释力、更易于分享、更多的选择。
其实,我们都知道函数越长,越难理解。早期编程语言,子程序调用需要额外开销,使得人们不乐意使用小函数,但现代编程语言几乎已完全免除进程内的函数调用开销。固然,小函数也会给代码阅读者带来一些负担,因为你必须经常切换上下文,才能看明白函数在做什么。但IDEA让你能在函数的调用处与声明处间快速跳转或同时看到这两处,让你不必来回跳转。说到底,让小函数易于理解的关键还是在于好的命名。若你能给函数起个好名,阅读者就能通过名字了解函数的作用,根本不必去看代码细节。 最终效果:你应该积极分解函数,这需要遵循原则:每当感觉需要以注释来说明时,就把需要说明的东西写进一个独立函数,并以其用途(而非实现手法)命名。可以对一组甚至一行代码做这件事。哪怕替换后的函数调用动作比函数自身还长,只要函数名称能够解释其用途,就该毫不犹豫这么做。
焦点不在于函数的长度,而在于函数“做什么”和“如何做”之间的语义距离。 99%的场景,要把函数变短,只需使用【提炼函数】。
若函数内有大量参数和临时变量,它们会对你的函数提炼形成阻碍,最终会把许多参数传递给被提炼出来的新函数,导致可读性几乎无任何提升。此时,可使用【以查询取代临时变量】消除这些临时元素。
【引入参数对象】和 【保持对象完整】则能将过长参数列表变得更简洁。
若一顿操作后,仍有大量临时变量和参数,就使杀手锏【命令取代函数】。
如何确定该提炼哪一段代码
寻找注释!
它们指出了代码用途和实现手法之间的语义距离。它们在提醒你:可以将这段代码替换成一个函数,而且可以在注释的基础上给这个函数命名!就算只有一行代码,若它需要以注释说明,也值得将它提炼到独立函数。
条件表达式和循环常常也是提炼的信号,可以用【分解条件表达式】处理条件表达式。
繁琐的switch语句,其中的每个分支都应通过【提炼函数】变成独立函数调用。若有多个switch语句基于同一个条件进行分支选择,应使用【以多态取代条件表达式】。
至于循环,应该将循环和循环内的代码提炼到一个独立函数。若你发现提炼出的循环很难命名,可能是因为它做了几件不同的事,请大胆使用【拆分循环】将其拆分成各自独立的任务!
4 过长参数列表(Long Parameter List)
初学Java时,老师说:把函数所需的所有东西都以参数的形式传递进去。此外,就只能选择全局数据,而全局数据很快就会变成邪恶的东西。但过长的参数列表本身也经常令人迷惑。 若能向某个参数发起查询而获得另一个参数的值, 则可使用【以查询取代参数】去掉这第二个参数。
若你发现自己正在从现有的数据结构中抽出很多数据项,考虑【保持对象完整】,直接传入原来的数据结构。若有几项参数总是同时出现,可用【引入参数对象】将其合并成一个对象。
若某参数被用作区分函数行为的标记(flag),可用【移除标记参数】。
使用类能有效缩短参数列表。若多个函数有同样的几个参数,引入一个类就很有意义。可用【函数组合成类】,将这些共同的参数变成这个类的字段。若戴上函数式编程的帽子,这重构过程就是创造了一组部分应用函数(partially applied function)。
3.5 全局数据(Global Data)
全局数据算得上是最刺鼻的坏味道之一,其问题在于,从代码库的任何一个角落都能修改它,而且没有任何机制可探测出到底哪段代码做出的修改。如此反复,全局数据铸就了一堆诡异bug,而问题根源却在遥远的别处,想找到出错代码难于上青天!
全局数据最常见形式就是全局变量,类变量和单例也有这样问题。首要防御手段是【封装变量】,每当看到可能被各处代码污染的数据,这总是第一招:
- 把全局数据用一个函数包装起来,至少你就能看见修改它的地方,并开始控制对其的访问
- 随后,最好将这个函数(及其封装的数据)搬移到一个类或模块,只允许模块内的代码使用它,从而尽量控制其作用域
能被修改的全局数据尤其可憎。若能保证在程序启动后就不再修改,这样的全局数据还算相对安全,不过得有编程语言提供这样的保证才行啊!
6 可变数据(Mutable Data)
对数据的修改经常导致出乎意料的结果和难以发现的bug。我在一处更新数据,却没有意识到软件中的另一处期望着完全不同的数据,于是一个功能失效了——若bug只在很罕见情况下发生,要找出原因就更困难。因此,函数式编程完全建立在“数据永不改变”的概念基础上:若要更新一个数据结构,就返回一份新的数据副本,旧数据仍保持不变。
不过这样的编程语言仍相对小众,大多数程序员使用的编程语言还是允许修改变量值。即便如此,也不应该忽视不可变性带来的优势,仍有很多办法可以约束对数据的更新,降低其使用风险。
可以用【封装变量】确保所有数据更新操作都通过很少几个函数进行,使其更容易监控和演进。若一个变量在不同时候被用于存储不同东西,可用【拆分变量】将其拆分为各自不同用途的变量,从而避免危险的更新操作。
使用【移动语句】和【提炼函数】,尽量把逻辑从处理更新操作的代码中搬移出来,将没有副作用的代码与执行数据更新操作的代码分开。设计API时,可使用【将查询函数和修改函数分离】确保调用者不会调到有副作用的代码,除非真要更新数据!
我们还乐于尽早使用移除设值函数,有时只是把设值函数的使用者找出来看看,就能帮我们发现缩小变量作用域的机会。
若可变数据的值能在其他地方计算出来,这就是一个特别刺鼻的坏味道。它不仅会造成困扰、bug和加班,而且毫无必要。消除这种坏味道的办法很简单,使用【以查询取代派生变量】。
- 若变量作用域只有几行代码,即使其中的数据可变,也不是啥大问题;但随着变量作用域的扩展,风险也随之增大。可用【函数组合成类】或【函数组合成变换】限制需要对变量进行修改的代码量
- 若一个变量在其内部结构中包含了数据,通常最好不要直接修改其中的数据,而是用【将引用对象改为值对象】令其直接替换整个数据结构
7 发散式变化(Divergent Change)
我们希望软件能更容易被修改,所以才是“软”的。一旦需要修改,希望能够跳到系统的某一点,只在该处做修改。若做不到这点,你就嗅出两种紧密相关的刺鼻味道中的一种。
若某个模块经常因为不同原因在不同方向上发生变化,发散式变化就出现了。当你看着一个类说:
- 若新加入一个DB,我必须修改这3个函数
- 若新出现一种金融工具,我必须修改这4个函数
这就是发散式变化的征兆。DB交互和金融逻辑处理是两个不同的上下文,将它们分别搬移到各自独立的模块中,能让程序变得更好:每当要对某个上下文做修改时,只需理解这个上下文,而不必操心另一个。
“每次只关心一个上下文”很重要!特别是如今这个信息爆炸、脑容量不够的年代。当然,往往只有在加入新数据库或新金融工具后,你才能发现这个坏味道。在程序刚开发出来,还在随着软件系统的能力不断演进时,上下文边界通常不是那么清晰。若发生变化的两个方向自然地形成了先后次序(比如,先从DB取出数据,再对其进行金融逻辑处理),就能用【拆分阶段】将两者分开,两者之间通过一个清晰的数据结构进行沟通。
- 若两个方向之间有更多的来回调用,应先创建适当的模块,然后用【搬移函数】把处理逻辑分开
- 若函数内部混合了两类处理逻辑,应先用【提炼函数】将其分开,再做搬移
- 若模块是以类的形式定义的,就可以用【提炼类】做拆分。
8 霰弹式修改(Shotgun Surgery)
类似发散式变化,但又恰恰相反。若每遇到某种变化,都必须在许多不同的类内做出许多小修改,那多半就是霰弹式修改。若要修改的代码散布四处,你不但很难找全它们,也很容易错过某个重要的修改。
应该使用【搬移函数】和【搬移字段 】把所有需要修改的代码放进同一个模块
- 若有很多函数都在操作相似的数据,可使用【函数组合成类 】
- 若有些函数的功能是转化或者充实数据结构, 可使用【函数组合成变换】
- 若一些函数的输出可组合后提供给一段专门使用这些计算结果的逻辑,这时常用得上【拆分阶段】
面对霰弹式修改,一个常用的策略就是使用与内联相关的重构,如【内联函数】或【内联类】把本不该分散的逻辑拽回一处。完成内联后,你可能会闻到过长函数或者过大的类的味道,不过你总 可以用与提炼相关的重构手法将其拆解成更合理的小块。即便如此钟爱小型的函数和类,我们也并不担心在重构的过程中暂时创建一些较大的程序单元。
9 依恋情结(Feature Envy)
模块化,就是力求将代码分出区域,最大化区域内部的交互、最小化跨区域的交互。但有时你会发现,一个函数跟另一个模块中的函数或者数据交流格外频繁,远胜于在 自己所处模块内部的交流,这就是依恋情结的典型情况。
我们经常看到某个函数为了计算某个值,从另一个对象那儿调用几乎半打的取值函数。疗法显而易见:这个函数想跟这些数据待在一起,那就使用【搬移函数】把它移过去。
有时,函数中只有一部分受这种依恋之苦,这时候应该使用【提炼函数】把这一部分提炼到独立的函数中,再使用【搬移函数】带它去它的梦想家园。
并非所有情况都这么简单。一个函数往往会用到几个模块的功能,那它究竟该被置于何处?原则
是:判断哪个模块拥有的此函数使用的数据最多,然后就把这个函数和那些数据摆在一起。如果先以【提炼函数】将这个函数分解为数个较小的函数并分别置放于不同地点,上述步骤也就比较容易完成了。
有几个复杂精巧的模式破坏了这条规则。说起这个话题,GoF[gof]的策略(Strategy)模式和访问者 (Visitor)模式立刻跳入我的脑海,Kent Beck的Self Delegation模式[Beck SBPP]也在此列。使用这些模式是为了对抗发散式变化这一坏味道。最根本的原则是:将总是一起变化的东西放在一块儿。数据和引用这些数据的行为总是一起变化的,但也有例外。如果例外出现,我们就搬移那些行为,保持变化只在一地发生。策略模式和和访问者模式使你得以轻松修改函数的行为,因为它们将少量需被覆写的行为隔离开来——当然也付出了“多一层间接性”的代价。
10 数据泥团(Data Clumps)
数据项就像小孩子,喜欢成群结队地待在一块儿。你常常可以在很多地方看到相同的三四项数据:两个类中相同的字段、许多函数签名中相同的参数。这些总是绑在一起出现的数据真应该拥有属于它们自己的对象。首先请找出这些数据以字段形式出现的地方,运用【提炼类】将它们提炼到一个独立对象中。然后将注意力转移到函数签名上,运用引入【参数对象】或【保持对象完整】为它瘦身。
这么做的直接好处是可以将很多参数列表缩短,简化函数调用。是的,不必在意数据泥团只用上新对象的一部分字段, 只要以新对象取代两个(或更多)字段,就值得这么做。
一个好的评判办法:删掉众多数据中的一项。如果这么做,其他数据有没有因而失去意义?如果它们不再有意义,这就是一个明确信号:你应该为它们产生一个新对象。
我们在这里提倡新建一个类,而非简单的记录结构,因为一旦拥有新的类,你就有机会让程序散发出一种芳香。 得到新的类后,就能着手寻找“依恋情结”,这可以帮你指出能够移至新类中的种种行为。这是一种强大的动力:有用的类被创建出来,大量的重复被消除,后续开发得以加速,原来的数据泥团终于在它们的小社会中充分发挥价值。
11 基本类型偏执(Primitive Obsession)
大多数编程环境都大量使用基本类型,即整数、浮点数和字符串等。一些库会引入一些小对象,如日期。但我们发现一个很有趣的现象:很多程序员不愿意创建对自己的问题域有用的基本类型,如钱、坐标、范围等。于是,我们看到把钱当作普通数字来计算的情况、计算物理量时无视单位(如把英寸与毫米相加)的情况以及大量类似if (a < upper && a > lower)这样的代码。
字符串是这种坏味道的最佳培养皿,比如,电话号码不只是一串字符。一个体面的类型,至少能包含一致的显示逻辑,在用户界面上需要显示时可以使用。“用字符串来代表类似这样的数据”是如此常见的臭味,以至于人们给这类变量专门起了一个名字,叫它们“类字符串类型”(stringly typed)变量。
可以运用【以对象取代基本类型】将原本单独存 在的数据值替换为对象,从而走出传统的洞窟,进入炙手可
热的对象世界。
- 若想要替换的数据值是控制条件行为的类型码,则可以运用【以子类取代类型码】加上【以多态取代条件表达式】的组合将它换掉
- 若你有一组总是同时出现的基本类型数据,这就是数据泥团的征兆,应该运用【提炼类】和【引入参数对象】来处理
12 重复的switch (Repeated Switches)
如果你跟真正的面向对象布道者交谈,他们很快就会谈到switch语句的邪恶。在他们看来,任何switch语句都应该用以多态取代条件表达式(272)消除掉。我们甚至还听过:所有条件逻辑都应该用多态取代,绝大多数if语句都应该被扫进历史的垃圾桶。
即便在不知天高地厚的青年时代,我们也从未无条件地反对条件语句。在本书第1版中,这种坏味道被称为“switch语句”(Switch Statements),那是因为在20世纪90年代末期,程序员们太过于忽视多态的价值,我们希望矫枉过正。
如今的程序员已经更多地使用多态,switch语句也不再像15年前那样有害无益,很多语言支持更复杂的switch语句,而不只是根据基本类型值来做条件判断。因此,我们现 在更关注重复的switch:在不同的地方反复使用同样的switch逻辑(可能是以switch/case语句的形式,也可能是以连续的if/else语句的形式)。重复的switch的问题在于:每当你想增加一个选择分支时,必须找到所有的switch,并逐一更新。多态给了我们对抗这种黑暗力量的武器,使我们得到更优雅的代码库。
13 循环语句(Loops)
从最早的编程语言开始,循环就一直是程序设计的核心要素。但我们感觉如今循环已经有点儿过时,就像喇叭裤和植绒壁纸那样。如今,函数作为一等公民已经得到了广泛的支持,因此我们可以使用【以管道取代循环 】来让这些老古董退休。我们发现,管道操作(如filter和map)可以帮助我们更快地看清被处理的元素以及处理它们的动作。
14 冗赘的元素(Lazy Element)
程序元素(如类和函数)能给代码增加结构,从而支持 变化、促进复用或者哪怕只是提供更好的名字也好,但有时我们真的不需要这层额外的结构。可能有这样一个函数,它的名字就跟实现代码看起来一模一样;也可能有这样一个类,根本就是一个简单的函数。这可能是因为,起初在编写这个函数时,程序员也许期望它将来有一天会变大、变复杂,但那一天从未到来;也可能是因为,这个类原本是有用 的,但随着重构的进行越变越小,最后只剩了一个函数。不 论上述哪一种原因,请让这样的程序元素庄严赴义吧。通常你只需要使用【内联函数】或是【内联类】。如果这个类处于一个继承体系中,可以使用【折叠继承体系】。
15 夸夸其谈通用性(Speculative Generality)
令我们十分敏感的坏味道,命名者是Brian Foote。当有人说“噢,我想我们总有一天需要做这事”, 并因而企图以各式各样的钩子和特殊情况来处理一些非必要的事情,这种坏味道就出现了。这么做的结果往往造成系统更难理解和维护。如果所有装置都会被用到,就值得那么做;如果用不到,就不值得。用不上的装置只会挡你的路,所以,把它搬开吧。
如果你的某个抽象类其实没有太大作用,请运用折叠继承体系(380)。不必要的委托可运用内联函数(115)和内联类(186)除掉。如果函数的某些参数未被用上,可以用改变函数声明(124)去掉这些参数。如果有并非真正需要、只是为不知远在何处的将来而塞进去的参数,也应该用改变函数声明(124)去掉。
如果函数或类的唯一用户是测试用例,这就飘出了坏味道“夸夸其谈通用性”。如果你发现这样的函数或类,可以 先删掉测试用例,然后使用移除死代码(237)。
16 临时字段(Temporary Field)
这样的类:其内部某个字段仅为某种特定情况而设。这样的代码让人不易理解,因为你通常认为对象
在所有时候都需要它的所有字段。在字段未被使用的情况下猜测当初设置它的目的,会让你发疯。
请使用【提炼类】给这个可怜的孤儿创造一个家,然后用【搬移函数】把所有和这些字段相关的代码都放进这个新家。也许你还可以使用【引入特例】在“变量不合法”的情况下创建一个替代对象,从而避免写出条件式代码。
17 过长的消息链(Message Chains)
如果你看到用户向一个对象请求另一个对象,然后再向后者请求另一个对象,然后再请求另一个对象……这就是消息链。在实际代码中你看到的可能是一长串取值函数或一长串临时变量。采取这种方式,意味客户端代码将与查找过程中的导航结构紧密耦合。一旦对象间的关系发生任何变化,客户端就不得不做出相应修改。
这时候应该使用隐藏委托关系(189)。你可以在消息链的不同位置采用这种重构手法。理论上,你可以重构消息链上的所有对象,但这么做就会把所有中间对象都变成“中间人”。通常更好的选择是:先观察消息链最终得到的对象是用来干什么的,看看能否以提炼函数(106)把使用该对象的代码提炼到一个独立的函数中,再运用搬移函数把这个函数推入消息链。如果还有许多客户端代码需要访问链上的其他对象,同样添加一个函数来完成此事。
有些人把任何函数链都视为坏东西,我们不这样想。我们的冷静镇定是出了名的,起码在这件事上是这样的。
18 中间人(Middle Man)
对象的基本特征之一就是封装:对外部世界隐藏其内部细节。
封装往往伴随着委托。比如,你问主管是否有时间参加一个会议,他就把这个消息“委托”给他的记事簿,然后才能回答你。很好,你没必要知道这位主管到底使用传统记事簿、电子记事簿抑或是秘书,来记录自己的行程。
但人们可能过度运用委托。你也许会看到某个类的接口有一半的函数都委托给其他类,这样就是过度运用。这时应该使用【移除中间人】,直接和真正负责的对象打交道。如果这样“不干实事”的函数只有少数几个,可以运用【内联函数】把它们放进调用端。如果这些中间人还有其他行为,可以运用以委托取代超类(399)或者以委托取代子类(381)把它变成真正的对象,这样你既可以扩展原对象的行为,又不必负担那么多的委托动作。
19 内幕交易(Insider Trading)
软件开发者喜欢在模块之间建起高墙,极其反感在模块之间大量交换数据,因为这会增加模块间的耦合。在实际情况里,一定的数据交换不可避免,但我们必须尽量减少这种情况,并把这种交换都放到明面上来。
- 如果两个模块总是在咖啡机旁边窃窃私语,就应该用搬移函数(198)和搬移字段(207)减少它们的私下交流
- 如果两个模块有共同的兴趣,可以尝试再新建一个模块,把这些共用的数据放在一个管理良好的地方;或者用隐藏委托关系(189),把另一个模块变成两者的中介
- 继承常会造成密谋,因为子类对超类的了解总是超过后者的主观愿望。如果你觉得该让这个孩子独立生活了,请运用以委托取代子类(381)或以委托取代超类(399)让它离开继承体系
20 过大的类(Large Class)
如果想利用单个类做太多事情,其内往往就会出现太多字段。一旦如此,重复代码也就接踵而至了。
运用提炼类(182)将几个变量一起提炼至新类内。提炼时应该选择类内彼此相关的变量,将它们放在一 起。例如,depositAmount和depositCurrency可能应该隶属同一个类。通常,如果类内的数个变量有着相同的前缀或后缀,这就意味着有机会把它们提炼到某个组件内。如果这个组件适合作为一个子类,你会发现提炼超类(375)或者以子类取代类型码(362)(其实就是提炼子类)往往比较简单。
有时候类并非在所有时刻都使用所有字段。若果真如此,你或许可以进行多次提炼。 和“太多实例变量”一样,类内如果有太多代码,也是代码重复、混乱并最终走向死亡的源头。最简单的解决方案:把多余的东西消弭于类内部。如果有5个“百行函数”,它们之中很多代码都相同,那么或许你可以把它们变成5个“十行函数”和10个提炼出来的“双行函数”。
观察一个大类的使用者,经常能找到如何拆分类的线索。看看使用者是否只用到了这个类所有功能的一个子集,每个这样的子集都可能拆分成一个独立的类。一旦识别出一个合适的功能子集,就试用提炼类(182)、提炼超类(375)或是以子类取代类型码(362)将其拆分出来。
21 异曲同工的类(AlternativeClasses with Different Interfaces)
使用类的好处之一就在于可以替换:今天用这个类,未来可以换成用另一个类。但只有当两个类的接口一致时,才能做这种替换。可以用【改变函数声明】 将函数签名变得一致。但这往往还不够,请反复运用搬移函数将某些行为移入类中,直到两者的协议一致为止。如果搬移过程造成了重复代码,或许可运用提炼超类补偿一下。
22 纯数据类(Data Class)
它们拥有一些字段,以及用于访问(读写)这些字段的函数,除此之外一无长物。
这样的类只是一种不会说话的数据容器,它们几乎一定被其他类过分细琐地操控着。这些类早期可能拥有public字段,若果真如此,你应该在别人注意到它们之前,立刻运用【封装记录】将它们封装起来。对于那些不该被其他类修改的字段,请运用【移除设值函数】。
然后,找出这些取值/设值函数被其他类调用的地点。尝试以搬移函数(198)把那些调用行为搬移到纯数据类里来。如果无法搬移整个函数,就运用提炼函数(106)产生一个可被搬移的函数。
纯数据类常常意味着行为被放在了错误的地方。即只要把处理数据的行为从客户端搬移到纯数据类里来, 就能使情况大为改观。但也有例外情况,一个最好的例外情况就是,纯数据记录对象被用作函数调用的返回结果,比如使用【拆分阶段】之后得到的中转数据结构就是这种情况。这种结果数据对象有一个关键的特征:它是不可修改的(至少在拆分阶段(154)的实际操作中是这样)。不可修改的字段无须封装,使用者可以直接通过字段取得数据,无须通过取值函数。
23 被拒绝的遗赠(Refused Bequest)
子类应该继承超类的函数和数据。但如果它们不想或不需要继承,又该怎么办呢?它们得到所有礼物,却只从中挑选几样来玩!
按传统说法,这就意味着继承体系设计错误。你需要为这个子类新建一个兄弟类,再运用【函数下移】和【字段下移】把所有用不到的函数下推给那个兄弟。这样一来,超类就只持有所有子类共享的东西。你常常会听到这样的建议:所有超类都应该是抽象(abstract)的。
既然使用“传统说法”这个略带贬义的词,你就可以猜到,我们不建议你这么做,起码不建议你每次都这么做。我们经常利用继承来复用一些行为,并发现这可以很好地应用于日常工作。这也是一种坏味道,我们不否认,但气味通常并不强烈,所以我们说,如果“被拒绝的遗赠”正在引起困惑和问题,请遵循传统忠告。但不必认为你每次都得那么做。十有八九这种坏味道很淡,不值得理睬。
如果子类复用了超类的行为(实现),却又不愿意支持超类的接口,“被拒绝的遗赠”的坏味道就会变得很浓烈。
拒绝继承超类的实现,这一点我们不介意;但如果拒绝支持超类的接口,这就难以接受了。既然不愿意支持超类的接口,就不要虚情假意地糊弄继承体系,应该运用以委托取代子类(381)或者以委托取代超类(399)彻底划清界限。
24 注释(Comments)
不是说你不该写注释。从嗅觉上说,注释不但不是一种坏味道,事实上它们还是一种香味。我们之所以要在这里提到注释,是因为人们常把它当作“除臭剂”来使用。常常会有这样的情况:你看到一段代码有着长
长的注释,然后发现,这些注释之所以存在乃是因为代码很糟糕。这种情况的发生次数之多,实在令人吃惊。 注释可以带我们找到本章先前提到的各种坏味道。找到坏味道后,我们首先应该以各种重构手法把坏味道去除。完成之后我们常常会发现:注释已经变得多余了,因为代码已经清楚地说明了一切。
- 如果你需要注释来解释一块代码做了什么,试试【提炼函数】
- 如果函数已经提炼出来,但还是需要注释来解释其行为,试试用【改变函数声明】为它改名
- 如果你需要注释说明某些系统的需求规格,试试【引入断言】
当你感觉需要撰写注释时,请先尝试重构,试着让所有注释都变得多余,如果你不知道该做什么,这才是注释的良好运用时机。除了用来记述将来的打算之外,注释还可以用来标记你并无十足把握的区域。你可以在注释里写下自己“为什么做某某事”。这类信息可以帮助将来的修改者,尤其是那些健忘的