重构改善既有代码的设计-代码的坏味道(上)

上篇文章对于重构的一些相关内容都做了定义,相信对于重构如何运作也有了一定的理解,但是什么时候做重构的动作,没那么“顺理成章”了。书里对这种描述给了几种定义,我更愿意总结一个自己的定义--感觉或者说经验,直觉。

什么时候发现代码有什么特征会让你知道该做什么样的重构动作呢?这就是当你发现代码有坏味道的时候。

一、代码的坏味道

如果当你发现一段代码存在问题,又不知道应该使用哪一种重构的手法时,你必须培养自己的判断力,总结以往的经验对代码后续的扩展以及可能出现的问题进行预判并避免或者预防、拓展。

书中罗列了几种坏味道,并进行了一些重构的建议。

1.1 重复代码

如果在一个以上的地方看到相同的代码结构,这时候我们可以肯定操作是:设法将他们合二为一,我们的程序会变更好。

示例1: 在同一个类中,两个函数含有相同的一段代码结构或者说表达式。

建议的操作:使用提炼函数(Extract method)的方式,提炼出重复的代码,并使两个函数都调用该提炼出来的代码。

示例2: 在不同类中,两个互为平级关系的类存在相同的表达式。

建议的操作: 对两个类都使用提炼函数(Extract method)的方式,提炼出重复的代码结构并使用pull up method(函数上移),将它移动到超类中。

如果代码结构只是类似,而不是完全相同,则可以使用提炼函数的手法,将相似部分与差异部分割开,构成一个函数,再使用模版方法的设计模式。

示例3: 在两个毫不相干的类中,存在相同表达式

建议的操作:可以将重复代码提炼到其中一个类,或者第三个类中(主要由开发者决定将抽离出来的函数放在哪儿?比较合适)

1.2 过长函数

总结:要记住一点: 拥有短函数的对象生命周期会更好,更长。"间接层所带来的全部好处---解释能力(更易于理解)、共享能力(可共享,减少代码数量也就是减少了维护成本)、选择能力(维护成本、拓展力)"--都是由小函数带来的。

顾名思义,这里说的是一个函数过长时,你就需要考虑改善了。

  • 这里我们遵循一个原则: 每当感觉需要以注释来说明点什么的时候,我们就可以把需要说明的实现写进一个独立的函数中,并以其用途来命名。只要能够解释其用途,我们都应该毫不犹豫的这样实现不管代码行数多不多。

建议的实现:在绝大多数情况下,都可以使用提炼函数(Extract Method)的手法就可以把函数变小。

  • 复杂一点的场景:如果函数内存在大量的参数和临时变量,则会对你的函数提炼形成阻碍,如果你尝试使用提炼函数,则会把许多参数和临时变量当作参数,传递给提炼出来的新函数,这对可读性来说,几乎没有多大的提升。

建议:此时,我们可以使用经常运用的Replace temp with Query(查询替代临时变量(将变量的获取改成使用getName()的方式))来消除临时元素,然后可以使用Introduce Object(引入参数对象,将一组多个参数整合在一个对象中,将传递多个参数改为传递一组对象)或者Preserve whole Object(保证对象完整,就是说如果你的一组入参都在一个对象中,则应该改为传递一整个对象,这样如果入参变了就需要对函数进行修改,降低维护的成本)这两种方式,可以将过长的参数列变得更简洁。

如果你按照上面的建议重构了还是不行,则可以使用这种杀手锏: Replace Method with Object(以函数对象取代函数--将这个函数放进一个单独对象中,把局部的变量改成该对象内的字段,这样就可以在同一个对象中将这个大型函数分解为多个小型函数,提高代码的可读性)

1.2.1 如何确定应该提炼哪一段代码结构呢??

    1. 寻找注释。 他们通常可以指出代码的用途以及实现手法之间的语义距离。如果代码前有注释,就是提醒你,下面的代码可以替换成一个函数。
    1. 条件表达式以及循环。一般可以使用Decompose Conditional(分解条件表达式)处理条件表达式。
      =
if (A || B  &&  C) { 

} 
else 
{
// 代码段B
}

此时你可以重构为:

if (Method()) {
    // MethodA()
} else {
// MethodB()
}

