您的位置:首页 > 编程语言

重构代码坏味道(优雅,little code)<上>

2014-12-17 17:54 155 查看

1. Duplicated Code(重复的代码)

臭味行列中首当其冲的就是Duplicated Code。如果你在一个以上的地点看到相同的程序结构,那么当可肯定:设法将它们合而为一,程序会变得更好。

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

另一种常见情况就是[两个互为兄弟(sibling)的subclasses内含有相同表达式]。要避免这种情况,只需要对两个classes都使用Extract Method,然后再对被提炼出的代码使用Pull Up Method,将它推入superclass内。如果代码之间只是类似,并非完全相同,那么就得运用Extract Method将相似部分和差异部分割开,构成单独一个函数。然后你可能发现或许可以运用Form Template
Method获得一个Template Method设计模式。如果有些函数以不同的算法做相同的事,你可以择定其中较清晰的一个,并使用Substitute Algorithm将其它函数的算法替换掉。

如果两个毫不相关的classes内出现Duplicated Code,你应该考虑对其中一个使用Extract Class,将重复代码提炼到一个独立class中,然后在另一个class内使用这个新class。但是,重复代码所在的函数也可能的确只应该属于某个class,另一个class只能调用它,抑或这个函数可能属于第三个class,而另两个classes应该引用这第三个class。你必须决定这个函数放在哪儿最合适,并确保它被安置后就不会再在其它任何地方出现。

个人理解:

代码出现重复的情况 : 解决方案

--函数内重复 :Extract Method 提取,然后让函数调用, 就是再加一个函数呗

--子类有重复方法:对子类Extract Method。然后Pull Up Method到父类,比如父类animals子类是各种动物,都有8:00起床的方法,就可以在父类 里实现,子类继承就好了。

--类没有关系却有重复代码:Extract Class,提取到一个类中,另外一个调用。也可以根据需要把提取出来做个独立的类。其实这是不是有点像interface,friends呢?

--代码相同的提取到一个函数里,相似的呢,重载、重写、模板是不是就可以这样派上用场了?

2. Long Method(过长函数)

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

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

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

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

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

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

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

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

个人理解:

过长函数 :愈长愈难理解,函数调用开销几乎没有了。短函数虽然需要不断翻看上下文但在解释能力、共享能力、选择能力还是有优势的。

-- 关键点:起个好名字,保证一个函数只做一件事,哈哈,术业有专攻,各司其职的好。觉得函数调用费时间,inline一下嘛~万能的编译器会帮我优化的吧(汗)

--危险: 提取方法,可能面临参数变多,通过查询getXXX()接口来减少临时变量和参数,实在不行就把这些参数封装成类对象(这个也会出现问题,只能中庸的说いい加減にしよ)。

-- 分解时机:出现大量注释的时候(其实我不爱写注释,懒);出现条件式和循环的时候。

-- 一般针对某个功能实现的时候,一个函数有多个功能没那么让人嗤之以鼻,但考虑到代码的优雅度啦、是需要好好改改(当年写C++会把所有的功能那个都写到main函数里,哎)

3. Large Class(过大类)

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

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

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

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

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

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

个人理解:

过长类 :和函数过长差不多,既然长,肯定是做了自己不该干的事,里面也一定夹杂着各种变量实例,就视功能分解呗

-- 解决方法1:提取子类到外部,也可以在内部(内部类存在就是这个原因吧)

--解决方法2:没帮法提取类了,就可以试试在内部分解成函数

-- 最后一段说的什么没有看懂哎,哭哭哭

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

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

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

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

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

个人理解:

过长参数列 :用参数传递函数需要的东西是有好处,但是参数一下1000个,会砸死键盘不是

--解决方法1:用全局变量(说是很危险,危险在哪里???)

--解决方法2:传递一个对象。数据库更新时候,需要user的name,passwd,age,email等信息,就可以直接传递一个update(user u)。面向对象编程啊,这是

--解决方法3:replace parameter with method, preserve whole object,introuce
parameter object(这是什么,难道是String []args和...???)

--注意:并不是把参数对象化就一定会好,还要考虑依存关系。我只关心你名字是什么的时候,你把你身高、家谱、爱好都传递过来我还能给你做媒人么~切~

5. Divergent Change(发散式变化)

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

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

个人理解:

发散式变化 : 强调的是对症下药,头痛医头,脚痛治脚的感觉,但在code里怎么用,不理解,联想不起来

--网页里有getElementById,getElementByName就是这种重构吗?

6. Shotgun Surgery(霰弹式修改)

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

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

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

个人理解:

霰弹式修改 :牵一发而全身动弹不得的感觉呢

--解决方法: 使用move method和 move field把要修改的放到同一个class里。

