重构改善既有代码的设计-对于函数的重构手法总结上

在《重构改善既有代码设计》一书中,作者的经验是,重构的大多数手法都是源自对于函数进行的处理,绝大多数是从过长函数开始。结合我的实际处理情况,不外如是。

过长函数,确实很讨厌。因为他们往往向书中所说,会包含太多的信息,这些信息又会被函数错综复杂的逻辑。不易鉴别。如何对付过长函数,作者系统化的总结出了一套经验:

对付过长函数,一项重要的重构手法就是.Extract Method (110), 它把一段代码从原先函 数中提取出来,放进一个单独函数中。而Inline Method(117)正好相反:将一个函数调用动作替换为该函数本体。如果在进行多次提炼之后,意识到提炼所得的某些函数 并没有做任何实质事情,或如果需要回溯恢复原先函数,我就需要Inline Method (117)。

Extract Method (110)最大的困难就是处理局部变量,而临时变量则是其中一个 主要的困难源头。处理一个函数时,我喜欢运用Replace Temp with Query (120)去掉 所有可去掉的临时变量。如果很多地方使用了某个临时变量,我就会先运用既Split Temporary Variable (128)将它变得比较容易替换。

但有时候临时变量实在太混乱,难以替换。这时候我就需要使用Replace Method with Method Object(135) 它让我可以分解哪怕最混乱的函数,代价则是引入一个新
类。

注释:重构手法后面的括号是指书中的对应处理手法的页数,之所以列出来也是为了读者可以方便去书中直接找对应的处理方法。

image.png

下面会一一介绍包括使用逻辑以及注意点:

一、Extract Method (提炼函数)

你有一段代码可以被组织在一起并独立出来。将这段代码放进一个独立函数中,并让函数名称解释该函数的用途。

image.png

为什么需要提炼函数呢? 首先,如果每个函数的粒度都很小,那么函数被复用的机会就更大;其次,这会使高层函数读起来就像一系列注释;再次,如果函数都是细粒度,那么函数的覆写也会更容易些。

做法:

1、创造一个新函数,根据这个函数的意图来对它命名(以它"做什么”来命名, 而不是以它“怎样做”命名)。
2、将提炼出的代码从源函数复制到新建的目标函数中。
3、仔细检査提炼出的代码,看看其中是否引用了"作用域限于源函数”的变量
(包括局部变量和源函数参数)。
4、检査被提炼代码段,看看是否有任何局部变量的值被它改变。如果一个临时 变量值被修改了,看看是否可以将被提炼代码段处理为一个查询,并将结果值赋值给相关变量。

如果很难这样做,或者如果被修改的变量不止一个,你就不能仅仅将这段代码原封不动地提炼出来。你可能需要先使用Splite Temporary Variable (128),然后再尝试提炼。也可以使用Temp with Query (120) 将临时变量消灭掉。

5、将被提炼代码段中需要读取的局部变量,当作参数传给目标函数。
6、处理完所有局部变量之后,进行编译。
7、在源函数中,将被提炼代码段替换为对目标函数的调用。
8、如果你将任何临时变量移到目标函数中,请检查它们原本的声明式是否在被提炼代码段的外围。如果是,现在你可以删除这些声明式了。
9、编译,测试。

如果你发现源函数的参数被赋值,应该马上使用Remove Assignments to Parameters (131)就是将入参的局部变量再赋值给一个同类型的参数。

void method(Integer a, Integer b) {
    // Remove Assignments to Parameters
    Integer c = a; 
    c += 1;
}

特殊情况

被赋值的临时变量也分两种情况。较简单的情况是:这个变量只在被提炼代码段中使用。果真如此,你可以将这个临时变量的声明移到被提炼代码段中,然后一 起提炼出去。另一种情况是:被提炼代码段之外的代码也使用了这个变量。这又分 为两种情况:如果这个变量在被提炼代码段之后未再被使用,你只需直接在目标函数中修改它就可以了;如果被提炼代码段之后的代码还使用了这个变量,你就需要让目标函数返回该变量改变后的值。

代码示例:(带有修改局部变量在赋值的情况)

void printowing() (
    Enumeration e = _orders.elements();
    double outstanding = 0.0;
    printBanner();
    // calculate outstanding while (e.hasMoreElements()) (
    Order each = (Order) e.nextElement();
     outstanding += each.getAmount();
)
printDetails(outstanding);
)
现在我把“计算”代码提炼出来:
void printowing() (
    printBanner();
    double outstanding = getOutstanding(); 
    printDetails(outstanding);
}

/** Enumeration变量e只在被提炼代码段中用到,所以可以将它整个搬到新函数
中。double变量outstanding在被提炼代码段内外都被用到,所以必须让提炼出 来的新函数返回它。
*/
double getOutstanding() (
    Enumeration e = _orders.elements();
    double outstanding = 0.0;
    while (e.hasMoreElements()) (
          Order each = (Order) e.nextElement();
         outstanding += each.getAmount();
     }
    return outstanding;
}

