6.1 Extract Method(提炼函数)

  1. **将代码放进一个独立函数中,并让函数名称解释该函数的用途。**
  1. void printOwing(double amount) {
  2. printBanner();
  3. //print details
  4. System.out.println("name:" + _name);
  5. System.out.println("amount:" + amount);
  6. }
  7. public void printBanner() {
  8. }

变成

  1. void printOwing(double amount) {
  2. printBanner();
  3. //print details
  4. printDetails(amount);
  5. }
  6. private void printDetails(double amount) {
  7. System.out.println("name:" + _name);
  8. System.out.println("amount:" + amount);
  9. }
  10. public void printBanner() {
  11. }

动机

  1. `Extract Method` 是最常用的重构手法之一。当一个过长的函数或者一段需要注释才能让人理解用途的代码,将其放进一个独立函数中比较好。
  2. 有几个原因是啥造成我喜欢简短而命名良好的函数。首先,如果每个函数的粒度都很小,那么函数被复用的机会就更大;其次,这会使高层函数读起来就像一系列注释;再次,如果函数都是细粒度,那么函数的覆写也会更容易些。
  3. 一个函数多长才算合适?长度不是问题,**关键在于函数名称和函数本体之间的语义距离**。 如果提炼可以强化代码的清晰度,那就去做,就算函数名称比提炼出来的代码还长也无所谓。

做法

  • 创造一个新函数,根据这个函数的意图来对他命名(以它“做什么”来命名,而不是以它“怎么做”来命名)
    • 即使你想要提炼的代码非常简单,例如只是一条消息或一个函数调用,只要新函数的名称能够以更好方式昭示代码意图,你也应该提炼它。但如果NIIT想不出一个更有意义的名称,就别动。
  • 将提炼出的代码从源函数复制到新建的目标函数中。
  • 仔细检查提炼出的代码,看看其中是否引用了“作用域限于源函数”的变量(包括局部变量和源函数参数)
  • 检查是否有“仅用于被提炼代码段”的临时变量。如果有,目标函数将他们声明为临时变量。
  • 检查被提炼代码段,看看是否有任何局部变量的值被改变。如果一个临时变量的值被修改了,看看是否可以将被提炼代码段处理为一个查询,并将结果赋值给相关变量。如果这样很难做,或如果被修改的变量不止一个,你就不能仅仅将这段代码原封不动地提炼出来。你可能需要先使用Split Temporary Variable。然后再尝试提炼。也可以使用Replace Temp with Query 将临时变量消灭掉。
  • 将被提炼代码段中需要读取的局部变量,当做参数传递给目标函数。
  • 处理完所有的局部变量之后,进行编译
  • 在源函数中,将被提炼代码段替换为对目标函数的调用。
    • 如果你将任何临时变量移动到目标函数中,检查它们原本的声明式是否在被提炼代码段的外围。如果是,现在你可以删除这些声明了。
  • 编译,测试。

范例:无局部变量

  1. static class Original {
  2. Vector<Order> _orders = new Vector<>();
  3. String _name = "tom";
  4. void printOwing() {
  5. Enumeration e = _orders.elements();
  6. double outStanding = 0.0;
  7. //print banner
  8. System.out.println("*****************");
  9. System.out.println("**Customer Owes**");
  10. System.out.println("*****************");
  11. //calculate outstanding
  12. while (e.hasMoreElements()) {
  13. Order each = (Order) e.nextElement();
  14. outStanding += each.getAmount();
  15. }
  16. //print details
  17. System.out.println("name:" + _name);
  18. System.out.println("amount:" + outStanding);
  19. }
  20. }
  1. **提炼打印代码段**
  1. void printOwing() {
  2. Enumeration e = _orders.elements();
  3. double outStanding = 0.0;
  4. //print banner
  5. printBanner();
  6. //calculate outstanding
  7. while (e.hasMoreElements()) {
  8. Order each = (Order) e.nextElement();
  9. outStanding += each.getAmount();
  10. }
  11. //print details
  12. System.out.println("name:" + _name);
  13. System.out.println("amount:" + outStanding);
  14. }
  15. void printBanner() {
  16. System.out.println("*****************");
  17. System.out.println("**Customer Owes**");
  18. System.out.println("*****************");
  19. }

