《重构-改善既有代码的设计 第3章代码的坏味道》学习笔记

何时重构、何时停止重构
学会判断一个类内有多少实例变量算是太大、一个函数内有多少行代码算太长

下面是对各种坏味道的简短汇集和解释

Duplicated Code
Long Method
Large Class
Long Parameter List
Divergent Change(多种变化导致一个类的变化)
Shotgun Surgery(一种变化导致多个类的变化)
Feature Envy(函数对某个类的兴趣高于对自己所处类的兴趣。)
Data Clumps(多个经常一起出现的字段或参数)
Primitive Obsession(不使用基本类型而使用类)
Switch Statements(同样的switch语句散布于不同地点。)
Parallel Inheritance Hierarchies(每当你为某个类增加一个子类,同时也必须为另一个类相应地增加一个子类。)
Lazy Class(冗赘类)
Speculative Generality(夸夸其谈未来性)
如果某个抽象类其实没有太大作用->Collapse Hierarchy。
不必要的委托可运用Inline Class除掉。
函数的某些参数未被用上->Remove Parameter。
函数名称带有多余的抽象意味->Rename Method,让它现实一些。
函数或类的唯一用户是测试用例,把它们连同其测试用例一并删掉。但如果它们的用途是帮助测试用例检测正当功能,则必须刀下留人。
Temporary Field(某个类内某个实例变量仅为某种特殊情况而设。)
Message Chains(用户向一个对象请求另一个对象,然后再向后者请求另一个对象,然后再请求另一个对象.... )
Middle Man(过度运用委托)
Inappropriate Intimacy(两个类过于亲密,花费太多时间去探究彼此的private成分。 )
Alternative Classes with Different Interfaces(异曲同工的类)
Incomplete Library Class(不完美的类库)
Data Class(限制属性的访问性)
Refused Bequest(被拒绝的遗赠)
子类破坏父类的接口
Comments(过多的注释)

3.1 Duplicated Code(重复代码)

同一个类的两个函数含有相同的表达式(Extract Method)
两个互为兄弟的子类内含有相同表达式:对两个类使用Extract Method,然后再对被提炼出来的代码使用Pull Up Method,将它推入超类内。

如果代码之间只是类似,并非完全相同,那么就得运用Extract Method将相似部分和差异部分分割开,构成单独一个函数。然后可能发现可以运用Form Template Method获得一个Template Method设计模式。有些函数以不同的算法做相同的事,你可以选择其中较清晰的一个,并使用Subsitute Algorithm将其他函数的算法替换掉。
如果两个毫不相关的类出现Duplicated Code,就应该考虑对其中一个使用Extract Class,将重复代码提炼到一个独立类中,然后在另一个类内使用这个新类。
但是重复代码所在的函数也可能的确只应该属于某个类,另一个类只能调用它,抑或这个函数可能属于第三个类,而另外两个类应该引用某个类。必须决定这个函数放在哪儿最合适,并确保它被安置后就不会再在其他任何地方出现。

3.2 Long Method(过长函数)

拥有短函数的对象会活得比较好、比较长。

“间接层”所能带来的全部利益——解释能力、共享能力、选择能力——都是由小型函数支持的。
程序愈长愈难理解。

早期的编程语言中,子程序调用需要额外开销,这使得人不太乐意使用小函数。

现在的OO语言进程内的函数调用开销可以忽略不计。

让小函数容易理解的真正关键在于一个好名字。

你应该更积极地分解函数。

我们遵循这样一条原则:如果感觉需要以注解来说明点什么的时候,我们就把需要说明的东西写进一个独立函数中,并以其用途(而非实现手法)命名。

我们可以对一组甚至一行代码做这件事,哪怕替换后的函数调用动作比函数自身还长,只要函数名称能够解释其用途,我们也该毫不犹豫地这么做。
关键不在于函数的长度,而在于函数“做什么”和“如何做”之间的语义距离。(方法调用告诉我们这个函数做了什么;直接把代码写在函数内,是通过代码的分析我们才能得知这些代码做了什么;所以如果可以永远都只告诉别人我做了什么,而不是我怎么一步一步做的。)

找到函数中适合集中在一起的部分,将它们提炼出来形成一个新函数。(Extract Method)

函数内有大量的参数和临时变量——>运用Extract Method,最终就会把许多参数和临时变量当做参数,传递给被提炼出来的新函数,导致可读性几乎没有任何提升。——>Replace Temp with Query来消除这些临时元素。Introduce Parameter Object和Perserve Whole Object则可以将过长的参数列变得更简洁一些。->Replace Method with Method Object。

如何确定该提炼哪一段代码呢?寻找注释。就算只有一行代码,如果它需要以注释来说明,那也值得将它提炼到独立函数去。
条件表达式和循环常常也是提炼的信号。可以使用Decompose Conditional处理条件表达式。循环:将循环和其内的代码提炼到一个独立函数中。

3.3 Large Class(过大的类)