本例中的outstanding变量只是很单纯地被初始化为一个明确初值,所以我可 以只在新函数中对它初始化。如果代码还对这个变量做了其他处理,就必须将它的 值作为参数传给目标函数。

如果需要返回的变量不止一个,又该怎么办呢???

有几种选择。最好的选择通常是:挑选另一块代码来提炼。我比较喜欢让每个 函数都只返回一个值,所以会安排多个函数,用以返回多个值。如果你使用的语言 支持“出参数"(output parameter),可以使用它们带回多个回传值。但我还是尽可 能选择单一返回值。

临时变量往往为数众多,甚至会使提炼工作举步维艰。这种情况下,我会尝试 先运用Replace Temp with Query (120)减少临时变量。如果即使这么做了提炼依旧困难重重,我就会动用Replace Method with Method Object (135),这个重构手法不在乎 代码中有多少临时变量,"也不在乎你如何使用它们。

二、Inline Method (内联函数)

一个函数的本体与名称同样清楚易懂。在函数调用点插入函数本体,然后移除该函数。比起提炼函数,内联函数的操作正好相反,

实例:

int getRatingf) {
return (moreThanFiveLateDeliveries()) ? 2 : 1;
}
boolean moreThanFiveLateDeliveries() (
    return _numberOfLateDeliveries > 5;
}
重构后:
int getRating() {
    return (_numberOfLateDeliveries > 5) ? 2 : 1;
}

本重构方法好像很简单,但是为了以防万一导致出现其他问题,还是罗列出具体的操作动作。

做法:

1、检査函数,确定它不具多态性。如果子类继承了这个函数,就不要将此函数内联,因为子类无法覆写一个 根本不存在的函数。
2、找出这个函数的所有被调用点。
3、将这个函数的所有被调用点都替换为函数本体。
4、编译,测试。
5、删除该函数的定义。

三、Inline Temp (内联临时变量)

你有一个临时变量,只被一个简单表达式赋值一次,而它妨碍了其他重构手法。
将所有对该变量的引用动作,替换为对它赋值的那个表达式自身。

示例:

double basePrice = anOrder.basePrice();
return (basePrice > 1000)

重构为:
return (anOrder.basePrice() > 1000)

Inline Temp (119)多半是作为Replace Temp with Query (120)的一部分使用的,所 以真正的动机出现在后者那儿。唯一单独使用Inline Temp情况是:你发现某 个临时变量被赋予某个函数调用的返回值。

实现步骤:

1、检査给临时变量赋值的语句,确保等号右边的表达式没有副作用。
2、如果这个临时变量并未被声明为final,那就将它声明为final,然后编译。=»这可以检查该临时变量是否真的只被赋值一次。
3、找到该临时变量的所有引用点,将它们替换为“为临时变量赋值”的表达式。
4、每次修改后,编译并测试。
5、修改完所有引用点之后,删除该临时变量的声明和赋值语句。
6、编译,测试。

四、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;
}

这样实现的动机为:

临时变量的问题在于:它们是暂时的,而且只能在所属函数内使用。由于临时 变量只在所属函数内可见,所以它们会驱使你写出更长的函数,因为只有这样你才 能访问到需要的临时变量。如果把临时变量替换为一个査询,那么同一个类中的所 有函数都将可以获得这份信息。这将带给你极大帮助,使你能够为这个类编写更清 晰的代码。

这个重构手法较为简单的情况是:临时变量只被赋值一次,或者赋值给临时变 量的表达式不受其他条件影响。其他情况比较棘手,但也有可能发生。你可能需要 先运用Splite Temporary Variable (128分解临时变量为多个)或 Separate Query from Modifier (279将查询函数和修改函数分离) 使情况变得简单一些,然后再替换临时变量。如果你想替换的临时变量是用来收集结果的(例如循环中的累加值),就需要将某些程序逻辑(例如循环)复制到查询函数去。

做法:

1、找出只被赋值一次的临时变量。
2、今如果某个临时变量被赋值超过一次,考虑使用splite Temporary Variable 将它分割成多个变量。
3、将该临时变量声明为final。-》确保改变量只被赋值一次
4、编译。

5、将"对该临时变量赋值”之语句的等号右侧部分提炼到一个独立函数中。

5.1 首先将函数声明为private.日后你可能会发现有更多类需要使用它,那时放松对它的保护也很容易.
5.2 确保提炼出来的函数无任何副作用,也就是说该函数并不修改任何对象内 容。如果它有副作用,就对它进行 Separate Query from Modifier (279).

6、编译,测试。
7、在该临时变量身上实现 Inline Temp 。

我们常常使用临时变量保存循环中的累加信息。在这种情况下,整个循环都可 以被提炼为一个独立函数,这也使原本的函数可以少掉几行扰人的循环逻辑。有时 候,你可能会在一个循环中累加好几个值,就像本书第26页的例子那样。这种情况下你应该针对每个累加值重复一遍循环,这样就可以将所有临时变量都替换为查询。

示例如下:

开始的实例:

