反向重构:以命令取代函数(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)给它一个体面的葬礼。