Boolean Method() { return A || B  &&  C  }
MethodA(){ // 代码段A  }
MethodB(){ // 代码段B  }

而对于循环的处理你可以将循环和在内的代码提炼到一个独立的函数中。

1.3 Large Class 过大的类

如果想要用一个类,做太多的事,其内往往会出现太多的实例变量,重复代码的问题也就马上出现了。我们此时就需要对这个类进行提炼类(Extract Class---将几个变量提炼到新的类中)提炼时,需要关注将相关联的变量放在一起。

如果是一个类,有太多的代码,也是代码重复、混乱的原因。最简单的处理方式,是将内部重复的代码提炼出来多个小函数,多次调用。

也可以使用extract class(提炼类)和extract SubClass (提炼子类)的方式。书中介绍了一种小技巧: 先确定客户端如何使用他们,然后运用Extract Interface(341)的方式为每一种使用方式提炼出一个接口,获取可以帮助你弄清楚如何分解这个类。

注:Extract Interface 若干客户使用类中的同一字类,或者两个类中的接口有相同的部分,将相同的子集提炼到一个独立的接口中。

1.4 Long Parammeter List(过长的参数列)

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

如果向已有的对象发出一条请求就可以取代一个参数,那么可以使用重构手法(replace Parammeter with Method)方法取代参数的手法,也就是说,在函数内,对象调用到某个函数时,可以将所得的结果作为参数,传递给另一个函数,让接收该参数的本身可以调用前一个函数。其实还是extract Method(提炼函数)的拓展。

replace Parammeter with Method 示例代码如下:

原有代码
int method(int a, int b, int c) {
c += a+b;
return c;
}


改造成:

int getA() {
return a;
}

int method(int b, int c) {
c += getA()+b;
return c;
}

注:"已有的对象"可能是指函数所属类内的一个字段,也可能是另一个参数。可以使用Preserve whole Object将同一对象的数据收集起来,也可以使用Introduce Paramter Object 为他们制造一个“业务”对象。

1.5 Divergent Change(发散式变化)

如果某个类经常因为不同的原因在不同的方向上发生变化,Divergent Change就出现了。

这个坏味道可能比较难以理解,因此我使用原书中的例子作为参考:

对于一个类,如果新加入一个数据库的字段,就必须修改其中三个函数,如果出现要一个新的方式,就要修改另外3-4个函数,那么此时这个对象也许分为两个会更好,其实就是说,单一职责。希望每个对象只负责一件事,每次改变都是因为一种原因而改变。

当然,往往只有当你遇到上述两情况你才会发现这类问题,针对外界发生的所有电话都要发生在单一类中,而这个新类内,所有内容都应该反应此变化才行。

为此你需要找出特定原因而造成的所有变化,然后运用Extract class 将它们提炼到另一个类中。

1.6 Shotgun Surgery(散弹式修改)

跟上一种坏味道恰相反,如果遇到某种变化,你都需要在许多不同的类中做出许多的小修改,则不但很难找到他们,还很容易忘记某个重要的修改。

重构建议:这种情况,你应该使用Move Field和Move Method 把所有需要修改的代码放在同一个类。如果没有合适的类,就想办法创造一个,handler处理类。

第二种处理方式,是运用inline class把一系列相关的行为都放进同一个类中,这可能会造成少量的发散式变化,但是你处理会变得非常方便。

1.7 Feature Envy (依恋情结)

面向对象编程的技术有一个要点:这是一种会将数据和对数据的操作行为都封装在一起的技术。特征就是:函数对某个类的兴趣远远高过自己所处的类的兴趣了。

怎么理解上述这句话呢? 通常就是指数据,如果一个类中的函数使用到的数据,多数都是另一个类中的对象的取值函数,则此时应该使用重构手法是Move Method(把它移动到它该去的地方)。

当然,并非所有情况并不是都是这么简单。一个函数往往会用到几个类的功能,那么它又该置于何处?

我们的原则是: 判断哪个类拥有最多被此函数使用的数据,然后就把这个函数和那些数据摆在一起。如果以Extract Method将这个函数分解为多个小函数,再置于不同的地点此时就容易多了。

(注:书中额外列举了一些设计模式会破坏这种规则,策略模式、观察者模式与单例模式等)

1.8 Data Clump(数据泥团)

我们可能常常可以在很多地方看到相同的三四项数据:两个不同类中存在相同的字段,许多函数签名中相同的参数。这些总是绑在一起出现的数据应该抽象出他们独有的对象。运用Extract Class将他们提炼到一个独立对象中。然后将注意力关注在函数的签名上面,运用Preserve whole Object或者Introduce Paramter Object将参数列缩短简化函数的调用。

减少字段和参数的个数,不止可以去除一些坏味道,获取到新对象后,也可以着手寻找依恋情结。进一步优化。

待续。。。
关注下一篇:

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

推荐阅读更多精彩内容