范例:有局部变量

提炼函数的难点在于局部变量,包括传进源函数的参数和源函数所声明的临时变量。局部变量的作用域仅限于源函数,所以当我们使用Extract Method 时,必须花费额外的功夫去处理这些变量。某些时候它们甚至可能妨碍我们,使我们根本无法进行重构。
局部变量最简单的情况是:被提炼代码段只是读取这些变量的值,并不修改它们,这种情况下简单地将它们当做参数传给目标函数。

  1. void printOwing() {
  2. Enumeration e = _orders.elements();
  3. double outStanding = 0.0;
  4. //print banner
  5. printBanner();
  6. //calculate outstanding
  7. while (e.hasMoreElements()) {
  8. Order each = (Order) e.nextElement();
  9. outStanding += each.getAmount();
  10. }
  11. //print details
  12. System.out.println("name:" + _name);
  13. System.out.println("amount:" + outStanding);
  14. }
  1. **提取**`printDetail`
  1. void printOwing() {
  2. Enumeration e = _orders.elements();
  3. double outStanding = 0.0;
  4. //print banner
  5. printBanner();
  6. //calculate outstanding
  7. while (e.hasMoreElements()) {
  8. Order each = (Order) e.nextElement();
  9. outStanding += each.getAmount();
  10. }
  11. //print details
  12. printDetail(outStanding);
  13. }
  14. private void printDetail(double outStanding) {
  15. System.out.println("name:" + _name);
  16. System.out.println("amount:" + outStanding);
  17. }

范例:对局部变量再赋值

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

  1. void printOwing() {
  2. Enumeration e = _orders.elements();
  3. double outStanding = 0.0;
  4. //print banner
  5. printBanner();
  6. //calculate outstanding
  7. while (e.hasMoreElements()) {
  8. Order each = (Order) e.nextElement();
  9. outStanding += each.getAmount();
  10. }
  11. //print details
  12. printDetail(outStanding);
  13. }

现在将“计算”代码提炼出来。

  1. void printOwing() {
  2. //print banner
  3. printBanner();
  4. //calculate outstanding
  5. double outStanding = getOutStanding();
  6. //print details
  7. printDetail(outStanding);
  8. }
  9. double getOutStanding() {
  10. Enumeration e = _orders.elements();
  11. double outStanding = 0.0;
  12. while (e.hasMoreElements()) {
  13. Order each = (Order) e.nextElement();
  14. outStanding += each.getAmount();
  15. }
  16. return outStanding;
  17. }

Enumeration 变量e 只在被提炼代码段中用到,所以可以将它整个搬到新函数中。double变量 outStanding在被提炼到代码段内外都被用到,所以必须让提炼出的新函数返回它。

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

6.2 Inline Method(内联函数)

在函数调用点插入函数本体,然后移除该函数。

  1. int getRating() {
  2. return moreThanFiveLateDeliveries() ? 2 : 1;
  3. }
  4. boolean moreThanFiveLateDeliveries() {
  5. return (_numberOfLateDeliveries > 5);
  6. }
  1. 变成
  1. int getRating() {
  2. return (_numberOfLateDeliveries > 5) ? 2 : 1;
  3. }

动机

本书经常以简短的函数表现动作意图,这样会使代码更清晰易懂。但有时候你会遇到某些函数,其内部代码和函数名称同样轻易易懂。也可能你重构了该函数,使得内部和其名称变得同样清晰。果真如此,你就应该去掉这个函数,直接使用其中的代码。间接性可能带来帮助,但非必要的间接性总是让人不舒服。
另一种需要使用Inline Method 的情况是:你手上有一群组织不甚合理的函数。你可以将它们都内联到一个大型函数中,在从中提炼出组织合理的小型函数。实施Replace Method with Method Object 之前先这么做,往往可以获取不错的效果。


