重构的第一步

        如果你要给程序添加一个特性,但发现代码因缺乏良好的结构而不易于进行更改,那就先重构那个程序,使其比较容易添加特性,然后再添加该特性。

重构的第一个例子

//plays.json...
{
  "hamlet": {"name": "Hamlet", "type": "tragedy"},
  "as-like": {"name": "As You Like It", "type": "comedy"},
  "othello": {"name": "Othello", "type": "tragedy"},
}
//invoices.json...
[
  {
      "customer": "BigCo",
      "performances": [
          {"playID": "Hamlet", "audience": 55},
          {"playID": "as-like", "audience": 35},
          {"playID": "othello", "audience": 40},
      ]
  }
]
// 下面函数用于打印账单详情
function statement (invoice, plays) {
  let totalAmount = 0;
  let volumeCredits = 0;
  let result = `Statement for ${invoice.customer}\n`;
  const format = new Intl.NumberFormat("en-US", { style: "currency", currency: "USD", minimumFractionDigits: 2 }).format;
  for (let perf of invoice.performances) {
      const play = plays[perf.playID];
      let thisAmount = 0;

      switch (play.type) {
          case "tragedy":
              thisAmount = 40000;
              if (perf.audience > 30) {
                  thisAmount += 1000 * (perf.audience - 30);
              }
              break;
          case "comedy":
              thisAmount = 30000;
              if (perf.audience > 20) {
                  thisAmount += 10000 + 500 * (perf.audience - 20);
              }
              thisAmount += 300 * perf.audience;
              break;
          default:
              throw new Error(`unknow type: ${play.type}`);
      }
      // add volume credits
      volumeCredits += Math.max(perf.audience - 30, 0);
      // add extra credit for every ten comedy attendees
      if ("comedy" === play.type) {
          volumeCredits += Math.floor(perf.audience / 5);
      }
      // print line for this order
      result += `${play.name}: ${format(thisAmount/100)} (${perf.audience} seats)\n`;
      totalAmount += thisAmount;
  }
  result += `Amount owed is ${format(totalAmount/100)}\n`;
  result += `You earned ${volumeCredits} credits\n`;
  return result;
}
// 使用上面的json数据文件测试输出

        这段程序设计如何?代码组织不清晰,但还在可忍受的限度内。如果在一个更大规模的程序中,把所有代码放到一个函数里就很难理解了。

        在这个例子中,如果用户希望对系统做几个修改:

        1、以HTML格式输出详单

        2、演员们尝试在表演类型上做更多突破,无论是历史剧、田园剧、田园戏剧、历史悲剧等,这对戏剧场次的计费方式、积分的计算方式都有影响

        我们需要强调,是需求的变化使重构变得必要。如果一段代码能正常工作且不会被修改,那么完全可以不去重构它。能改进当然更好,但是确实有人需要理解它的工作原理,并且觉得理解起来很费劲,那么就需要改进一下代码了。

第一步

        进行重构的第一个步骤永远相同:我得确保即将修改的代码拥有一组可靠的测试。重构前,先检查自己是否有一套可靠的测试集。这些测试必须有自我检查能力。

        进行重构时,我们需要依赖测试。测试是bug检测器,它能保护我们不被自己犯的错误所困扰。构筑测试体系对重构来说实在太重要了。测试我们在后面章节会详细讨论。

分解statement函数

        这块代码是在计算一场戏剧演出的费用,我们先对它进行分解,这种重构手法叫做提炼函数,比如命名为amountFor(performance)。

        每次想将一块代码抽取成一个函数时,我们都需要遵循一个标准流程,最大程度减少犯错的可能。首先,检查哪些变量会离开原本的作用域。此示例中的perf、play、thisAmount,前两个变量会被提炼后的函数使用,但不会被修改,可以以参数方式传递进来,thisAmount会被修改,可以直接将它从函数中直接返回。

function amountFor (aPerformance, play) {
  let result= 0;
  switch (play.type) {
      case "tragedy":
          result= 40000;
          if (aPerformance.audience > 30) {
              result+= 1000 * (aPerformance.audience - 30);
          }
          break;
      case "comedy":
          result= 30000;
          if (aPerformance.audience > 20) {
              result+= 10000 + 500 * (aPerformance.audience - 20);
          }
          result+= 300 * aPerformance.audience;
          break;
      default:
          throw new Error(`unknow type: ${play.type}`);
  }
  return result;
}

        重构技术就是以微小的步伐修改程序,如果你犯下错误,很容易便可发现它,每次修改后就运行测试。如果改动太多了,犯错时就可能陷入麻烦的调试,耗费大把实践。

移除play变量

        观察amountFor函数参数,aPerformance是从循环变量中来,每次循环都会改变,但play是从计算得到的,因此没必要将它作为参数传入,我们可以在amountFor函数中计算得到。一般太多的具有局部作用域的临时变量会使提炼函数变得更加复杂,这种重构手法是以查询取代临时变量

        删除play变量定义,由函数playFor返回,去掉amountFor函数参数play...

function playFor(aPerformance) {
  return plays[aPerformance.playID]
}

提炼计算观众量积分的逻辑

        移除play变量的好处,即移除一个局部作用域变量,提炼这个逻辑更简单一些。还有两个变量:perf可以轻易作为参数传入,但volumeCredits是一个累加变量,循环迭代会更新,最简单方式是将整块逻辑提炼到新函数:

function volumeCreditsFor(perf) {
  let volumeCredits = 0;
  volumeCredits += Math.max(perf.audience - 30, 0);
  if ("comedy" === perf.play.type) {
      volumeCredits += Math.floor(perf.audience / 5);
  }
  return volumeCredits;
}

移除format变量

        这里有个典型的“将函数赋值给临时变量”的场景,更好的方式是将其替换为一个明确声明的函数。

function usd(aNumber) {
  return new Intl.NumberFormat("en-US", 
          { style: "currency", currency: "USD",
           minimumFractionDigits: 2 }).format(aNumber/100);
}

移除观众量积分总和

        在移除volumeCredits的过程中,主要分为下面4步:

                1、使用拆分循环分离出累加过程;

                2、使用移动语句将累加变量的声明与累加过程集中到一起;

                3、使用提炼函数提炼出计算总数的函数;

                4、使用内联变量完全移除中间变量。

function totalAmount() {
  let result = 0;
  for (let perf of invoice.performances) {
      result += amountFor(perf);
  }
  return result;
}
function totalVolumeCredits() {
  let result = 0;
  for (let perf of invoice.performances) {
      result += volumeCreditsFor(perf);
  }
  return result;
}

拆分计算阶段与格式化阶段

        到目前为止,我们重构主要是为原函数添加足够的结构,看清它的逻辑结构。要实现复用有许多方法,拆分阶段就是其中一种。现在将逻辑分成两部分:一部分计算详单所需的数据,另一部分将数据渲染成文本或HTML。然后分离到两个文件中:

// statement.js...
import createStatementData from "./createStatementData";
function statement (invoice, plays) {
  return renderPlainText(createStatementData(invoice, plays));

}
function renderPlainText (data) {
  let result = `Statement for ${data.customer}\n`;
  for (let perf of data.performances) {
    result += `${perf.play.name}: ${usd(perf.amount)} (${perf.audience} seats)\n`;
  }
  result += `Amount owed is ${usd(data.totalAmount)}\n`;
  result += `You earned ${data.totalVolumeCredites} credits\n`;
  return result;
}
function htmlStatement(invoice, plays) {
  return renderHtml(createStatementData(invoice, plays));
}
function renderHtml(data) {
  let result = `<h1>Statement for ${data.customer}</h1>\n`;
  result += '<table>\n';
  result += '<tr><th>play</th><th>seates</th><th>costs</th></tr>';
  for (let perf of data.performances) {
    result += ` <tr><td>${perf.play.name}</td><td>${perf.audiance}</td>`;
    result += `<td>${usd(perf.amount)}</td></tr>`
  }
  result += '</table>\n';
  result += `<p>Amount owed is <em>${usd(data.totalAmount)}</em></p>\n`;
  result += `<p>You earned <em>${data.totalVolumeCredites}</em> credits</p>\n`;
  return result;
}
function usd(aNumber) {
  return new Intl.NumberFormat("en-US", 
          { style: "currency", currency: "USD",
           minimumFractionDigits: 2 }).format(aNumber/100);
}
// createStatementData.js...
export default function createStatementData(invoice, plays) {
  const result = {};
  result.customer = invoice.customer;
  result.performances = invoice.performances.map(enrichPerformance);
  result.totalAmount = totalAmount(result);
  result.totalVolumeCredits = totalVolumeCredits(result);
  return result;
}
function enrichPerformance(aPerformance) {
  const result = Object.assign({}, aPerformance);
  result.play = playFor(result);
  result.amount = amountFor(result);
  result.volumeCredits = volumeCreditsFor(result);
  return result;
}
function playFor(aPerformance) {
  return plays[aPerformance.playID]
}
function amountFor (aPerformance) {
  let result = 0;
  switch (aPerformance.play.type) {
      case "tragedy":
        result = 40000;
          if (aPerformance.audience > 30) {
            result += 1000 * (aPerformance.audience - 30);
          }
          break;
      case "comedy":
        result = 30000;
          if (aPerformance.audience > 20) {
            result += 10000 + 500 * (aPerformance.audience - 20);
          }
          result += 300 * aPerformance.audience;
          break;
      default:
          throw new Error(`unknow type: ${aPerformance.play.type}`);
  }
  return result;
}
function volumeCreditsFor(aPerformance) {
  let result = 0;
  result += Math.max(aPerformance.audience - 30, 0);
  if ("comedy" === aPerformance.play.type) {
    result += Math.floor(aPerformance.audience / 5);
  }
  return result;
}
function totalAmount() {
  // 以管道取代循环
  // let result = 0;
  // for (let perf of invoice.performances) {
  //     result += amountFor(perf);
  // }
  // return result;
  return data.performances.reduce((total, p) => total + p.amount, 0)
}
function totalVolumeCredits(data) {
  // let result = 0;
  // for (let perf of invoice.performances) {
  //     result += volumeCreditsFor(perf);
  // }
  // return result;
  return data.performances.reduce((total, p) => total + p.volumeCredits, 0)
}

        虽然代码行数由开始重构时的40多行增加到了70多行,主要是将代码抽取到函数里带来的额外包装成本。代码行数虽然增加,但重构也带来了代码可读性的提高,后面的扩展也容易了许多。

使用多态计算器来提供数据

        再次调整代码结构,将不同的戏剧种类的计算各自集中到了一处地方。如果大多数修改都涉及特定类型的计算,像这样按类型进行分离就很有意义。当添加新剧种时,只需添加一个子类,并在创建函数中返回它。

export default function createStatementData(invoice, plays) {
  const result = {};
  result.customer = invoice.customer;
  result.performances = invoice.performances.map(enrichPerformance);
  result.totalAmount = totalAmount(result);
  result.totalVolumeCredits = totalVolumeCredits(result);
  return result;
}
function enrichPerformance(aPerformance) {
  const calculator = createPerformanceCalculator(aPerformance, playFor(aPerformance));
  const result = Object.assign({}, aPerformance);
  result.play = calculator.play;
  result.amount = calculator.amount;
  result.volumeCredits = calculator.volumeCredits;
  return result;
}
function playFor(aPerformance) {
  return plays[aPerformance.playID]
}
// function amountFor (aPerformance) {
//   ...
// }
// function volumeCreditsFor(aPerformance) {
//   ...
// }
function totalAmount() {
  return data.performances.reduce((total, p) => total + p.amount, 0)
}
function totalVolumeCredits(data) {
  return data.performances.reduce((total, p) => total + p.volumeCredits, 0)
}
function createPerformanceCalculator(aPerformance, aPlay) {
  switch (aPlay.type) {
    case 'tragedy': return new TragedyCalculator(aPerformance, aPlay);
    case 'comedy': return new ComedyCalculator(aPerformance, aPlay);
    default:
      throw new Error(`unknow type: ${aPlay.type}`);
  }
}
class PerformanceCalculator {
  constructor(aPerformance, aPlay) {
    this.performance = aPerformance;
    this.paly = aPlay;
  }
  get amount() {
    throw new Error('subclass responsibility');
  }
  get volumeCredits() {
    return Math.max(this.performance.audience - 30, 0);
  }
}
class TragedyCalculator extends PerformanceCalculator {
  get amount() {
    let result = 40000;
    if (this.performance.audience > 30) {
      result += 1000 * (this.performanceaudience.audience - 30);
    }
    return result;
  }
}
class ComedyCalculator extends PerformanceCalculator {
  get amount() {
    let result = 30000;
    if (this.performance.audience.audience > 20) {
        thisAmount += 10000 + 500 * (this.performance.audience - 20);
    }
    result += 300 * this.performance.audience;
    return result;
  }
  get volumeCredits() {
    return super.volumeCredits + Math.floor(this.performance.audience / 5);
  }
}

结语

        通过这个简单的例子,或许能让我们对“重构怎么做”有一点感觉。示例中我只是选取了部分关键步骤的代码,如果需要了解更详细的过程可以阅读本书。这段重构有3个较为重要的节点:将原函数分解为一套嵌套的函数、应用拆分阶段分离计算逻辑和输出格式化逻辑、计算器引入多态性来处理计算逻辑。每一步都添加了更多的结构,以便能更好的表达代码的意图。

        好代码的检验标准就是人们是否能轻而易举地修改它。。。

下一章:代码的坏味道

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

~卷心菜~

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值