Extract Class将几个变量一起提炼到新类内。提炼时应该选择类内彼此相关的变量,将它们放在一起。

通常如果类内的数个变量有着相同的前缀或字尾,这就意味有机会把它们提炼到某个组件内。如果这个组件适合作为一个子类->Extract Subclass。

有时候类并非在所有时刻都使用所有实例变量。->Extract Class或Extract Subclass。

一个类如果拥有太多代码->Extract Class和Extract Subclass.
先确定客户端如何使用它们,然后运用Extract Interface为每一种使用方式提炼出一个接口。可以帮助你看清楚如何分解这个类。

Large Class是个GUI类->Duplicate Observed Data

3.4 Long Parameter List(过长参数列表)

太长的参数列难以理解,太多参数会造成前后不一致、不易使用,而且一旦你需要更多数据,就不得不修改它。如果将对象传递给函数,大多数修改都将没有必要,因为只需(在函数内)增加一两条请求,就能得到更多数据。
如果向已有的对象(函数所属类内的一个字段或另一个参数)发出一条请求就可以取代一个参数->Replace Parameter with Method.

Preserve Whole Object将来自同一对象的一堆数据收集起来,并以该对象替换它们。

如果某些数据缺乏合理的对象归属->Introduce Parameter Object为它们制造出一个“参数对象”。
如果不希望造成“被调用对象”与“较大对象”间的某种依赖关系,可以将数据从对象中拆解出来单独作为参数。

3.5 Divergent Change(发散式变化)(一个类只能有一个导致它变化的理由,单一职责) 一个类受多种变化的影响

软件必须能够更容易被修改。
一旦需要修改,最好是只需要修改一个地方甚至是不需要修改,只需要添加一些类和做一些配置。

某个类经常因为不同的原因在不同的方向上发生变化。
针对某一外界变化的所有相应修改,都只应该发生在单一类中。

应该找到某特定原因而造成的所有变化,然后运用Extract Class将它们提炼到另一个类中。

3.6 Shotgun Surgery(霰弹式修改)一种变化引发多个类相应修改

如果每遇到某种变化,就必须在许多不同的类内做出许多小修改。

Move Method和Move Field把所有需要修改的代码放进一个类。如果眼下没有合适的类可以放置这些代码,就创造一个。
Inline Class把一系列相关行为放进同一个类。

“外界变化”与“需要修改的类”最好一一对应。
最根本的原则:将总是一起变化的东西放在一块儿。数据和引用这些数据的行为总是一起变化的,但也有例外。如果例外出现,我们就搬移哪些行为,保持变化只在一地发生。

3.7 Feature Envy(依恋情结)

函数对某个类的兴趣高于对自己所处类的兴趣。(数据)

使用Move Method把它移到它该去的地方。

如果函数中只有一部分有这种情况,先使用Extract Method把这一部分提炼到独立函数中,再使用Move Method。

使用到几个类的功能的函数该移动到什么位置?判断哪个类拥有最多被此函数使用的数据,然后就把这个函数和哪些数据摆在一起。可以先以Extract Method将这个函数分解为数个较小函数并分别置放于不同地点。

3.8 Data Clumps(数据泥团)

两个类有相同的三四个字段、许多函数签名中有相同的三四个参数。这些总是绑在一起出现的数据真应该拥有属于它们自己的对象。
首先找出这些数据以字段形式出现的地方,运用Extract Class将它们提炼到一个独立对象中。然后将注意力转移到函数签名上,运用Introduce Parameter Object或Preserve Whole Object为它减肥。这么做的好处是可以将很多参数列缩短,简化函数调用。
不必在意Data Clumps只用上新对象的一部分字段,只要以新对象取代两个以上的字段就可以了。
得到新对象后,可以着手寻找Feature Envy,这可以帮你获知哪些能够移至新类中的种种程序行为。

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

3.9 Primitive Obsession(基本类型偏执)

对象的一个极大的价值在于:它们模糊(甚至打破)了横亘于基本数据和体积较大的类之间的界限。可以轻松编写出一些与语言内置(基本)类型无异的小型类。

Replace Data Value with Object将原本单独存在的数据值替换为对象。

如果想要替换的数据项是类型码,而它并不影响行为,可以运用Replace Type Code with Class。
如果有与类型码相关的条件表达式,可运用Replace Type Code with Subclass或Replace Type Code with State/Startegy加以处理。

有一组总是被放在一起的字段,运用Extract Class。
在参数列表中看到基本类型数据,运用Introduce Parameter Object。
在数组中挑选数据,运用Replace Array with Object。

3.10 Switch Statements(switch惊悚现身)

面向对象程序的一个最明显特征就是:少用switch(或case)语句。从本质上说,switch语句的问题在于重复。
你常会发现同样的switch语句散布于不同地点。如果要为switch添加一个新的case字句,就必须找到所有switch语句并修改它们。