如果别人使用了太多间接层,使得系统中左右的函数都似乎只是对另一个函数的简单委托,造成我们在这些委托动作之间晕头转向,那么我们通常会使用Inline Method 。当然,间接层尤其价值,但不是所有间接层都要价值,试着使用内联手法,我们可以找出那些没用的间接层,同时将无用的间接层去除。

做法

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

    6.3 Inline Temp(内联临时变量)

    将所有对该变量的引用动作,替换为对它复制的那个表达式自身。

    1. double basePrice = this.basePrice();
    2. return (basePrice > 1000);

    变成

    1. return ( this.basePrice() > 1000);

    动机

    Inline Temp 多半是作为Replace Temp with Query 的一部分使用,所以真正的动机出现在后者那儿。唯一单独使用Inline Temp 的情况是:你发现某个临时变量被赋予某个函数调用的返回值。一般来说,这样的临时变量不会有任何危害,可以放心地把它留在哪儿。但如果这个临时变量妨碍了其他的重构手法,例如 Extract Method ,你就应该将它内联化。

    做法

  • 检查给临时变量赋值的语句,确保等号右边的表达式没有任何副作用。

  • 如果这个临时变量未被声明为final,那就将它声明为final,然后编译
    • 这可以检测该临时变量是否真的只被赋值一次。
  • 找到临时变量的所有引用点,将它们替换为“为临时变量赋值”的表达式
  • 每次修改后,编译并测试。
  • 修改完所有引用点之后,删除该临时变量的声明和赋值语句
  • 编译,测试

    6.4 Replace Temp with Query(以查询取代临时变量)

    将这个表达式提炼到一个独立函数中。将这个临时变量所有的引用点替换为新函数的调用。此时,新函数就可以被其他函数使用。
    1. double basePrice = _quantity * _itemPrice;
    2. if (basePrice > 1000) {
    3. return basePrice * 0.95;
    4. }
    5. return basePrice * 0.98;
    变成 ```java

if (basePrice() > 1000) { return basePrice() 0.95; } return basePrice() 0.98;


double basePrice() { return _quantity * _itemPrice; }

  1. ---
  2. 临时变量的问题在于:它们是暂时的,而且只能在所属函数内使用。由于临时变量只在所属函数内可见,所以它们会驱使你写出更长的函数,因为只有这样你才能访问到需要的临时变量。如果把临时变量替换为一个查询,那么一个类中所有的函数都可以获得这份信息。这将带给你极大帮助,使你能够为这个类编写更清晰的代码。<br />`Replace Temp with Query` 往往是你运用 `Extract Method` 之前必不可少的一个步骤。布局变量会使代码难以提炼,所以你应该尽可能把它们替换为查询式。
  3. 这个重构手法较为简单的情况是:临时变量只被赋值一次,或者赋值给临时变量的表达式不受其他环境的影响。其他情况比较棘手,但也有可能发生。你可能先运用`Split Temporary Variable` `Separate Query from Modifiter` 使情况变得简单一些,然后再替换临时变量。如果你想替换的临时变量是用来收集结果的(例如循环中的累加值),就需要将某些程序逻辑复制到查询函数去。
  4. <a name="Gv5iw"></a>
  5. ### 做法
  6. 首先是简单情况:
  7. - 找出只被赋值一次的临时变量。
  8. - 如果某个临时变量被赋值超过一次,考虑使用`Split Tempoary Variable` 将它分割成多个变量。
  9. - 将给临时变量声明为final
  10. - 编译
  11. - 将“对该临时变量赋值”之语句的等号有部分提炼到一个独立函数中
  12. - 首先将函数声明为private,日后你可能会发现更多类需要使用它,那是放松对它的保护也很容易。
  13. - 确保提炼出来的函数无任何副作用,也就是说该函数并不修改任何对象内容。如果它有副作用,就对它进行`Separate Query from Modifler`
  14. - 编译,测试
  15. - 在该临时变量身上实施`Inline Temp`
  16. <a name="UbZjH"></a>
  17. ### 范例
  18. ```java
  19. double getPrice() {
  20. double basePrice = _itemPrice * _quantity;
  21. double discountFactor;
  22. if (basePrice > 10000) {
  23. discountFactor = 0.95;
  24. } else {
  25. discountFactor = 0.98;
  26. }
  27. return basePrice * discountFactor;
  28. }
  1. 我们希望将两个临时变量都替换掉。<br />先将其声明为final,检查他们是否只被赋值一次。
  1. double getPrice() {
  2. final double basePrice = _itemPrice * _quantity;
  3. final double discountFactor;
  4. if (basePrice > 10000) {
  5. discountFactor = 0.95;
  6. } else {
  7. discountFactor = 0.98;
  8. }
  9. return basePrice * discountFactor;
  10. }
  1. 如果有任何问题,编译器就会警告。之所以先做这件事,因为如果临时变量不只被赋值一次,我们就不该进行这项重构。之后提花临时变量
  1. double getPrice() {
  2. final double basePrice = getBasePrice(_itemPrice, _quantity);
  3. final double discountFactor;
  4. if (basePrice > 10000) {
  5. discountFactor = 0.95;
  6. } else {
  7. discountFactor = 0.98;
  8. }
  9. return basePrice * discountFactor;
  10. }
  11. private double getBasePrice(double itemPrice, double quantity) {
  12. return itemPrice * quantity;
  13. }
  1. 然后编译测试,并使用 ` Inline Temp` 首先把临时变量basePrice替换
  1. double getPrice() {
  2. final double discountFactor;
  3. if (getBasePrice(_itemPrice, _quantity) > 10000) {
  4. discountFactor = 0.95;
  5. } else {
  6. discountFactor = 0.98;
  7. }
  8. return getBasePrice(_itemPrice, _quantity) * discountFactor;
  9. }
  1. 替换basePrice之后,用类似的方法提炼discountFactor
  1. double getPrice() {
  2. final double discountFactor = getDiscountFactor();
  3. return getBasePrice(_itemPrice, _quantity) * discountFactor;
  4. }
  5. private double getDiscountFactor() {
  6. if (getBasePrice(_itemPrice, _quantity) > 10000) {
  7. return 0.95;
  8. }
  9. return 0.98;
  10. }
  1. 替换临时变量
  1. double getPrice() {
  2. return getBasePrice(_itemPrice, _quantity) * getDiscountFactor();
  3. }

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

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

  1. public void original() {
  2. if ((platform.toUpperCase().indexOf("MAC") > -1) &&
  3. (browser.toUpperCase().indexOf("IE") > -1) &&
  4. wasInitialized() && resize > 0
  5. ) {
  6. // do something
  7. }
  8. }
  9. public void newMethod() {
  10. final boolean isMacOSs = platform.toUpperCase().indexOf("MAC") > -1;
  11. final boolean isIEBrowser = browser.toUpperCase().indexOf("IE") > -1;
  12. final boolean wasResized = resize > 0;
  13. if (isMacOSs && isIEBrowser && wasInitialized() && wasResized) {
  14. // do something
  15. }
  16. }

