`
足至迹留
  • 浏览: 486217 次
  • 性别: Icon_minigender_1
  • 来自: OnePiece
社区版块
存档分类
最新评论

2.1 代码的坏味道(上)

阅读更多
当闻到代码里的坏味道时,你就可以考虑重构了。所谓的坏味道就是代码里看起来不符合设计难以理解难以修改的代码。
关于“何时重构”并没有一个精确衡量标准。没有任何度量规矩比得上一个见多识广者的直觉,这里只能介绍一些迹象何时可以用重构解决问题。我们必须培养自己的判断力,学会判断一个类内有多少实例变量算是太大,一个函数内有多少行代码才算太长。大致分为22个现象,本篇介绍前10个,下一篇介绍其他的12个。这里提到的一些做法,比如extrat method后面也会逐个介绍。

2.1 Duplicated Code (重复代码)
坏味道行列中首当其冲的就是Duplicated  Code.  如果你在一个以上的地点看到相同的程序结构,那么可以肯定 :设法将它们合而为一,程序会变得更好。首先,其他很多坏味道是因为重复代码导致的,再者重复代码经常引入bug,修改或扩展代码的时候要保持一致的修改,漏掉一处就会导致bug。

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

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

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

2.2 Long Method(过长函数)
拥有短函数的对象会活得比较好 、比较长,《clean code》一书里也重点强调了一个类一个方法只做一件事情,函数要尽可能的短小精炼。不熟悉面向对象技术的人,常常觉得对象程序中只有无穷无尽的委托,根本没有进行任何计算。和此类程序共同生活数年之后,你才会知道,这些小小函数有多大价值。"间接层"所能带来的全部利益一一解释能力、共享能力、选择能力,这都是由小型函数支持的。

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

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

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

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

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

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

2.3 Large Class (过大的类)
如果想利用单个类做太多事情 ,其内往往就会出现太多实例变量 .一旦如此,Duplicated  Code也就接踵而至了.

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

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

和"太多实例变量"一样,类内如果有太多代码,也是代码重复、混乱并最终走向死亡的源头.最简单的解决方案(还记得吗,我们喜欢简单的解决方案)是把多余的东西消灭于类内部.如果有五个"百行函 数",它们之中很多代码都相同,那么或许你可以把它们变成五个"十行函 数"和十个提炼出来的"双行函数"。

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

2.4 Long Parameter List (过长参数列)
刚开始学习编程的时候,老师教我们:把函数所需的所有东西都以参数传递进去.这可以理解 ,因为除此之外就只能选择全局数据, 而全局数据是邪恶的东西。对象技术改变了这一情况 :如果你手上没有所需的东西,总可以叫另 一个对象给你。

因此,有了对象 ,你就不必把函数需要的所有东西都以参数传递给它了,只需传给它足够的、让函数能从中获得自己需要的东西就行了。函数需要的东西多半可以在函数的宿主类中找到。面向对象程序中的函数,其参数列通常比在传统程序中短得多。

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

如果向已有的对象发出一条请求就可以取代一个参数,那么你应该激活重构手法Replace  Parameter  with Method。在这里,"已有的对象"可能是函数所属类内的一个字段,也可能是另一个参数。你还可以运用Preserve   Whole  Object将 来自同一对象的一堆数据收集起来,并以该对象替换它们。如果某些数据缺乏合理 的对象归属,可使用Introduce Parameter Object为它们制造出一个"参数对象"。

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

2.5 Divergent Change(发散式变化)
我们希望软件能够更容易被修改一一毕竟软件再怎么说本来就该是"软"的。一旦需要修改,我们希望能够跳到系统的某一点,只在该处做修改。如果不能做到这点,你就嗅出两种紧密相关的刺鼻味道中的一种了。

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

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

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

Divergent Change是指"一个类受多种变化的影响“,Shotgun Surgery则是指 "一种变化引发多个类相应修改”。这两种情况下你都会希望整理代码 ,使 "外界变化" 与 "需要修改的类" 趋于一一对应。

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

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

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

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

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

减少字段和参数的个数,当然可以去除一些坏味道,但更重要的是 : 一旦拥有新对象 ,你就有机会让程序散发出一种芳香 .得到新对象后 ,你就可以着手寻找 Feature Envy,这可
以帮你指出能够移至新类中的种种程序行为 。不必太久,所有的类都将在它们的小小社会中充分发挥价值 。

2.9 Primitive Obsession (基本类型偏执)
大多数编程环境都有两种数据:结构类型允许你将数据组织成有意义的形式;基本类型则是构成结构类型的积木块。结构总是会带来一定的额外开销.它们可能代表着数据库中的表,如果只为做一两件事而创建结构类型也可能显得太麻烦。

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

对象技术的新手通常不愿意在小任务上运用小对象,像是结合数值和币种的 money类、 由一个起始值和一个结束值组成的 range类、电话号码或邮政编码等等的特殊字符串。你可以运用Replace Data Value with Object将原本单独存在的数据值替换为对象,从而走出传统的洞窟,进入炙手可热的对象世界。如果想要替换的数据值是类型码,而它并不影响行为 ,则可以运用Replace Type Code with Class将它换掉。如果你 有与类型码相关的条件表达式, 可运用Replace Type Code with Subclass或Replace Type Code with State/Strategy 加以处理 。

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

这里说的用对象代替基本类型,并不是代替单个的基本类型,而是几个基本类型放在一起更有意义时应该使用兑现,比如“电话号码”由基本号码,区号,地区等一起来描述时,就组装成对象,比每次都用基本类型字段描述更好。

2.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 Polymorphism了。

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

相关推荐

Global site tag (gtag.js) - Google Analytics