首页 > 代码库 > 转:Bad Smell重构和设计的标准
转:Bad Smell重构和设计的标准
Bad Smell重构和设计的标准。——与其无尽的等待完美的设计,不如立刻着手实现可行的设计,然后再在设计出现臭味的时候重构实现!
食品有做得好不好之分。其中一个重要的指标就是保质期。好的食品,保质期就长,坏的食品保质期就短。对于代码来说也一样。也有一个保质能力的问题。好的代码,入口芬芳。坏的代码早早就变质了,而且随着时间的推移越来越臭,直到最后,我们只好把它扔掉,虽然它也曾经是我们花钱买来的……
比如说,我们接到一个项目,我们经过思考,获得了一个可行的设计方案,然后着手实现。此时,可能有一个小功能。我们是使用GOF23个设计模式,用n个类实现呢,还是立刻用10来行代码解决呢?我们应该选择“可到达目标的简单方案”。这是XP方法的一大原则。现在,这一段代码散发着well-smell,过了几天,我们又增加几项功能。再看到这段代码时,它已经散发出bad smell了。也许是因为,我们的代码大大增加了,一段语意含混不清的代码,让阅读者头晕。也许是因为,出现了大量代码的重复,或者是我们这一段不太“香”的代码被很多代码所调用。
当你学会用挑剔的眼光审视自己所写的文字时,会发现将一段文字反复读上五六遍,每次都会找到新的问题。重构,也就是对既有代码设计的改善,要求你首先知道什么样的代码需要改善。重构目录可以帮助你获得这样的知识,但是你的情况可能与目录中所看到的不同。所以,了解常见的设计问题非常必要,这样你才能在自己的代码中识别出这些问题。
下面列举出几种Bad Smell(代码的坏味道):
1.Duplicated Code(重复的代码)
臭味行列中首当其冲的就是Duplicated Code。如果你在一个以上的地点看到相同的程序结构,那么当可肯定:设法将它们合而为一,程序会变得更好。最单纯的Duplicated Code就是[同一个class内的两个方法含有相同表达式(expression)]。这时候你需要做的就是采用Extract Method提炼出重复的代码,然后让这两个地点都调用被提炼出来的那一段代码。另一种常见情况就是[两个互为兄弟(sibling)的subclasses内含有相同表达式]。要避免这种情况,只需要对两个classes都使用 Extract Method,然后再对被提炼出的代码使用Pull Up Method,将它推入superclass内。如果代码之间只是类似,并非完全相同,那么就得运用ExtractMethod将相似部分和差异部分割开,构成单独一个方法。然后你可能发现或许可以运用Form TemplateMethod获得一个Template Method设计模式。如果有些方法以不同的算法做相同的事,你可以择定其中较清晰的一个,并使用Substitute Algorithm将其它方法的算法替换掉。如果两个毫不相关的classes内出现Duplicated Code,你应该考虑对其中一个使用Extract Class,将重复代码提炼到一个独立class中,然后在另一个class内使用这个新class。但是,重复代码所在的方法也可能的确只应该属于某个 class,另一个class只能调用它,抑或这个方法可能属于第三个class,而另两个classes应该引用这第三个class。你必须决定这个方法放在哪儿最合适,并确保它被安置后就不会再在其它任何地方出现。
2.Long Method(过长方法)
拥有[短方法](shortmethods)的对象会活得比较好、比较长。不熟悉面向对象技术的人,常常觉得对象程序中只有无穷无尽的delegation(委托),根本没有进行任何计算。和此类程序共同生活数年之后,你才会知道,这些小小方法有多大价值。[间接层]所能带来的全部利益——解释能力、共享能力、选择能力——都是由小型方法支持的。很久以前程序员就已认识到:程序愈长愈难理解。早期的编程语言中,[子程序调用动作]需要额外开销,这使得做你们不太乐意使用small method,现代OO语言几乎已经完全免除了进程内的[方法调用动作额外开销]。不过代码阅读者还是得多费力气,因为他必须经常转换上下文去看看子程序做了什么。某些开发环境允许用户同时看到两个方法,这可以帮助你省去部分麻烦,但是让small method容易理解的真正关键在于一个好名字。如果你能给方法起个好名字,读者就可以通过名字了解方法的作用,根本不必去看其中写了些什么。最终的效果是:你应该更积极进取地分解方法。我们遵循这样一条原则:每当感觉需要以注释来说明点什么的时候,我们就把需要说明的东西写进一个独立的方法中,并以其用途(而非实现手法)命名。我们可以对一组或甚至短短一行代码做这件事。哪怕替换后的方法调用动作比方法自身还长,只要方法名称能够解释其用途,我们也该毫不犹豫地那么做。关键不在于方法的长度,而在于方法[做什么]和[如何做]之间的语义距离。百分之九十九的场合里,要把方法变小,只需使用ExtractMethod。找到方法中适合集在一起的部分,将它们提炼出来形成一个新方法。如果方法内有大量的参数和临时变量,它们会对你的方法提炼形成阻碍。如果你尝试运用Extract Method,最终就会把许多这些参数和临时变量当作参数,传递给被提炼出来的新方法,导致可读性几乎没有任何提升。啊是的,你可以经常运用 Replace Temp with Query则可以将过长的参数列变得更简洁一些。如果你已经这么做,仍然有太多临时变量和参数,那就应该拿出我们的杀手锏:Replace Method with Method Object。如何确定该提炼哪一段代码呢?一个很好的技巧是:寻找注释。它们通常是指出[代码用途和实现手法间的语义距离]的信号。如果代码前言有一行注释,就是在提醒你:可以将这段代码替换成一个方法,而且可以在注释的基础上给这个方法命名。就算只有一行代码,如果它需要以注释来说明,那也值得将它提炼到独立的方法去。条件式和循环常常也是提炼的信号。你可以使用Decompose Conditional处理条件式。至于循环,你应该将循环和其内的代码提炼到一例独立方法中。
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。这里有个有用技巧:先确定客户端如何使用它们,然后运用ExtractInterface为每一种使用一个接口。这或许可以帮助你看清楚如何分解这个class。如果你的Large Class是个GUI class,你可能需要把数据和行为移到一个独立的领域对象去。你可能需要两边各保留一些重复数据,并令这些数据同步。Duplicate Observed Data告诉你该怎么做。这种情况下,特别是如果你使用旧式AWT组件,你可以采用这种方式去掉GUI class并代以Swing组件。
4.Long Parameter List(过长参数列)
刚开始学习编程的时候,老师教我们:把方法所需的所有东西都以参数传递进去。这可以理解,因为除此之外就只能选择全局数据,而全局数据是邪恶的东西。对象技术改变了这一情况,因为如果你手上没有你所需要的东西,总可以叫另一个对象给你。因此,有了对象,你就不必把方法需要的所有东西都以参数传递给它了,你只需给它足够的东西、让方法能从中获得自己需要的所有东西就行了。方法需要的东西多半可以在方法的宿主类(host class)中找到。面向对象程序中的方法,其参数列通常比在传统程序中短得多。这是好现象,因为太长的参数列难以理解,太多参数会造成前后不一致、不易使用,而且一旦你需要更多数据,就不得不修改它。如果将对象传递给方法,大多数修改都将没有必要,因为你很可能只需(在方法内)增加一两条请求,就能得到更多数据。如果[向既有对象发出一条请求]就可以取得原本位于参数列上的一份数据,那么你应该激活重构准则Replace Parameter with Method。上述的既有对象可能是方法所属class内的一个字段,也可能是另一个参数。你还可以运用Preserve Whole Object将来自同一对象的一堆数据收集起来,并以该对象替换它们。如果某些数据缺乏合理的对象归属,可使用Introduce Parameter Object为它们制造出一个[参数对象]。此间存在一个重要的例外。有时候你明显不希望造成[被调用之对象]与[较大对象]间的某种依存关系。这时候将数据从对象中拆解出来单独作为参数,也很合情合理。但是请注意其所引发的代价。如果参数列太长或变化太频繁,你就需要重新考虑自己的依存结构了。
5.Divergent Change(发散式变化)
我们希望软件能够更容易被修改——毕竟软件再怎么说本来就该是[软]的。一旦需要修改,我们希望能够跌到系统的某一点,只在该处做修改。如果不能做到这点,你就嗅出两种紧密相关的刺鼻味道中的一种了。如果某个class经常因为不同的原因在不同的方向上发生变化,Divergent Change就出现了。当你看着一个class说:“ 呃,如果新加入一个数据库,我必须修改这三个方法;如果新出现一种金融工具,我必须修改这四个方法”,那么此时也许将这个对象分成两个会更好,这么一来每个对象就可以只因一种变化而需要修改。当然,往往只有在加入新数据库或新金融工具后,你才能发现这一点。针对某一外界变化的所有相应修改,都只应该发生在单一class中,而这个新class内的所有内容都应该反应该外界变化。为此,你应该找出因着某特定原因而造成的所有变化,然后运用Extract Class将它们提炼到另一个class中。
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相应修改]。这两种情况下你都会希望整理代码,取得[外界变化]与[待改类]呈现一对一关系的理想境地。
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使你得以轻松修改方法行为,因为它们将少量需要被覆写的行为隔离开来——当然也付出了[多一层间接性]的代价。
8.Data Clumps(数据泥团)
数据项就像小孩子:喜欢成群结队地待在一块儿。你常常可以在很多地方看到相同的三或四笔数据项:两个classes内的相同字段、许多方法签名式中的相同参数。这些[总是绑在一起出现的数据]真应该放进属于它们自己的对象中。首先请找出这些数据的字段形式出现点,运用ExtractClass将它们提炼到一个独立对象中。然后将注意力转移到方法签名式上头,运用IntroduceParameter Object或Preserve Whole Object为它减肥。这么做的直接好处是可以将很多参数列缩短,简化方法调用动作。是的,不必因为Data Clumps只用上新对象的一部分字段而在意,只要你以新对象取代两个(或更多)字段,你就值回票价了。一个好的评断办法是:删掉众多数据中的一笔。其它数据有没有因而失去意义?如果它们不再有问询,这就是个明确信号:你应该为它们产生一个新对象。缩短字段个数和参数个数,当然可以支队一些坏味道,但更重要的是:一旦拥有新对象,你就有机会让程序散发出一种芳香。得到新对象后,你就可以着手寻找 Feature Envy,这可以帮你指出[可移到新class]中的种种程序行为。不必太久,所有classes都将在它们的小小社会中充分发挥自己的生产力。
9.Primitive Obsession(基本型别偏执)
大多数编程环境都有两种数据:结构型别允许你将数据组织成有意义的形式;基本型别则是构成结构型别的积木块。结构总是会带来一定的额外开销。它们有点像数据库中的表格,或是那些得不偿失的东西。对象的一个极具价值的东西早到:它们模糊了横亘于基本数据和体积较大的classes之间的界限。你可以轻松编写出一些与语言内置型别无异的小型 classes。例如Java就以基本型别表示数值,而心class表示字符串和日期——这两个型别在其它许多编程环境中都以基本型别表现。对象技术的新手通常在小任务上运用小对象——像是结合数值和币别的money class、含一个起始值和一个结束值的range class、电话号码或邮政编码等等的特殊strings。你可以运用Replace Data Value with Object将原本单独存在的数据值替换为对象,从而走出传统的洞窟,进入炙手可热的对象世界。如果欲替换之数据值是type code,而它并不影响行为,你可以运用Replace Type Codewith Class将它换掉。如果你有相依于此type code的条件式,可运用Replace Type Code with Subclass或Replace TypeCode with State/Strategy加以处理。如果你有一组应该总是被放在一起的字段,可运用ExtractClass。如果你在参数列中看到基本型数据,不妨试试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或ReplaceType Code with State/Strategy。一旦这样完成继承结构之后,你就可以运用ReplaceConditional with Polymorphism了。如果你只是在单一方法中髭选择事例,而你并不想改动它们,那么[多态]就有点杀鸡用牛刀了。这种情况下ReplaceParameter with Explicit Methods是个不错的选择。如果你的选择条件之一是null,可以试试Introduce Null Object。
11.Parallel Inheritance Hierarchies(平等继承体系)
Parallel Inheritance Hierarchies其实是ShotgunSurgery的特殊情况。在这种情况下,每当你为某个class增加一个subclass,必须也为另一个class相应增加一个subclass。如果你发现某个继承体系的class名称前缀和另一个继承体系的class名称前缀完全相同,便是闻到了这种坏味道。消除这种重复性的一般策略是:让一个继承体系的实体指涉另一个继承体系的实体。如果再接再厉运用Move Method和Move Field,就可以将指涉端的继承体系消弭于无形。
12.Lazy Class(冗赘类)
你所创建的每一个class,都得有人去理解它、维护它,这些工作都是要花钱的。如果一个class的所得不值其身份,它就应该消失。项目中经常会出现这样的情况:某个class原本对得起自己的身份,但重檐使它身形缩水,不再做那么多工作;或开发者事前规划了某些变化,并添加一个class来就会这些变化,但变化实际上没有发生。不论上述哪一种原因,请让这个class庄严赴义吧。如果某些subclass没有做满足够工作,试试Collapse Hierarchy[合并继承]。对于几乎没用的组件,你应该以Inline Class对付它们。
13.Speculative Generality(夸夸其谈未来性)
这个令我们十分敏感的坏味道,命名者是Brian Foote。当有人说“噢,我想我们总有一天需要做这事”并因而企图以各式各样的挂勾和特殊情况来处理一些非必要的事情,这种坏味道就出现了。那么做的结果往往造成系统更难理解和维护。如果所有装置都会被用到,那就值得那么做;如果用不到,就不值得。用不上的装置只会挡你的路,所以,把它搬弄吧。如果你的某个abstract class其实没有太大作用,请运用Collapse Hierarchy。非必要之delegation可运用Inline Class除掉。如果方法的某些参数示被用上,可对它实施Rename Method让它现实一些。如果方法或class的惟一用户是test cases,这就飘出了坏味道Speculative Generality。如果你发现这样的方法或class,请把它们连同其test cases都删掉。但如果它们的用途是帮助test cases检测正当功能,当然必须刀下留人。
14.Temporary Field(令人迷惑的暂时字段)
有时你会看到这样的对象:其内某个instance 变量仅为某种特定情势而设。这样的代码让人不易理解,因为你通常认为对象在所有时候都需要它的所有变量。在变量未被使用的情况下猜测当初其设置目的,会让你发疯。请使用Extract Class给这个可怜的孤独创造一个家,然后把所有和这个变量相关的代码都放进这个新家。也许你还可以使用Introduce Null Object在[变量不合法]的情况下创建一个Null对象,从而避免写出[条件式代码]。如果class中有一个复杂算法,需要好几个变量,往往就可能导致坏味道Temporary Field的出现。由于实现者不希望传递一长串参数,所以他把这些参数都放进字段中。但是这些字段只在使用该算法时才有效,其它情况下只会让人迷惑。这时候你可以利用Extract Class把这些变量和其相关方法提炼到一个独立class中。提炼后的新对象将是一个method object。
15.Message Chains(过度耦合的消息链)
如果你看到用户向一个对象索求另一个对象,然后再向后者索求另一个对象,然后再索求另一个对象……这就是Message Chain。实际代码中你看到的可能是一长串getThis()或一长串临时变量。采取这种方式,意味客户将与查找过程中的航行结构紧密耦合。一旦对象间的关系发生任何变化,客户端就不得不做出相应修改。这时候你应该使用Hide Delegate。你可以在Message Chain的不同位置进行这种重构手法。理论上你可以重构Message Chain上的任何一个对象,但这么做往往会把所有中介对象都变成MiddleMan。通常更好的选择是:先观察Message Chain最终得到的对象是用来干什么的,看看能否以Extract Method把使用该对象的代码提炼到一个独立方法中,再运用MoveMethod把这个方法推入Message Chain。如果这条链上的某个对象有多位客户打算航行此航线的剩余部分,就加一个方法来做这件事。有些人把任何方法链都视为坏东西,我们不这样想。呵呵,我们的总代表镇定是出了名的,起码在这件事情上是这样。
16.Middle Man(中间转手人)
对象的基本特征之一就是封装——对外部世界隐藏其内部细节。封装往往伴随delegation。比如说你问主管是否有时间参加一个会议,他就把这个消息委托给他的记事簿,然后才能回答你。很好,你没必要知道这位主管到底使用传统记事簿或电子记事簿抑或秘书来记录自己的约会。但是人们可能过度运用delegation。你也许会看到某个class接口有一半的方法都委托给其它class,这样就是过度运用。这里你应该使用 Remove Middle Man,直接和负责对象打交道。如果这样[不干实事]的方法只有少数几个,可以运用InlineMethod把它们”inlining”,放进调用端。如果这些MiddleMan还有其它行为内销可以运用Replace Delegation with Inheritance把它变成负责对象的subclass,这样你既可以扩展原对象的行为,又不必负担那么多的委托动作。
17.Inappropriate Intimacy(狎昵关系)
有时候你会看到两个classes过于亲密,花费太多时间去探究彼此的private成分。如果这发生在两个[人]之间,我们不必做卫道之士;但对于 classes,我们希望它们严守清规。就像古代恋人一样,过份狎昵的classes必须拆散。你可以采用Move Method和Move Field帮它们划清界线,从而减少狎昵行径。你也可以看看是否运用Change Bidirectional Association to Unidirectional[将双向关联改为单向]让其中一个class对另一个斩断情丝。如果两个classes实在情投意合,可以运用Extract Class把两者共同点提炼到一个安全地点,让它们坦荡地使用这个新class。或者也可以尝试运用Hide Delegate让另一个class来为它们传递相思情。继承往往造成过度亲密,因为subclass对superclass的了解总是超过superclass的主观愿望。如果你觉得该让这个孩子独自生活了,请运用Replace Inheritance with Delegation让它离开继承体系。
18.Alternative Classes with Different Interfaces(异曲同工的类)
如果两个方法做同一件事,却有着不同的签名式,请运用RenameMethod根据它们的用途重新命名。但这往往不够,请反复运用Move Method将某些行为移入classes,直到两者的协议一致为止。如果你必须重复而赘余地移入代码才能完成这些,或许可运用Extract Superclass为自己赎点罪。
19.Incomplete Library Class(不完美的程序库类)
复用常被视为对象的终极目的。我们认为这实在是过度估计了。但是无可否认,许多编程技术都建立在library classes的基础上,没人敢说是不是我们都把排序算法忘得一干二净了。Libraryclasses构筑者没有未卜先知的能力,我们不能因此责怪他们。毕竟我们自己也几乎总是在系统快要构筑完成的时候才能弄清楚它的设计,所以 library构筑者的任务真的很艰巨。麻烦的是library的形式往往不够好,往往不可能让我们修改其中的classes使它完成我们希望完成的工作。这是否意味那些经过实践检验的战术如MoveMethod等等,如今都派不上用场了?幸好我们有两个专门就会这种情况的工具。如果你只想修改libraryclasses内的一两个方法,可以运用Introduce Foreign Method;如果想要添加一大堆额外行为,就得运用Introduce Local Extension。
20.Data Class(纯稚的数据类)
所谓Data Class是指:它们拥有一些字段,以及用于访问这些字段的方法,除此之外一无长物。这样的classes只是一种[不会说话的数据容器],它们几乎一定被其它classes过份细琐地操控着。这些classes早期可能拥有public字段,果真如此你应该在别人注意到它们之前,立刻运用 Encapsulate Field将它们封装起来。如果这些classes内含容器类的字段,你应该检查它们是不是得到了恰当的封装;如果没有,就运用Encapsulate Collection把它们封装起来。对于那些不该被其它classes修改的字段,请运用Remove Setting Method。然后,找出这些[取值/设值]方法被其它classes运用的地点。尝试以Move Method把那些调用行为搬移到Data Class来。如果无法搬移整个方法,就运用Extract Method产生一个可被搬移的方法。不久之后你就可以运用HideMethod把这些[取值/设值]方法隐藏起来了。Data Class就像小孩子。作为一个起点很好,但若要让它们像[成年]的对象那样参与整个系统的工作,它们就必须承担一定责任。
21.Refused Bequest(被拒绝的遗赠)
Subclasses应该继承superclass的方法和数据。但如果它们不想或不需要继承,又该怎么办呢?它们得到所有礼物,却只从中挑选几样来玩!按传统说法,这就意味继承体系设计错误。你需要为这个subclass新建一个兄弟,再运用Push Down Method和Push Down Field把所有用不到的方法下推给那兄弟。这样一来superclass就只持有所有subclasses共享的东西。常常你会听到这样的建议:所有 superclasses都应该是抽象的。既然使用[传统说法]这个略带贬义的词,你就可以猜到,我们不建议你这么做,起码不建议你每次都这么做。我们经常利用subclassing手法来复用一些行为,并发现这可以很好地应用于日常工作。这也是一种坏味道,我们不否认,但气味通常并不强烈。所以我们说:如果Refused Bequest引起困惑和问题,请遵循传统忠告。但不必认为你每次都得那么做。十有八九这种坏味道很淡,不值得理睬。如果subclass复用了superclass的行为(实现),却又不愿意支持superclass的接口,Refused Bequest的坏味道就会变得浓烈。拒绝继承superclass的实现,这一点我们不介意;但如果拒绝继承superclass的接口,我们不以为然。不过即使你不愿意继承接口,也不要胡乱修改继承系,你应该运用Replace Inheritance with Delegation来达到目的。
22.Comments(过多的注释)
别担心,我们并不是说你不该写注释。从嗅觉上说,Comments不是一种坏味道;事实上它们还是一种香味呢。我们之所以要在这里提到Comments,因为人们常把它当作除臭剂来使用。常常会有这样的情况:你看到一段代码有着长长的注释,然后发现,这些注释之所以存在乃是因为代码很糟糕。这种情况的发生次数之多,实在令人吃惊。Comments可以带我们找到本章先前提到的各种坏味道。找到坏味道后,我们首先应该以各种重构手法把坏味道去除。完成之后我们常常会发现:注释已经变得多余了,因为代码已经清楚说明了一切。如果你需要注释来解释一块代码做了什么,试试Extract Method;如果你需要注释说明某些系统的需求规格,试试IntroduceAssertion。如果你不知道该做什么,这才是注释的良好运用时机。除了用来记述将来的打算之外,注释还可以用来标记你并无十足把握的区域。你可以在注释里写下自己[为什么做某某事]。这类信息可以帮助将来的修改者,尤其是那些健忘的家伙。
Bad Smells & Refactoring
1 题记
Any fool can write code that a computer can understand. Good programmers writecode that humans can understand.——Martin Fowler
(任何一个傻瓜都能写出计算机可以理解的代码。唯有写出人类容易理解的代码,才是优秀的程序员。)
2 Bad Smells
2.1 Duplicated Code重复的代码
(重复代码,不需要定义,大家都知道是什么东东。)
1. “重复的代码”有什么不好?(比方说,从可维护性的角度看,很多地方的代码看似一样,又有可能有细微差别,对读代码的人容易产生困扰,于是可以定罪,可读性不好;同样的修改可能需要修改多次,而且容易遗漏,可修改性不好。)(但是,重复代码也有好处啊,代码行数多,千行代码故障率会少,对我们的考核比较有利……开玩笑的是么?)
2. 代码重复到什么程度,Smell才算Bad?(if (filename == null || filename.trim().equals(""))这句话可作为例子,我们认为这句话就算是重复代码了,比如哪天 filename.trim().equals("")这个判断我觉得不好,想要换成filename.trim().length() == 0,那么岂不是霰弹式修改?使用绝技“Ctrl-C/ Ctrl-V”、同时还有用“Ctrl-F”,因为大家没有住在一起,要搜索一下才联系得到……)
3. 三种情况的重复代码:
a) 同一个class中的两个method含有相同表达式。(Extract Method)
b) 两个互为兄弟的subclass内含有相同表达式。(ExtractMethod、Pull up Method、FormTemplate Method)
c) 两个毫不相关的class内含有相同表达式。(Extract Method、 Extract Class)
2.2 Long Method过长函数
2.2.1 “过长函数”有什么不好?
1. 可读性:使用短函数,高层函数看起来像系列注释、低层函数不超过(比方说)10行,逻辑一目了然,可读性更优。
2.可重用性:(长函数可能包含逻辑A/B/C,想单独重用A逻辑,不可能。一个类比的例子,发动机的重用机会肯定比车大。大家知道深圳BYD使用的是三菱发动机,标致307/206 1.6系列使用的发动机跟富康、爱丽舍16V系列是相同的。发动机上面的螺丝重用机会更大。很多不同种类发动机上使用的螺丝很可能是同一个厂家生产的相同螺丝。)
3. 可插入性:使用短函数,利用Override等操作,替换处理逻辑会更加容易。比如,更容易应用模板方法模式Template Method,参见Form Template Method章节。
2.2.2 “短函数”难道没有缺点?
1. 调用开销?(现代OO语言几乎已经完全免除了进程内的“函数调用动作的额外开销”。)
2. 看代码的时候,跳来跳去,很心烦?(高层函数看起来像系列注释,而且函数名字起得好,单纯看代码,比如接手模块的时候,低层的函数甚至可以不看。)
2.2.3 函数到了多长,Smell才算Bad?(写多长的函数才比较合适?)
1. 李维说:5行。(李维大家认识么?台湾IT界著名散文家,与侯捷齐名。不得不承认,此处我断章取义了,他说“5行”,是有一定语境的,他是想说“很短”的意思,建议大家不要追究他说的是5还是6。)
2. Martin Fowler说:长度不是问题,关键在于函数名称和函数本体之间的语义距离。(Martin Fowler大家认识么?《重构》《UML精粹》《分析模式》《企业应用架构模式》等经典著作的作者。后面,Martin还是,如果提炼动作可以强化代码的清晰度,那就去做,就算函数名称比提炼出来的代码还长也无所谓。什么叫做“函数名称与函数本体之间的语义距离”?函数名称要能概括函数体的动作,我引用的这句话没有能表达Martin的所有意思。)
3.王振宇说:应该很短、可以较长,只要函数是在做一件从语义上无法再次拆解的事。(王振宇大家认识么?注意到没有,我们认为这句话是对Martin上一句话的补充,在函数体的组织方式上做了要求。简单说就是,一个函数做一件事儿。Extract Method中的那个例子。)
2.3 Large Class(过大类)
(一个例子)
1. “过大类”有什么不好?(难维护、容易出现重复代码。)
2. “过大类”的常见情况:
a) 本应该是针对不同类的操作放到一个类中。(比如,某些类本应拆出一些小的零件类,该类与零件类之间可能是关联、依赖关系,但没有这么做,而是所有代码放在一个类中。)
b) 大量静态方法放在“*Comm”类中。
i. 现象:大量静态方法放在“Comm”类中。很像C的函数库。(这个现象常见么?好不好?)
ii. 评述:这是一种使用面向对象语言编写面向过程代码的尝试。我个人觉得这类尝试是一种倒退,拒绝面向对象所带来的所有特性。没有很好地封装,程序的结构化不好;使用方与之的依赖是静态的;没有可插入性。
iii. 措施:解决这个问题的做法是,按照操作实施在哪个对象身上来把操作规划到对象所在的类里面;如果保持静态方法不变,也不要所有方法放到一个类中,最好按照语义来划分到合适的类。JDK类库提供了那么多方法,很少出现静态函数库的现象。当然,也不是说不存在,比如Math、ArrayList等类,不过至少他们在语义上分得很清晰。一个例子,比如,String的substring方法,很可能被一些程序员设计成public static,因为他们可能觉得无法把这个方法归属到哪个类中,于是放到“Comm”类中。(大家体会一下?是不是这么个事儿。)
iv. 疑问:那不是想调用某个方法的时候就要new一个实例(性能问题!)?首先,这又是面向过程的思维方式,想的是过程、调用,而不是对象、依赖、关联。其次,轻量级的对象,创建、回收成本很低。我曾经对一些不同算法、策略从性能角度做过相应的比较试验,通常,执行次数在10w-100w以上量级时,才有差别。我使用面向对象的做法,可以明显地获得更优的可维护性(可读、可扩展、可改、可插入、可重用),而且面向对象本身不会造成什么性能问题。当然,具体情况要具体分析,如有明显的性能隐患,最好能够做一个简单的试验,用数据来说话。做个类比,说某些政客在很多场合的潜台词中认为,民主、自由会破坏安定的大好局面,显然不能够让人信服;同样,说面向对象增加了对象的创建、销毁成本,会影响性能,影响软件系统的稳定局面,也是不能够让人信服的。于是在编程时尽可能地使用静态方法,这种做法,不可取。
3. 类长到多大才算“Large”?类,应该较小、可以较大,只要该类从语义上无法再次拆解。(“发动机”类,可以包含对“螺丝”类的引用(关联),但不要把“螺丝”类的操作也放到“发动机”类中来实现。)
2.4 Long Parameter List(过长参数列)
1. “过长参数列”有什么不好?(难读、难用、难记,还有一点,无法获得对象所带来的优势。比如,参数之间的约束关系没有得到很好地封装。例如,startTime/endTime,他俩作为参数来讲,可能不算长,这里仅做示例来说事儿。这对表示时间范围的参数可能多处使用,在没有包装成对象的时候,如果要保证“startTime < endTime”这个约束,就需要所有用到这对参数的地方都做判断;包装成对象,情况就好多了,比如叫做TimeRange,在类的构造函数中可以做这个判断。显然,使用对象,把变化封装得好一些。)
2. 想一下,JAVA类库的函数,比起C类库的函数,传递的参数是不是大都短很多?(应该是。这体现了面向对象的优势。)
3. “参数太多”这个Smell如何去除?
a) Introduce Parameter Object,无非是把多个参数封装成一个对象。
2.5 Divergent Change(发散式变化)
1. 什么是“发散式变化”?
a) “某一个类受到多种变化的影响”,A/B/C/D……多种功能变化的时候它都需要修改。
2. 为什么会造成“发散式变化”?哪儿没弄好?
a) 大致是由于这个类担负了多项任务,太操心了,不该他做的事儿也来做,越俎代庖。很可能需要再拆分几个类出来,把变化封装得更细。
3.历史教训(反面教材)(以前我写配置MAF端代码的时候,写过一个P_Unit类,他处理所有BSC单元的逻辑,但各种单板的逻辑是不一样的,于是DTB 改逻辑的时候要修改P_Unit、ABPM改的时候要修改P_Unit、IPCF、UPCF、GCM……所有具有特殊逻辑的单板修改功能的时候,他都要修改,甚至HDLC/UID等逻辑修改的时候P_Unit都要改。显然该类管得太多了。后来,我看了一本书,翻然悔悟,痛下把代码决心做了重构。其实早在 03年,徐峰(据说徐峰要离开公司,这么牛的人离开了对我们整个OMC损失很大,我在这里提一下他的名字,简陋地送别一下。)做配置CAF的时候建议我针对每种有特殊逻辑的板子弄一个类,我完全不以为然。显然,当时没有理解“封装变化”这四个字。)
2.6 Shotgun Surgery(霰弹式修改)
1. 什么是“霰弹式修改”?
a) “一个变化引发多个类的修改”,完成某个需求的时候,A/B/C/D……多个类都需要修改。
2. 为什么会造成“霰弹式修改”?哪儿没弄好?
a) 大致是多个类之间的耦合太严重。很可能是类没有规划好,没有把变化封装得足够令人满意。
3. 一个插曲:记得此前讨论这个Bad Smell的时候,严钧认为,去掉这个Bad Smell不好强求,而且举出Abstract Factory模式作为例证。也有道理。我在这一点上是这么认为的:我们要清楚的认识到我们努力的方向,Abstract Factory模式同样不完美,它没有满足Open-Close原则。我们可以在某些条件(包括技术条件)受限的时候写出不完美的代码,但一定要知道它是不完美的。
a) Factory Method模式(工厂方法)代替Abstract Factory来说事儿。
每增加一种Produce的实现类,就要同时增加一个对应该类的Creator类。当时严钧可能说的是Abstract Factory模式,我用Factory Method模式来说事儿,因为他简单些,但同样可以说明问题。
b) Open-Close原则
软件实体应该对扩展开放,对修改关闭。Open-Close原则是一个愿景性质的原则,如果系统能够达到Open-Close原则描述的情形就比较理想了,对扩展开放、对修改关闭,即,不修改原有代码即可完成对系统的扩展。系统可以获得最大可能的稳定性,加功能的时候旧有代码不修改,当然不会带入 BUG。
? 玉帝招安美猴王的故事
齐天大圣美猴王,想当初可是功夫了得,从东海龙王那儿拿了根棍儿,大闹天宫……叫嚣得不行(后来怎么不灵了,一根灯草、一根头绳、一条看大门的狮子狗都整不过),喊出来一些革命口号:“皇帝轮流做,明年到我家”,“只教他搬出去,将天宫让与我!”。有一些农民起义领袖的风范。
太白金星给玉皇大帝打了个报告出主意:“把他宣来上界……与他籍名在箓……一则不动众劳师,二则收仙有道也”。
玉皇大帝遵循Open-Close原则招安了美猴王。不动众劳师,不破坏天规,是关闭对已有系统的修改,不修改,是Closed。收仙有道,是对已有系统的扩展,可扩展,是Open。
同时应用了依赖倒换原则,合成/聚合复用原则,以后有机会给大家讲讲面向对象的设计原则。
4.讲回霰弹式修改这个smell,很多程序在接手时,前辈一再嘱咐,改什么功能的时候,一定要注意,什么什么……一堆地方必须同时修改,要细心不要漏了…… 这很可能是设计水平的问题给维护造成的难度。其实如果程序设计得好,此后的工作将愉快很多。(插播广告)我记得,刚看到斯诺克比赛在电视上转播的时候(那时候还小,初中吧,还没见过真的斯诺克台球桌),很不屑,觉得他们大部分时候在打一些比较近距离的球,最多半张台子距离吧,我甚至盲目自信,感觉那些球我都能打进,电视上的人有什么了不起。其实大家都知道,母球走位是难度更大、更重要的工作(要经过全盘性思考的),走位走好了,下一杆就好打;就好比做软件,程序结构设计得好,维护就更容易。
2.7 Data Clumps(数据泥团)
1. 什么是“数据泥团”?
a) 某些数据项经常黏在一起行动,称之为“数据泥团”。时间长了就应该考虑是不是该把他们封装到一个对象中,来封装这个泥团所可能具有的逻辑。(比如一堆表示定位信息的字段,system、subsystem、unit、rack、shelf、slot……总是一起出现。)(这个Bad Smell在在很多时候与Long Parameter List,是一样的,但Data Clumps的涵盖范围比Long Parameter List要大一些,比如,某些类的Field,可能没有当作参数来传递,但是总是黏在一起,也可能出现数据之间的逻辑,于是也需要绑成一个对象,来做封装。)
2. 如何判断是否属于“数据泥团”?
a) 删掉这些数据项中的其中之一项,其他数据有没有因此失去意义?(比如startTime/ endTime,就是成对表示时间范围的,去掉其中一个,另一个失去意义。)
2.8 Switch Statements(Switch惊悚现身)
(惊悚现身,很像香港翻译好莱坞电影片名的风格是么?)
1. Switch语句有什么不好?
a) 容易形成“长函数”(比较容易理解)
b) 容易形成“霰弹式修改”
2. 如何替换掉Switch语句?
a) 多态(使用Pet的例子的第三个版本来说明)
3. 是不是使用多态可以去掉所有Switch语句?
a) 不是。比如,根据消息号,分发把消息分发到相应的处理函数(处理类)来处理。(原因是某些情况下,调用端无法动态创建确切(子类的)实例,于是依然需要分发过程。即,需要Switch语句分发、或者“配置文件+反射”的方式分发。)
4. 对Switch语句有什么要求?
a) Switch语句可以存在,但每个case的处理语句不应超过2-3行。
2.9 Comments(过多的注释)
(首先,注释本身没有错,很多时候注释是必须存在的。但,注释过多,就是坏味道了。)
1. Why?为什么?
a) 过多的注释,是降低代码可读性的帮凶。(如果,代码只有通过大量注释才能被理解,那么说明代码的可读性不好。事实上,很多文章也就此有些说法:代码要写得 “自解释能力强”、自己解释自己;代码就是文档。这就要求,类、方法的编写要清爽,类名、方法名、变量名要起得好。)
2. How?如何写好注释?(Why?How?想起那个关于两个渔夫和一个美人鱼的荤笑话,由于有未成年人士在场,我不便当众详细讲。)
a) 写“why”。(注释应该写代码的编写思路,特别是某些地方没有按常理出牌,要写注释来说明。比如,对数组做for循环遍历,边界一般是数组的length,如果某一次出于某种特殊考虑,没这么做,就需要注释说明。)
b) 不写“what”。(注释不要写代码是干什么的,“what”这样的信息应该尽量包含在类名、方法名、变量名中。)
c) 不写充数注释。(不要为了写注释而写注释,不要往猪肉里注水,虽然没什么大碍,但终归是没品味的做法。比如,“Stringa = null;//创建一个String实例。”看到这样的注释,我胃口都不舒服。虽然部门有注释比例的要求,但像我们这样的高级程序员、高级工程师,还是不要充数用的注释。)
3 Refactoring
3.1 Extract Method
void printOwing() {
printBanner();
//print details
System.out.println ("name:" + _name);
System.out.println ("amount" + getOutstanding());
}
重构为:
void printOwing() {
printBanner();
printDetails(getOutstanding());
}
void printDetails (double outstanding) {
System.out.println ("name:" + _name);
System.out.println ("amount" + outstanding);
}
(前面说了,函数应该短。重复一下带来的好处:可读性好,高层函数像注释、低层函数行数少;可重用性好,比如上例中的printDetails,可能别处也能用,重构前是无法被重用的;可插入性好,子类可能写一个新的printDetails,使用不同格式打印。)
3.1.1 抽取函数时候,参数、临时变量如何处理
1. Replace Temp With Query 去掉临时局部变量,代之以查询类的方法,拆开的小函数需要此临时变量的时候,就调用这个查询方法。
2. Introduce Parameter Object 让长参数列变得简洁。
3. Replace Method with Method Object 去掉多个参数、局部变量。为待重构的大函数创建一个对象,这样,所有方法内的临时变量就变成对象的field,于是大函数拆开的所有小函数就共享这些field,不必再使用参数传递。
3.1.2 起名字很重要!名字应该:(此处的名字包括函数、类等)
1. 清晰、恰当。表达的信息涵盖函数的所作所为。
a) 当一个函数名字为了涵盖函数所为“必须”起成“do1stThingAndDo2ndThing”的时候,就有必要实施ExtractMethod来抽取函数了。
b) 一个OMC代码中的例子,某函数叫做checkParameter,但函数体中除了检查参数之外,还“顺便”为几个类属性赋值,虽然此函数很短、很超值,但我们认为他的命名是不恰当的,甚至他的函数设计也是不恰当的,一个函数要干单纯的一件事儿,函数内部从语义上无法再次分解。
2. 尽量简短、可以较长。但应该首先满足上一条要求。
a) compareToIgnoreCase(String类的方法)、getDisplayLanguage(Locale类的方法)、 getTotalFrequentRenterPoints(《重构》书中的示例代码),这些函数名长不长?(重要的是把信息表述清楚,名字长一点没关系。)
3.2 Replace Temp with Query
double basePrice = _quantity * _itemPrice;
if (basePrice > 1000)
return basePrice * 0.95;
else
return basePrice * 0.98;
重构为:
if (basePrice() > 1000)
return basePrice() * 0.95;
else
return basePrice() * 0.98;
…
double basePrice() {
return _quantity * _itemPrice;
}
(例子很容易理解,basePrice是临时变量,临时变量的问题在于:它们是暂时的,而且只能在所属函数内使用。由于临时变量只有在所属函数内才可见,所以它们会驱使你写出更长的函数,因为只有这样你才能访问到想要访问的临时变量。如果把临时变量替换为一个查询式(query method),那么同一个class中的所有函数都将可以获得这份信息。为拆解大函数提供了方便。)
3.2.1 一个借助Replace Temp with Query来提炼函数的例子
double getPrice() {
int basePrice = _quantity * _itemPrice;
double discountFactor;
If (basePrice >1000) discountFactor = 0.95;
else basePrice = 0.98;
return basePrice * discountFactor;
}
重构为:
double getPrice() {
return basePrice() * discountFactor();
}
private int basePrice() {
return _quantity * _itemPrice;
}
Private double discountFactor() {
If (basePrice() >1000) return 0.95;
else return 0.98;
}
(重构前,在getPrice方法中,先计算基础价格、再计算折扣因子、再计算最终价格,做了语义上可以再次拆解的三件事儿,不符合“函数只做一件事儿” 的要求,于是使用ExtractMethod方法来重构。借助Replace Temp with Query重构方法,将临时变量basePrice、discountFactor用相应的查询函数来替代。
需要指出的是,此次重构,查询函数basePrice被调用了两次,损失的一点点性能可以忽略,我们认为这样做是值得的。)
3.3 Split Temporary Variable
double temp = 2 * (_height + _width);
System.out.println (temp);
temp = _height * _width;
System.out.println (temp);
重构为:
final double perimeter = 2 * (_height + _width);
System.out.println (perimeter);
final double area = _height * _width;
System.out.println (area);
(如果临时变量被赋值超过一次就意味它们在函数中承担了一个以上的责任(循环变量等用途除外)。
例子中,临时变量temp开始被用来记录矩形周长,后来被用来记录矩形面积。应该拆解为多个临时变量,否则:
1. 影响代码可读性。(多用途临时变量通常无法获得合适的命名)
2. 增加代码出错机会。(程序某处,可能都记不清这个临时变量现在是记录什么数值的)
实际操作中,推荐使用final来限定临时变量被赋值次数。)
3.4 Remove Assignments to Parameters
int discount (int inputVal, int quantity, int yearToDate) {
if (inputVal > 50) inputVal -= 2;
重构为:
int discount (int inputVal, int quantity, int yearToDate) {
int result = inputVal;
if (inputVal > 50) result -= 2;
1. 不要对参数赋值
void nextDate(Date arg) {
arg.setDate(arg.getDate() + 1);
}
void nextDate(Date arg) {
arg = new Date(arg.getYear(), arg.getMonth(), arg.getDate()+ 1);
}
上面两个函数的写法,哪个是对参数赋值了的?哪个会起到应有的作用?
2. Java是pass by value(传值)的
a) 传进函数体中的参数,是调用语句那个传入参数在内存中的一份拷贝
b) 函数体内对参数的再赋值不会影响调用方的参数原始值(比如,修改int等基本类型参数的数值、修改Object等对象引用的指向)
c) Java中,对参数的再次赋值是一种纯粹降低程序清晰程度的做法
3.5 Replace Method with Method Object
class Order...
double price() {
double primaryBasePrice;
double secondaryBasePrice;
double tertiaryBasePrice;
// long computation;
...
}
…
重构为:
(一个大的函数,提炼出一个专门实现这个函数功能的类。
比如getMoney方法可能就是提炼出一个MoneyGetter类。此处是price方法提炼出PriceCalculator类。
这样做的理由是,price可能是个很大的函数,我们为了获得短函数的优势(前面说过的,可重用、可读、可插入等),想利用Extract Method抽取出多个短函数,但每个短函数可能都需要primaryBasePrice、secondaryBasePrice、 tertiaryBasePrice等几个临时变量,把它们都作为参数传递显然太笨了。而把这个大函数提炼成类,这些临时变量就变成了类的Field,在类中是共享的,这样,抽取出来的小函数之间就可以不需要传递参数,就很容易实现ExtractMethod这个重构过程。)
3.5.1 如此这般之后,类是不是太多了?
面向对象的套路,玩的就是类。函数不嫌多,为什么嫌类多?多个风马牛不相及的函数杂居在一个类中(能够容忍么?),为什么不多弄几个类把它们各自封装?类,对应了现实世界的有机无机物种,把物种分得足够细致,世界才得到了完美地描述。
3.6 Replace Array with Object
String[] person = new String[3];
person[0] = “Robert De Niro";
person[1] = “60";
person[2] = “Actor”;
重构为:
Person robert = new Person();
robert.setName(" Robert De Niro");
robert.setAge(“60");
robert.setProfession(“Actor”);
1. 数组应该容纳一组相似的对象,用户很难记住“数组第一个元素是人名、第二个元素是年龄”这样的约定。
a) 用注释来保证这种约定么?
b) 使用数组是出于效率考虑么?(抬杠?)
2. 同理,能够恰当地利用既有数据结构把接口约束得紧一些(更贴切、更严丝合缝),对大家都有好处。
a) 比如,能够确定是一组Person类实例,就要用Person[]来装,而不用Set、Map这样的“广口”容器。(拒绝“私下、口头约定,注释”等靠不住的协议方式所带来的弊端。)
3.7 Encapsulate Field
public String _name;
重构为:
private String _name;
public String getName() {
return _name;
}
public void setName(String arg) {
_name = arg;
}
强调封装:
一般来讲,任何时候都不要将类field声明为public(常量除外)。数据和使用数据的行为被集中在一起,一旦情况发生变化,代码的修改比较容易,因为需要修改的代码都集中在同一块地方,而不是星罗棋布地散落在整个程序中。
3.8 Replace Magic Number with Symbolic Constant
double potentialEnergy(double mass, double height) {
return mass * 9.81 * height;
}
重构为:
double potentialEnergy(double mass, double height) {
return mass * GRAVITATIONAL_CONSTANT * height;
}
static final double GRAVITATIONAL_CONSTANT = 9.81;
宏值定义代替散落在代码中的“魔术数”,没什么好说的。
3.9 Encapsulate Collection
重构为:
依然是强调封装:
1.类内高内聚:数据和对数据的操作紧密结合在一起,对数据的实施操作比较容易。(比如,餐厅管理系统中的某个类,dishes(Vector类型)作为类的 field,装载点菜时被点中的菜目,如果想统计一下哪些菜受欢迎,对于按Encapsulate Collection设计的类就比较容易操作,add方法中做一些手脚即可(分类累加)。)
2. 类之间松耦合:内部数据结构不要暴露的外界,外界也不需要关心。(这样,即便你把内部数据由array换成Vector,外部都不需要知道。)
3.10 Replace Type Code with Class
重构为:
3.10.1 Why?Type Code有什么不好?
1. Type Code会降低可读性。
a) 在定义的地方可能看不出来(定义时使用宏值,可读性挺好),但在使用的地方就会显现问题。上例中,比如有个方法getCharacter获得血型对应的性格描述,参数是血型,使用Type Code时,参数类型为int,重构后,参数类型为BloodGroup,显然后者的可读性好。
2. 使用Type Code失去了使用对象所拥有的独立扩展的机会。
a) 像这个例子,Type Code很容易有它自己的行为,比如根据血型得到性格描述、得到ABO溶血症可能性、判断血型之间的输血匹配可能……于是将其抽取成类是比较好的做法。(还是封装!)
3.10.2 插科打诨,Meilir Page-Jones讲的故事
(故事是讲面向对象的,面向对象的主要特征有哪些?封装、继承、多态)
Meilir Page-Jones在《UML面向对象设计基础》(个人认为此书堪称经典)一书中编了一个故事:
软件界在“面向对象”的定义上,一度很难达成一致。我开始步入面向对象领域时,决定澄清一下“面向对象”的定义。
我把数十位面向对象的老前辈关在一个没有食物和水的房间里。我告诉他们只有当他们的定义达成一致的意见,并且可以在软件世界发布时才允许他们出去。在一小时的喧哗过后,房内一片安静,老前辈们背靠背谁也不理谁了,陷入了僵局。此时,蹦出来一位组织者,让每个人都列出他们认为在面向对象世界中不可缺少的特性,大家同意。一通罗列,每个人都列出了三个五个、十个八个。
此时,刚才蹦出来那位组织者又蹦出来开始讲话,说,现在我们大致有两种做法:一种是建立一个长列表,该列表是每个人列表的并集;另一种是建立一个短列表,该列表是每个人列表的交集。大家选择了后者,产生了一个短列表,该列表中的特性在每个人列表中都有。这个列表确实很短,短到只有一个词,“封装”。
一堆废话告诉大家一个道理,封装,是面向对象最为重要的特性,封装好了,才能做到所谓的高内聚、松耦合。获得面向对象思想许诺的种种优势。
3.11 Replace Type Code with Subclass
(跟前面宠物店的例子是不是很像?)
重构为:
3.12 Replace Type Code with State/Strategy
重构为:
这个就厉害了!清晰地展示了“合成/聚合复用原则”。
上面例子,将Engineer和Salesman弄成并列的子类,是存在问题的。(什么问题?)
1. Salesman明确地从Employee继承,那么就无法再从Male、Newcomer等类继承来获得他们的特性。
2. Salesman的实例被new出来之后,他可能转岗做研发,想变成Engineer,无法实现。
3.12.1 什么是合成/聚合复用原则
1. 要尽量使用合成/聚合,尽量不要使用继承。
2. 从复用角度来说:“合成/聚合复用”比“继承”复用灵活。前者是动态复用(因而具有可插入性)、后者是静态复用(编译时就固定了复用关系),而且后者的复用有“不支持多重继承”的限制。
3.13 Decompose Conditional
if (date.before (SUMMER_START) || date.after(SUMMER_END))
charge = quantity * _winterRate + _winterServiceCharge;
else charge = quantity * _summerRate;
重构为:
if (notSummer(date))
charge = winterCharge(quantity);
else charge = summerCharge (quantity);
Extract Method在条件判断语句段中的应用。
3.14 Consolidate Conditional Expression
double disabilityAmount() {
if (_seniority < 2) return 0;
if (_monthsDisabled > 12) return 0;
if (_isPartTime) return 0;
// compute the disability amount
重构为:
double disabilityAmount() {
if (isNotEligableForDisability()) return 0;
// compute the disability amount
这条比较雕虫小技,可视具体情况参考实施。
3.15 Consolidate Duplicate Conditional Fragments
if (isSpecialDeal()) {
total = price * 0.95;
send();
}
else {
total = price * 0.98;
send();
}
重构为:
if (isSpecialDeal()) {
total = price * 0.95;
}
else {
total = price * 0.98;
}
send();
虽然这条也比较雕虫小技,但前面这样的代码确实也有人写得出来。
3.16 Remove Control Flag
set done to false
while not done
if (condition)
do something
set done to true
next step of loop
Control Flag为什么不好?
影响可读性,程序看起来比较绕。
3.16.1 一个例子
void checkSecurity(String[] people) {
String found = "";
for (int i = 0; i < people.length; i++) {
if (found.equals("")) {
if (people[i].equals ("Don")){
sendAlert();
found = "Don";
}
if (people[i].equals ("John")){
sendAlert();
found = "John";
}
}
}
someLaterCode(found);
}
无论找到Don还是John都退出循环做其他事儿。注意:这里使用了标志found。
重构为:
void checkSecurity(String[] people) {
String found = foundMiscreant(people);
someLaterCode(found);
}
String foundMiscreant(String[] people){
for (int i = 0; i < people.length; i++) {
if (people[i].equals ("Don")){
sendAlert();
return "Don";
}
if (people[i].equals ("John")){
sendAlert();
return "John";
}
}
return "";
}
重构之后,去掉了标志,增加了函数的出口,同时增加了程序的可读性。
3.17 Replace Nested Conditional with Guard Clauses
(卫语句:某些条件判断为真时,立即从函数返回。这样的判断就应该首先、单独进行,把这种单独检查称之为“卫语句”。Guard Clauses。是KentBeck给起的名字,Kent Beck是TDD、XP的第一倡导者。)
double getPayAmount() {
double result;
if (_isDead) result = deadAmount();
else {
if (_isSeparated) result = separatedAmount();
else {
if (_isRetired) result = retiredAmount();
else result = normalPayAmount();
};
}
return result;
};
重构为:
double getPayAmount() {
if (_isDead) return deadAmount();
if (_isSeparated) return separatedAmount();
if (_isRetired) return retiredAmount();
return normalPayAmount();
};
(好处是明显的,可以减少if/else嵌套的数目,从而强烈地提高程序可读性。比较重要的是,需要习惯“函数有多个出口”这种做法。)
3.18 Replace Conditional with Polymorphism
double getSpeed() {
switch (_type) {
case EUROPEAN:
returngetBaseSpeed();
case AFRICAN:
returngetBaseSpeed() - getLoadFactor() *
_numberOfCoconuts;
case NORWEGIAN_BLUE:
return(_isNailed) ? 0 : getBaseSpeed(_voltage);
}
throw new RuntimeException ("Should beunreachable");
}
重构为:
(跟讲Switch那个Bad Smell时举过的例子,基本一样。)
3.19 Introduce Parameter Object
重构为:
3.20 Replace Error Code with Exception
int withdraw(int amount) {
if (amount > _balance) {
return -1;
else {
_balance -= amount;
return 0;
}
}
重构为:
void withdraw(int amount) throws BalanceException {
if (amount > _balance) throw new BalanceException();
_balance -= amount;
}
Why?
1. 提高代码可读性。
2. 方便调用方。调用方可以不再判断返回的Error Code,而只是把异常直接抛出去,待最终接受方处理。比如,类C/S的结构,服务端代码在所有环节都可以直接透传Exception,简化处理流程。Exception最终由客户端统一处理。
(注:有些地方无法完全取代Error Code,比如前台回来的消息处理,onMessage函数。)
转:Bad Smell重构和设计的标准