首先,我从一个简单函数开始:
double getPrice() (
int basePrice = _quantity * _itemPrice;
double discountFactor;
if (basePrice > 1000) discountFactor = 0.95;
else discountFactor = 0.98;
return basePrice * discountFactor;
}
我希望将两个临时变量都替换掉。
先把临时变量声明为final,检查它们是否的确只被赋值一次.
double getPrice() {
final int basePrice = _quantity * _itemPrice;
final double discountFactor;
if (basePrice > 1000) discountFactor = 0.95;
else discountFactor = 0.98;
return basePrice * discountFactor;
}
如果有任何问题,编译器就会警告我。之所以先做这件事,因为如 果临时变量不只被赋值一次,我就不该进行这项重构。

重构后:

private int basePrice() (
return _quantity * _itemPrice;
}

private double discountFactor() (
if (basePrice() > 1000) return 0.95;
else return 0.98;
}

最终,getPrice ()变成了这样:

double getPrice() {
return basePrice() * discountFactor();
}

五、 Introduce Explaining Variable (引入解释性变量)

你有一个复杂的表达式。将该复杂表达式(或其中一部分)的结果放进一个临时变量,以此变量名称来解释表达式用途。

if ( (platform. toUpperCase () . indexOf () > -1) && (browser.toUpperCase().indexOf(nIE") > -1) && waslnitialized() && resize > 0)
{
// do something
}

重构后:
final boolean isMacOs = platform.toUpperCase().indexOf("MAC") > -1; final boolean isIEBrowser = browser. toUpperCase () . indexOf ('* IE") > -1; final boolean wasResized = resize > 0;
if (isMacOs && isIEBrowser && waslnitialized().&& wasResized) (
//do something
}

动机:缘由

表达式有可能非常复杂而难以阅读。这种情况下,临时变量可以帮助你将表达 式分解为比较容易管理的形式。你可以用这项 重构将每个条件子句提炼出来,以一个良好命名的临时变量来解释对应条件子句的 意义。代码可读性强。

做法

做法
1、声明一个final临时变量,将待分解之复杂表达式中的一部分动作的运算结果赋值给它。
2、将表达式中的“运算结果”这一部分,替换为上述临时变量。
3、如果被替换的这一部分在代码中重复出现,你可以毎次一个,逐一替换。
4、编译,测试。
5、重复上述过程,处理表达式的其他部分。

示例

我们从一个简单计算开始:
double price() (
    // price is base price - quantity discount + shipping 
    return .quantity *  _itemPrice  
                      -Math.max(0, _quantity - 500) * _itemPrice * 0.05 
                      +Math.min(_quantity * _itemPrice * 0.1, 100.0);
}
这段代码还算简单,不过我可以让它变得更容易理解。首先我发现,底价(base price)等于数量(quantity)乘以单价(item price)o, 于是,我把这一部分计算的结 果放进一个临时变量中:

稍后也用上了 '‘数量乘以单价"运算结果,所以我同样将它替换为basePrice 临时变量.

批发折扣(quantity discount)的计算提炼出来,将结果赋予临时变量.

最后,我再把运费(shipping)计算提炼出来,将运算结果赋予临时变量 shipping。同时我还可以删掉代码中的注释,因为现在代码已经可以完美表达自 己的意义了:

重构后:
double price() (
    final double basePrice = _quantity * _itemPrice;
    final double quantityDiscount = Math.rnax(Or _quantity - 500)* _itemPrice * 0.05; final double shipping = Math.min(basePrice * 0.1, 100.0);
    return basePrice - quantityDiscount + shipping;
}

使用 Extract Method处理上述范例
面对上述代码,我通常不会以临时变量来解释其动作意图,我更喜欢使用 Extract Method(110)。

这一次我把底价计算提炼到一个独立函数中,批发折扣(quantity discount)的计算提炼出来,运费(shipping)计算提炼出来。

最终可以得到如下的代码:

double price() (
     return basePrice() - quantityDiscount() + shipping();
)

// 一开始我会把这些新函数声明为private; 如果其他对象也需要它们,我可以轻易释放这些函数的访问限制。
private double quantityDiscount() (
    return Math.max(0, .quantity - 500) * _itemPrice * 0.05;
)
private double shipping() (
    return Math.min(basePrice() * 0.1, 100.0);
}
private double basePrice() (
    return ^quantity * _itemPrice;
}


说明:那么,应该在什么时候使用Introduce Explaining Variable (124)呢?答案是:在 Extract Method (110)需要花费更大工作量时。如果我要处理的是一个拥有大量局部变量的算法,那么使用Extract Method (110)绝非易事。这种情况下就会使用Introduce Explaining Variable (124)来理清代码,然后再考虑下一步该怎么办。搞清楚代码逻 辑之后,我总是可以运用Replace Temp with Query (120)把中间引入的那些解释性临 时变量去掉。况且,如果我最终使用Replace Method with Method Object (135)>那么 中间引入的那些解释性临时变量也有其价值。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念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

推荐阅读更多精彩内容