美文网首页js css html
重构改善既有代码的设计-对于函数的重构手法总结上

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

作者: 先生zeng | 来源:发表于2023-03-03 13:34 被阅读0次

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

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

对付过长函数,一项重要的重构手法就是.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)>那么 中间引入的那些解释性临时变量也有其价值。

相关文章

网友评论

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

    本文链接:https://www.haomeiwen.com/subject/zfcukdtx.html