动机

表达式有可能非常复杂难以阅读。这种情况下,临时变量可以帮助你将表达式分解为比较容易管理的形式。
这条件逻辑中,Introduce Explaining Variable 特别有价值:你可以用这项重构将每个条件子句提炼出来,以一个良好命名的临时变量来解释对应条件子句的意义。使用这种重构的另一种情况是,在较长算法中,可以运用临时变量来解释每一步运算的意义。

Introduce Explaining Variable 是一个很常见的重构手法,但是,我们并不常用它。可以尽量使用Extract Method 来解释一段代码的意义。毕竟临时变量只在它所处的那个函数中才有意义,局限性比较大,函数则可以在对象的整个生命中都有用。并且可以被其他对象使用。但有时候,当局部变量使用Extract Method 难以进行时,才是用 Introduce Explaining Variable

6.6 Split Temporary Variable(分解临时变量)

如果程序有某个临时变量被赋值超过一次,它既不是循环遍历也不被用于收集计算结果。针对每次赋值,创造一个独立、对应的临时变量。

  1. double temp = 2 * (_height + _width);
  2. System.out.println(temp);
  3. temp = _height * _width;
  4. System.out.println(temp);
  5. -------------------------------
  6. --- 变成如下
  7. -------------------------------
  8. final double perimeter = 2 * (_height + _width);
  9. System.out.println(perimeter);
  10. final double area = _height * _width;
  11. System.out.println(area);