-- 和发散式变化的区别:发散是指一个class受到多种变化的影响,霰弹式是一个变化依法多个class修改。

--理想状态:外界变化和待改类实现一对一。

7. Feature Envy(依恋情结)

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

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

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

个人理解:

依恋情结 :面向对象将数据和行为封装在一起(不面向对象的C怎么办?它可以用结构体把数据和(void ×)函数指针装在一起,好奇葩的coder)。

依恋情结的焦点在数据,说白了就是一个外部函数你参数一大半都是人家类里的数据是怎么回事?你直接搬家到class里不就得了嘛~(居然想到了参数过长)

--解决方法1:move method,用谁家东西最多就搬去谁家。然后extract method 看函数是不是过大

--例外,策略和观察者模式,没看明白(哭哭哭)链接在此,等我长发及腰,再来啃一遍!
http://www.cnblogs.com/java-my-life/archive/2012/06/14/2545381.html http://blog.csdn.net/chenjie19891104/article/details/6393770
--注:设计模式是硬伤,不会,不会,不会,记不住,看不明白

8. Data Clumps(数据泥团)

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

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

缩短值域个数和参数个数,当然可以支队一些坏味道,但更重要的是:一旦拥有新对象,你就有机会让程序散发出一种芳香。得到新对象后,你就可以着手寻找Feature Envy,这可以帮你指出[可移到新class]中的种种程序行为。不必太久,所有classes都将在它们的小小社会中充分发挥自己的生产力。

个人理解:

数据泥团 :那些总是绑在一起玩耍的数据,扔到对象里去

--解决方法1:extract class到独立object里。函数签名式上头,introuduce parameter object或preserve whole object减肥,这不就是缩短参数长度时要做的嘛!

--作者是有多喜欢面向对象编程啊。可是那些用C写出操作系统的大神们思路也是得有多么清晰啊,跪伏。

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

大多数编程环境都有两种数据:结构型别允许你将数据组织成有意义的形式;基本型别则是构成结构型别的积木块。结构总是会带来一定的额外开销。它们有点像数据库中的表格,或是那些得不偿失的东西。

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

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

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

个人理解:

基本型别偏执 :基本型是指int,float,double这些。这里是说不要吝啬于创建自己的数据类型

--之前看到一个表示money的类,当时感觉很罗嗦,明明用一个float表示就可以,还把单位、正负号封装成一个money类型,现在想象也是基于这种面向对象结构化思想吧。目前我还不能接受这个看法,当然必要的时候还是会封装成class

--解决方案:replace data value with object/class/subclass/state/strategy。

如果是一组,extract class;参数列中,introduce parameter object;array里就可以,replace array with object。

10. Switch Statements(switch惊悚现身)

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

大多数时候,一看到switch语句你就应该考虑以多态来替换它。问题是多态该出现在哪儿?switch语句常常根据type code进行选择,你要的是[与该type code相关的函数或class]。所以你应该使用Extract Method将switch语句提炼到一个独立函数中,再以Move Method将它搬移到需要多态性的那个class里头。此时你必须决定是否使用Replace Type Code with Subclasses或Replace
Type Code with State/Strategy。一旦这样完成继承结构之后,你就可以运用Replace Conditional with Polymorphism了。

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

个人理解:

switch : 好吧,面向对象要求少用switch、case,switch本身就是在重复?!!!也是啊,面向对象里有多态(静态、动态),我觉得jdk新特性里面的enum也很帅哎

--解决方法1:extract method把switch提到一个函数中,然后move method到多态class里,

replace type code with subclass或者replace type code with state / strategy。

--动不动就用多态感觉太夸张,函数重载也可以的(怎么判断一个int a=10的类型是int?java里有反射可是c++呢???有知道的告诉我,我找了好久了)

--introude null object这是什么?什么是null http://www.cnblogs.com/mingzi/archive/2009/01/03/1367493.html http://www.importnew.com/13002.html http://sourcemaking.com/refactoring/introduce-null-object 也是有些极端啊~

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

Parallel Inheritance Hierarchies其实是Shotgun Surgery的特殊情况。在这种情况下,每当你为某个class增加一个subclass,必须也为另一个class相应增加一个subclass。如果你发现某个继承体系的class名称前缀和另一个继承体系的class名称前缀完全相同,便是闻到了这种坏味道。

消除这种重复性的一般策略是:让一个继承体系的实体指涉另一个继承体系的实体。如果再接再厉运用Move Method和Move Field,就可以将指涉端的继承体系消弭于无形。

个人理解:

平等继承体系 : 这个是在说如果子类都有某个方法的话,就不如上移添加到父类里

-- move method move field
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: