为了进一步研究,我需要引入一些代码。在本例中,是我在世纪之交编写的原始示例的JavaScript版本。

  1. function statement(customer, movies) {
  2. let totalAmount = 0;
  3. let frequentRenterPoints = 0;
  4. let result = `Rental Record for ${customer.name}\n`;
  5. for (let r of customer.rentals) {
  6. let movie = movies[r.movieID];
  7. let thisAmount = 0;
  8. // 确定每部电影的数量
  9. switch (movie.code) {
  10. case "regular":
  11. thisAmount = 2;
  12. if (r.days > 2) {
  13. thisAmount += (r.days - 2) * 1.5;
  14. }
  15. break;
  16. case "new":
  17. thisAmount = r.days * 3;
  18. break;
  19. case "childrens":
  20. thisAmount = 1.5;
  21. if (r.days > 3) {
  22. thisAmount += (r.days - 3) * 1.5;
  23. }
  24. break;
  25. }
  26. //增加频繁租借点
  27. frequentRenterPoints++;
  28. // add bonus for a two day new release rental
  29. if(movie.code === "new" && r.days > 2) frequentRenterPoints++;
  30. //print figures for this rental
  31. result += `\t${movie.title}\t${thisAmount}\n` ;
  32. totalAmount += thisAmount;
  33. }
  34. // add footer lines
  35. result += `Amount owed is ${totalAmount}\n`;
  36. result += `You earned ${frequentRenterPoints} frequent renter points\n`;
  37. return result;
  38. }

我这里用的是ES6。代码对两个数据结构进行操作,这两个数据结构都是json记录的列表。客户记录是这样的:

{
  "name": "martin",
  "rentals": [
    {"movieID": "F001", "days": 3},
    {"movieID": "F002", "days": 1},
  ]
}

电影结构是这样的:

{
  "F001": {"title": "Ran",                     "code": "regular"},
  "F002": {"title": "Trois Couleurs: Bleu",     "code": "regular"},
  // etc
}

在最初的书中,电影只是作为java对象结构中的对象出现。对于本文,我更喜欢将json结构作为参数传入。我将假设使用某种全局查找,例如存储库,不适合这个应用程序。

statement方法打印租借语句的简单文本输出:

Rental Record for martin
  Ran 3.5
  Trois Couleurs: Bleu 2
Amount owed is 5.5
You earned 2 frequent renter points

即使按照示例代码的标准,这个输出也是粗糙的。我甚至都懒得把这些数字格式化一下吗?但是,请记住,这本书是在String之前使用Java 1.1编写的。格式被添加到语言中。这也许能部分原谅我的懒惰。

语句函数是smell Long方法的一个例子。它的大小足以让我怀疑。但是仅仅因为代码闻起来不好并不足以成为重构它的理由。分解不良的代码是一个问题,因为它很难理解。很难理解的代码很难修改,无论是添加新特性还是调试。因此,如果您不需要阅读和理解某些代码,那么它糟糕的结构不会对您造成伤害,您可以暂时不去管它。所以为了激发我们对这段代码的兴趣,我们需要一个改变它的理由。正如我在书中使用的,我们的原因是编写语句方法的HTML版本,它会打印出类似这样的东西。

<h1>Rental Record for <em>martin</em></h1>
<table>
  <tr><td>Ran</td><td>3.5</td></tr>
  <tr><td>Trois Couleurs: Bleu</td><td>2</td></tr>
</table>
<p>Amount owed is <em>5.5</em></p>
<p>You earned <em>2</em> frequent renter points</p>

正如我前面所指出的,在本文中,我将探索重构这段代码的多种方法,以便更容易地添加额外的输出呈现。所有这些都有相同的开始:将单个方法分解为一组函数来捕获逻辑的不同部分。一旦我完成了这个分解,我将探索四种不同的方法来安排这些函数来支持替代渲染。
image.png

1. 分解成几个函数

每当我处理这样一个过长的函数时,我的第一个想法是使用 Extract function 来查找逻辑代码块并将它们转换成自己的函数。首先吸引我的是switch语句。

 function statement(customer, movies) {
    let totalAmount = 0;
    let frequentRenterPoints = 0;
    let result = `Rental Record for ${customer.name}\n`;
    for (let r of customer.rentals) {
      let movie = movies[r.movieID];
      let thisAmount = 0;

      // determine amount for each movie
      switch (movie.code) {
        case "regular":
          thisAmount = 2;
          if (r.days > 2) {
            thisAmount += (r.days - 2) * 1.5;
          }
          break;
        case "new":
          thisAmount = r.days * 3;
          break;
        case "childrens":
          thisAmount = 1.5;
          if (r.days > 3) {
            thisAmount += (r.days - 3) * 1.5;
          }
          break;
      }

      //add frequent renter points
      frequentRenterPoints++;
      // add bonus for a two day new release rental
      if(movie.code === "new" && r.days > 2) frequentRenterPoints++;

      //print figures for this rental
      result += `\t${movie.title}\t${thisAmount}\n` ;
      totalAmount += thisAmount;
    }
    // add footer lines
    result += `Amount owed is ${totalAmount}\n`;
    result += `You earned ${frequentRenterPoints} frequent renter points\n`;

    return result;
  }

