曾用名:令函数携带参数(Parameterize Method)

function tenPercentRaise(aPerson) {
  aPerson.salary = aPerson.salary.multiply(1.1);
}
function fivePercentRaise(aPerson) {
  aPerson.salary = aPerson.salary.multiply(1.05);
}
 
function raise(aPerson, factor) {
  aPerson.salary = aPerson.salary.multiply(1 + factor);
}

动机

如果我发现两个函数逻辑非常相似,只有一些字面量值不同,可以将其合并成一个函数,以参数的形式传入不同的值,从而消除重复。这个重构可以使函数更有用,因为重构后的函数还可以用于处理其他的值。

做法

从一组相似的函数中选择一个。

运用改变函数声明(124),把需要作为参数传入的字面量添加到参数列表中。

修改该函数所有的调用处,使其在调用时传入该字面量值。

测试。

修改函数体,令其使用新传入的参数。每使用一个新参数都要测试。

对于其他与之相似的函数,逐一将其调用处改为调用已经参数化的函数。每次修改后都要测试。

如果第一个函数经过参数化以后不能直接替代另一个与之相似的函数,就先对参数化之后的函数做必要的调整,再做替换。

范例

下面是一个显而易见的例子:

function tenPercentRaise(aPerson) {
  aPerson.salary = aPerson.salary.multiply(1.1);
}
function fivePercentRaise(aPerson) {
  aPerson.salary = aPerson.salary.multiply(1.05);
}

很明显我可以用下面这个函数来替换上面两个:

function raise(aPerson, factor) {
  aPerson.salary = aPerson.salary.multiply(1 + factor);
}

情况可能比这个更复杂一些。例如下列代码:

function baseCharge(usage) {
 if (usage < 0) return usd(0);
 const amount =
    bottomBand(usage) * 0.03
    + middleBand(usage) * 0.05
    + topBand(usage) * 0.07;
 return usd(amount);
}
 
function bottomBand(usage) {
 return Math.min(usage, 100);
}
 
function middleBand(usage) {
 return usage > 100 ? Math.min(usage, 200) - 100 : 0;
}
 
function topBand(usage) {
 return usage > 200 ? usage - 200 : 0;
}

这几个函数中的逻辑明显很相似,但是不是相似到足以支撑一个参数化的计算“计费档次”(band)的函数?这次就不像前面第一个例子那样一目了然了。

在尝试对几个相关的函数做参数化操作时,我会先从中挑选一个,在上面添加参数,同时留意其他几种情况。在类似这样处理“范围”的情况下,通常从位于中间的范围开始着手较好。所以我首先选择了 middleBand 函数来添加参数,然后调整其他的调用者来适应它。

middleBand 使用了两个字面量值,即 100 和 200,分别代表“中间档次”的下界和上界。我首先用改变函数声明(124)加上这两个参数,同时顺手给函数改个名,使其更好地表述参数化之后的含义。

function withinBand(usage, bottom, top) {
 return usage > 100 ? Math.min(usage, 200) - 100 : 0;
}
 
function baseCharge(usage) {
 if (usage < 0) return usd(0);
 const amount =
    bottomBand(usage) * 0.03
    + withinBand(usage, 100, 200) * 0.05
    + topBand(usage) * 0.07;
 return usd(amount);
}

在函数体内部,把一个字面量改为使用新传入的参数:

function withinBand(usage, bottom, top) {
  return usage & gt;
  bottom ? Math.min(usage, 200) - bottom : 0;
}

然后是另一个:

function withinBand(usage, bottom, top) {
  return usage & gt;
  bottom ? Math.min(usage, top) - bottom : 0;
}

对于原本调用 bottomBand 函数的地方,我将其改为调用参数化了的新函数。

function baseCharge(usage) {
 if (usage < 0) return usd(0);
 const amount =
    withinBand(usage, 0, 100) * 0.03
    + withinBand(usage, 100, 200) * 0.05
    + topBand(usage) * 0.07;
 return usd(amount);
}
 
function bottomBand(usage) {
 return Math.min(usage, 100);
}

为了替换对 topBand 的调用,我就得用代表“无穷大”的 Infinity 作为这个范围的上界。

function baseCharge(usage) {
 if (usage < 0) return usd(0);
 const amount =
    withinBand(usage, 0, 100) * 0.03
    + withinBand(usage, 100, 200) * 0.05
    + withinBand(usage, 200, Infinity) * 0.07;
 return usd(amount);
}
 
function topBand(usage) {
  return usage > 200 ? usage - 200 : 0;
}

照现在的逻辑,baseCharge 一开始的卫语句已经可以去掉了。不过,尽管这条语句已经失去了逻辑上的必要性,我还是愿意把它留在原地,因为它阐明了“传入的 usage 参数为负数”这种情况是如何处理的。