动机

如果临时变量被赋值超过一次,就意味着它们在函数中承担了一个以上的责任。如果临时变量承担多个责任,它就应该被替换(分解)成多个临时变量。每个变量只承担一个责任。同一个临时变量承担两件不同的事情,会令代码阅读者糊涂。

做法

  • 在待分解临时变量的声明以及第一次被赋值处。修改你名称。
  • 将新的临时变量声明为final
  • 以该临时变量的第二次赋值动作为界,修改此前对该临时变量的所有引用点,让他们引用新的临时变量。
  • 在第二次赋值处,重试声明原先那个临时变量。
  • 编译,测试
  • 逐次重复上述过程。每次都在声明处对临时变量改名,并修改下赋值之前的引用点。

6.7 Remove Assignments to Parameters(移除对参数的赋值)

代码对一个参数进行赋值,以一个临时变量取代该参数的位置。

  1. int discount(int inputVal,int quantity ,int yearToDate){
  2. if (inputVal>50) {
  3. inputVal-=2;
  4. }
  5. return inputVal;
  6. }
  7. int discount2(int inputVal,int quantity ,int yearToDate){
  8. int result =inputVal;
  9. if (inputVal>50) {
  10. result-=2;
  11. }
  12. return result;
  13. }

动机

对参数赋值。如果把一个名为foo的对作为参数传给某个函数,那么“对参数赋值”意味改变foo,使它引用另外一个对象。如果在“传入对象”身上进行什么操作,没问题,针对“foo 被改变而指向另一个对象”讨论。

  1. void aMethod(Object foo){
  2. foo.modifyInSomeWay();
  3. foo = anotherObject();
  4. }
  1. 这样的做法,降低了代码的清晰度,而且混淆了按值传递和按引用传递这两种方式。

6.8 Replace Method with Method Object(以函数对象取代函数)

有一个大型函数,其中对局部变量的使用使你无法采用Extract Method。将这个函数放进一个单独的对象中,如此一来局部变量就变成了对象内的字段。然后可以在同一个对象中将这个大型函数分解成多个小型函数。

动机

本书强调小型函数的优美动人。只要将相对独立的代码从大型函数中提炼出来,就可以大大提高代码的可读性。
但是,局部变量的存在会增加函数分解难度。如果一个函数之中又局部变量泛滥成灾,想分解这个函数是非常困难的。Replace Temp with Query 可以减轻这一负担,但有时候会发现根本无法拆解一个需要拆解的函数。这种情况下,可以使用函数对象。
Replace Method with method Object 会将所有的局部变量都变成函数对象的字段。然后就可以使用对这个新对象使用Extract Method 创造出新函数,从而将原本的大型函数拆机诶变短。