我的IDE (IntelliJ)为我提供了自动进行重构的功能,但它做得并不正确——它的JavaScript功能不如Java重构那么可靠或成熟。所以我用手工的方式来做,包括查看候选提取所使用的数据。这里有三位数据:

  • 这个值是由提取的代码计算的值。我可以在函数中初始化它并在最后返回它
  • r是循环中检查的租金,我可以把它作为参数传递进来。
  • 电影是出租的电影,它是之前制作的临时影片。像这样的临时变量通常会在重构过程代码时产生阻碍,所以我更喜欢先使用Replace Temp with Query将它们转换成一个函数,以便在任何提取的代码中调用。

在用查询替换临时变量之后,代码如下所示。

function statement(customer, movies) {
    let totalAmount = 0;
    let frequentRenterPoints = 0;
    let result = `Rental Record for ${customer.name}\n`;
    for (let r of customer.rentals) {
      let thisAmount = 0;

      // determine amount for each movie
      switch (movieFor(r).code) {
        case "regular":
          thisAmount = 2;
          if (r.days > 2) {
            thisAmount += (r.days - 2) * 1.5;
          }
          break;
        case "new":
          thisAmount = r.days * 3;
          break;
        case "childrens":
          thisAmount = 1.5;
          if (r.days > 3) {
            thisAmount += (r.days - 3) * 1.5;
          }
          break;
      }

      //add frequent renter points
      frequentRenterPoints++;
      // add bonus for a two day new release rental
      if(movieFor(r).code === "new" && r.days > 2) frequentRenterPoints++;

      //print figures for this rental
      result += `\t${movieFor(r).title}\t${thisAmount}\n` ;
      totalAmount += thisAmount;
    }
    // add footer lines
    result += `Amount owed is ${totalAmount}\n`;
    result += `You earned ${frequentRenterPoints} frequent renter points\n`;

    return result;

    function movieFor(rental) {return movies[rental.movieID];}
  }

现在我提取switch语句。

  function statement(customer, movies) {
    let totalAmount = 0;
    let frequentRenterPoints = 0;
    let result = `Rental Record for ${customer.name}\n`;

    for (let r of customer.rentals) {
      const thisAmount = amountFor(r);

      //add frequent renter points
      frequentRenterPoints++;
      // add bonus for a two day new release rental
      if(movieFor(r).code === "new" && r.days > 2) frequentRenterPoints++;

      //print figures for this rental
      result += `\t${movieFor(r).title}\t${thisAmount}\n` ;
      totalAmount += thisAmount;
    }
    // add footer lines
    result += `Amount owed is ${totalAmount}\n`;
    result += `You earned ${frequentRenterPoints} frequent renter points\n`;

    return result;

    function movieFor(rental) {return movies[rental.movieID];}

    function amountFor(r) {
      let thisAmount = 0;

      // determine amount for each movie
      switch (movieFor(r).code) {
        case "regular":
          thisAmount = 2;
          if (r.days > 2) {
            thisAmount += (r.days - 2) * 1.5;
          }
          break;
        case "new":
          thisAmount = r.days * 3;
          break;
        case "childrens":
          thisAmount = 1.5;
          if (r.days > 3) {
            thisAmount += (r.days - 3) * 1.5;
          }
          break;
      }
      return thisAmount;
    }
  }

现在我将注意力转移到计算频繁租户点上。我可以对它的代码进行类似的提取

function statement(customer, movies) {
  let totalAmount = 0;
  let frequentRenterPoints = 0;
  let result = `Rental Record for ${customer.name}\n`;
  for (let r of customer.rentals) {
    const thisAmount = amountFor(r);
    frequentRenterPointsFor(r);

    //print figures for this rental
    result += `\t${movieFor(r).title}\t${thisAmount}\n` ;
    totalAmount += thisAmount;
  }
  // add footer lines
  result += `Amount owed is ${totalAmount}\n`;
  result += `You earned ${frequentRenterPoints} frequent renter points\n`;

  return result;
…

  function frequentRenterPointsFor(r) {
   //add frequent renter points
    frequentRenterPoints++;
    // add bonus for a two day new release rental
    if (movieFor(r).code === "new" && r.days > 2) frequentRenterPoints++;
  }

尽管我提取了这个函数,但我不喜欢它通过更新父变量作用域来工作的方式。这些副作用使得代码很难理解,所以我对其进行了修改,使其在主体中没有副作用。

function statement(customer, movies) {
  let totalAmount = 0;
  let frequentRenterPoints = 0;
  let result = `Rental Record for ${customer.name}\n`;
  for (let r of customer.rentals) {
    const thisAmount = amountFor(r);
    frequentRenterPoints += frequentRenterPointsFor(r);

    //print figures for this rental
    result += `\t${movieFor(r).title}\t${thisAmount}\n` ;
    totalAmount += thisAmount;
  }
  // add footer lines
  result += `Amount owed is ${totalAmount}\n`;
  result += `You earned ${frequentRenterPoints} frequent renter points\n`;

  return result;
…

  function frequentRenterPointsFor(r) {
    let result = 1;
    if (movieFor(r).code === "new" && r.days > 2) result++;
    return result;
  }

在我理解这两个提取的函数的同时,我想借此机会对它们进行一些清理。

function amountFor(rental) {
    let result = 0;
    switch (movieFor(rental).code) {
      case "regular":
        result = 2;
        if (rental.days > 2) {
          result += (rental.days - 2) * 1.5;
        }
        return result;
      case "new":
        result = rental.days * 3;
        return result;
      case "childrens":
        result = 1.5;
        if (rental.days > 3) {
          result += (rental.days - 3) * 1.5;
        }
        return result;
    }
    return result;
  }
  function frequentRenterPointsFor(rental) {
    return (movieFor(rental).code === "new" && rental.days > 2) ? 2 : 1;
   }