一看到switch语句,就应该考虑以多态来替换它。
switch语句常常根据类型码进行选择,你要的是“与该类型码相关的函数或类”,所以应该使用Extract Method将switch语句提炼到一个独立函数中,再以Move Method将它搬移到需要多态性的那个类里。此时你必须决定是否使用Replace Type Code with Subclasses或Replace Type Code with State/Strategy。一旦这样完成继承结构之后,你就可以运用Replace Conditional with Polymorhpism了。

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

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对象,从而避免写出条件式代码。
如果类中有一个复杂算法,需要好几个变量,往往就可能导致坏味道Temporary Field的出现。由于实现者不希望传递一长串参数,所以他把这些参数都放进字段中。但是这些字段只有在使用该算法时才有效,其他情况下只会让人迷惑。->Extract Class把这些变量和其相关函数提炼到一个独立类中。提炼后的新对象将是一个函数对象。

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

消息链:用户向一个对象请求另一个对象,然后再向后者请求另一个对象,然后再请求另一个对象....
采取这种方式,意味客户代码将与查找过程中的导航结构紧密耦合。一旦对象间的关系发生任何变化,客户端就不得不做出相应修改。
Hide Delegate->可以在消息链的不同位置进行这种重构手法。理论上可以重构消息链上的任何一个对象,但这么做往往会把一系列对象都变成Middle Man。通常更好的选择是:先观察消息链最终得到的对象是用来干什么的,看看能够以Extract Method把使用该对象的代码提炼到一个独立函数中,再运用Move Method把这个函数推入消息链。如果这条链上的某个对象有多位客户打算导航此航线的剩余部分,就加一个函数来做这件事。

3.16 Middle Man(中间人)

封装往往伴随委托。
过度运用委托:某个类接口有一半的函数都委托给其他类。
Remove Middle Man——>直接和真正负责的对象打交道。
如果这样“不干实事”的函数只有少数几个,可以运用Inline Method把它们放进调用端。
如果这些Middle Man还有其他行为->Replace Delegation with Inheritance把它变成实责对象的子类,这样既可以扩展原对象的行为,又不必负担那么多的委托动作。

3.17 Inappropriate Intimacy(狎昵关系)

两个类过于亲密,花费太多时间去探究彼此的private成分。
Move Method和Move Field帮它们划清界限,从而减少狎昵行径。
Change Bidirectional Association to Unidirectional让其中一个类对另一个类斩断关系。
如果两个类实在是情投意合,运用Extract Class把两者共同点提炼到一个安全地点,让它们使用这个新类。或运用Hide Delegate让另一个类来为它们传递关系。

继承往往造成过度亲密,因为子类对超类的了解总是超过后者的主观愿望。->Replace Inheritance with Delegation让子类离开继承体系。

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

如果两个函数做同一件事,却有着不同的签名->Rename Method根据它们的用途重新命名。->反复运用Move Method将某些行为移入类,直到两者的协议一致为止。如果你必须重复而赘余地移入代码才能完成这些,或许可以运用Extract Superclass。

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

大多数对象只要够用就好。

如果类库构造得不够好,我们不可能修改其中的类使它完成我们希望完成的工作。
只想修改类库中的类的一两个函数->Introduce Foreign Method
添加一大堆额外行为->Introduce Local Extension

3.20 Data Class(幼稚的数据类)

Data Class:拥有一些字段,以及用于访问(读写)这些字段的函数。
如果拥有public字段->Encapsulate Field
如果这些类内含容器类的字段,应该检查它们是不是得到了恰当地封装->Encapsulate Collection

对于不该被其他类修改的字段->Remove Setting Method->找出取值/设置函数被其他类运用的地点->Move Method把这些调用行为搬移到Data Class来。如果无法搬移整个函数,就运用Extract Method产生一个可被搬移的函数->Hide Method把这些取值/设置函数隐藏起来。

3.21 Refused Bequest(被拒绝的遗赠)

子类应该继承超类的函数和数据。子类继承得到所有函数和数据,却只使用了几个。
继承体系设计错误。需要为这个子类新建一个兄弟类->Push Down Method和Push Down Field把所有用不到的函数下推给兄弟类,这样一来,超类就只持有所有子类共享的东西。
所有超类都应该是抽象的。

拒绝继承超类的实现,可以不用介意;拒绝继承超类的接口,可以不以为然。但是不能胡乱修改继承体系->Replace Inheritance with Delegation.

3.22 Comments(过多的注释)

因为代码很糟糕所以写了很多注释。
注释提示我们代码有坏味道,重构完会发现注释变得多余了。
需要注释来解释一块代码做了什么->Extract Method
函数已经提炼出来,但还是需要注释解释其行为->Rename Method
需要注释说明某些系统的需求规格->Introduce Assertion

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

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,482评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,377评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,762评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,273评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,289评论 5 373
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,046评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,351评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,988评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,476评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,948评论 2 324
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,064评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,712评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,261评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,264评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,486评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,511评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,802评论 2 345

推荐阅读更多精彩内容