反向重构:以命令取代函数(337)

class ChargeCalculator {
  constructor(customer, usage) {
    this._customer = customer;
    this._usage = usage;
  }
  execute() {
    return this._customer.rate * this._usage;
  }
}
 
function charge(customer, usage) {
  return customer.rate * usage;
}

动机

命令对象为处理复杂计算提供了强大的机制。借助命令对象,可以轻松地将原本复杂的函数拆解为多个方法,彼此之间通过字段共享状态;拆解后的方法可以分别调用;开始调用之前的数据状态也可以逐步构建。但这种强大是有代价的。大多数时候,我只是想调用一个函数,让它完成自己的工作就好。如果这个函数不是太复杂,那么命令对象可能显得费而不惠,我就应该考虑将其变回普通的函数。

做法

运用提炼函数(106),把“创建并执行命令对象”的代码单独提炼到一个函数中。

这一步会新建一个函数,最终这个函数会取代现在的命令对象。

对命令对象在执行阶段用到的函数,逐一使用内联函数(115)。

如果被调用的函数有返回值,请先对调用处使用提炼变量(119),然后再使用内联函数(115)。

使用改变函数声明(124),把构造函数的参数转移到执行函数。

对于所有的字段,在执行函数中找到引用它们的地方,并改为使用参数。每次修改后都要测试。

把“调用构造函数”和“调用执行函数”两步都内联到调用方(也就是最终要替换命令对象的那个函数)。

测试。

移除死代码(237)把命令类消去。

范例

假设我有一个很小的命令对象。

class ChargeCalculator {
  constructor(customer, usage, provider) {
    this._customer = customer;
    this._usage = usage;
    this._provider = provider;
  }
  get baseCharge() {
    return this._customer.baseRate * this._usage;
  }
  get charge() {
    return this.baseCharge + this._provider.connectionCharge;
  }
}

使用方的代码如下。

调用方…

monthCharge = new ChargeCalculator(customer, usage, provider).charge;

命令类足够小、足够简单,变成函数更合适。

首先,我用提炼函数(106)把命令对象的创建与调用过程包装到一个函数中。

调用方…

monthCharge = charge(customer, usage, provider);

顶层作用域…

function charge(customer, usage, provider) {
  return new ChargeCalculator(customer, usage, provider).charge;
}

接下来要考虑如何处理支持函数(也就是这里的 baseCharge 函数)。对于有返回值的函数,我一般会先用提炼变量(119)把返回值提炼出来。

class ChargeCalculator…

get baseCharge() {
  return this._customer.baseRate * this._usage;
}
get charge() {
  const baseCharge = this.baseCharge;
  return baseCharge + this._provider.connectionCharge;
}

然后对支持函数使用内联函数(115)。

class ChargeCalculator…

get charge() {
  const baseCharge = this._customer.baseRate * this._usage;
  return baseCharge + this._provider.connectionCharge;
}

现在所有逻辑处理都集中到一个函数了,下一步是把构造函数传入的数据移到主函数。首先用改变函数声明(124)把构造函数的参数逐一添加到 charge 函数上。

class ChargeCalculator…

constructor (customer, usage, provider){
 this._customer = customer;
 this._usage = usage;
 this._provider = provider;
}
 
charge(customer, usage, provider) {
 const baseCharge = this._customer.baseRate * this._usage;
 return baseCharge + this._provider.connectionCharge;
}

顶层作用域…

function charge(customer, usage, provider) {
  return new ChargeCalculator(customer, usage, provider).charge(
    customer,
    usage,
    provider
  );
}

然后修改 charge 函数的实现,改为使用传入的参数。这个修改可以小步进行,每次使用一个参数。

class ChargeCalculator…

constructor (customer, usage, provider){
 this._customer = customer;
 this._usage = usage;
 this._provider = provider;
}
 
charge(customer, usage, provider) {
 const baseCharge = customer.baseRate * this._usage;
 return baseCharge + this._provider.connectionCharge;
}

构造函数中对 this._customer 字段的赋值不删除也没关系,因为反正没人使用这个字段。但我更愿意去掉这条赋值语句,因为去掉它以后,如果在函数实现中漏掉了一处对字段的使用没有修改,测试就会失败。(如果我真的犯了这个错误而测试没有失败,我就应该考虑增加测试了。)

其他参数也如法炮制,直到 charge 函数不再使用任何字段:

class ChargeCalculator…

charge(customer, usage, provider) {
  const baseCharge = customer.baseRate * usage;
  return baseCharge + provider.connectionCharge;
}

现在我就可以把所有逻辑都内联到顶层的 charge 函数中。这是内联函数(115)的一种特殊情况,我需要把构造函数和执行函数一并内联。

顶层作用域…

function charge(customer, usage, provider) {
  const baseCharge = customer.baseRate * usage;
  return baseCharge + provider.connectionCharge;
}

现在命令类已经是死代码了,可以用移除死代码(237)给它一个体面的葬礼。