做法

  • 建立一个新类,根据待处理函数的用途,为这类命名。
  • 在新类中建立一个final字段,用以保存原先大型函数所在的对象。我们将这个字段称为“源对象”。同时,针对原函数的每个临时变量和每个参数,在新类中建立一个对应字段保存。
  • 在新类中建立一个构造函数,接受源对象与原函数的所有参数作为参数
  • 在新类中建立一个compute()函数。
  • 将原函数复制到compute()函数中。如果需要调用源对象的任何函数,请通过源对象字段调用。
  • 编译
  • 将旧函数的函数体替换为这样一个条语句:“创建上述新类的一个新对象,而后调用其中的compute()函数”

    范例

    1. public class Account {
    2. int gama(int inputVal, int quantity, int yearToDate) {
    3. int importantValue1 = (inputVal * quantity) + dalta();
    4. int importantValue2 = (inputVal * quantity) + 100;
    5. if ((yearToDate - importantValue1) > 100) {
    6. importantValue2 -= 20;
    7. }
    8. int importantValue3 = importantValue2 * 7;
    9. return importantValue3 - 2 * importantValue1;
    10. }
    11. private int dalta() {
    12. return 0;
    13. }
    14. }

    为了把这个函数编程一个函数字段,首先需要声明一个新类。在新类中提供一个final字段用以保存源对象;对于函数的每一个参数和每一个临时变量,也以一个字段逐一保存。

    1. public class Gamma {
    2. private final Account _account ;
    3. private int inputVal;
    4. private int quantity;
    5. private int yearToDate;
    6. private int importantValue1;
    7. private int importantValue2;
    8. private int importantValue3;
    9. }

接下来,加入一个构造函数

  1. public Gamma(Account _account, int inputVal, int quantity, int yearToDate) {
  2. this._account = _account;
  3. this.inputVal = inputVal;
  4. this.quantity = quantity;
  5. this.yearToDate = yearToDate;
  6. }

现在把原本的函数搬到compute()了。函数中任何调用A从count类的地方,必须改用_acount字段。

  1. int compute() {
  2. importantValue1 = (inputVal * quantity) + _account.dalta();
  3. importantValue2 = (inputVal * quantity) + 100;
  4. if ((yearToDate - importantValue1) > 100) {
  5. importantValue2 -= 20;
  6. }
  7. importantValue3 = importantValue2 * 7;
  8. return importantValue3 - 2 * importantValue1;
  9. }
  1. 然后,修改旧函数。让它将工作委托给刚完成这个函数对象
  1. int gamma(int inputVal, int quantity, int yearToDate) {
  2. return new Gamma(this,inputVal,quantity,yearToDate).compute();
  3. }
  1. <br />这就是本项重构的基本原则,它带来的好处是:现在我们可以轻松地对compute()函数采取`Extract Method` ,不必担心参数传递问题。
  1. int compute() {
  2. importantValue1 = (inputVal * quantity) + _account.dalta();
  3. importantValue2 = (inputVal * quantity) + 100;
  4. importantThing();
  5. importantValue3 = importantValue2 * 7;
  6. return importantValue3 - 2 * importantValue1;
  7. }
  8. private void importantThing() {
  9. if ((yearToDate - importantValue1) > 100) {
  10. importantValue2 -= 20;
  11. }
  12. }

6.9 Substitute Algorithm(替换算法)

将函数本体替换为另一个算法

  1. String foundPerson(String[] people) {
  2. for (int i = 0; i < people.length; i++) {
  3. if (people[i].equals("Don")) {
  4. return "Don";
  5. }
  6. if (people[i].equals("John")) {
  7. return "John";
  8. }
  9. if (people[i].equals("Kent")) {
  10. return "Kent";
  11. }
  12. }
  13. return "";
  14. }
  1. 变成
  1. String foundPersonNew(String[] people) {
  2. List<String> candidates = Arrays.asList(new String[]{"Don", "John", "Kent"});
  3. for (String person : people) {
  4. if (candidates.contains(person)) {
  5. return person;
  6. }
  7. }
  8. return "";
  9. }

动机

解决问题有好几种方法,其中某些方法比另一个简单。如果发现做一件事可以有更清晰的方式,就应该以比较清晰的方式取代复杂的方式。“重构”可以把一些复杂东西分解为较为简单的小块,但有时你必须壮士断腕,删除掉整个算法,代之较简单的算法。
使用这项重构手法之前,清先确定自己已经尽可能分解了原先函数。替换一个巨大而复杂的算法是非常困难的,只有先将它分解为较为简单的小型函数,然后才有把握进行算法替换工作。