重构,改善代码的既有设计——重构 API
重构 API
模块和函数是软件的骨肉,而 API 则是将骨肉连接起来的关节。易于理解和使用的 API 非常重要,但同时也很难获得。随着对软件理解的加深,我会学到如何改进 API,这时我需要对 API 进行重构。
将查询函数和修改函数分离
动机
如果某个函数只是提供一个值,没有任何看得到的副作用,那么这是一个很有价值的东西。我可以任意调用这个函数,也可以把调用动作搬到调用函数的其他地方。这种函数的测试也更容易。
明确表现出有副作用和无副作用两种函数之间的差异,是个很好的想法。下面是一条好规则:任何有返回值的函数,都不应该有看得到的副作用——命令与查询分离。这里看得到的副作用的有一种常见的优化方法:将查询所得结果缓存于某个字段,这样一来后续的重复查询就可以大大加快速度。
如果遇到一个既有返回值又有副作用的函数,我会试着将查询动作从修改动作中分离出来。
做法
- 复制整个函数,将其作为一个查询来命名。
- 新建的查询函数中去掉所有造成副作用的语句。
- 执行静态检查。
- 查找所有调用原函数的地方。如果调用处用到了该函数的返回值,就将其改为调用新建的查询函数,并在下面马上调用一次原函数,每次修改之后都要测试。
- 从原函数中去掉返回值。
- 测试。
范例
有这样一个函数:它会遍历一份恶棍名单,检查一群人(people)是否混进了恶棍。如果发现了恶棍,该函数会返回恶棍的名字,并拉响警报。如果人群中有多名恶棍,该函数也只汇报找出的第一名恶棍。
function alertForMiscreant(people) {
for (const p of people) {
if (p === "Don") {
setOffAlarms();
return "Don";
}
if (p === "John") {
setOffAlarms();
return "John";
}
}
return "";
}
首先我复制整个函数,用他的查询部分功能为其命名,然后在查询函数中去掉副作用。
function findMiscreant(people) {
for (const p of people) {
if (p === "Don") {
return "Don";
}
if (p === "John") {
return "John";
}
}
return "";
}
// 然后找到原函数的调用者,将其改为调用新建的查询函数,并在其后调用一次修改函数
const found = findMiscreant(people);
alertForMiscreant(people);
之后在原函数中去掉所有返回值
function alertForMiscreant(people) {
for (const p of people) {
if (p === "Don") {
setOffAlarms();
return;
}
if (p === "John") {
setOffAlarms();
return;
}
}
return;
}
// 可以看到原函数和修改函数中有大量重复代码,此时可以使用替代算法,让修改函数使用查询函数
function alertForMiscreant(people) {
if (findMiscreant(people) !== "") setOffAlarms();
}
函数参数化
动机
如果我发现两个函数逻辑非常相似,只有一些字面量的不容,可以将其合并成一个函数,一参数的形式传入不同的值,从而消除重复。这个重构可以使函数更有用,因为重构后的函数还可以用于处理其他的值。
做法
- 从一组相似的函数中选择一个。
- 运用改变函数声明,把需要作为参数传入的字面量添加到参数列表中。
- 修改该函数所有的调用处,使其在调用时传入该字面量的值。
- 测试。
- 修改函数体,令其使用新传入的参数。每使用一个新参数都要测试。
- 对于其他与之相似的函数,注意将其调用处改为调用已经参数化的函数。每次修改后都测试。
范例
下面是一个显而易见的例子:
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;
}
这几个函数中的逻辑明显很相似,但是不是相似到足以支撑一个参数化的计算‘计费档次’的函数?在类似处理“范围”的情况下,通常从位于中间的范围开始着手比较好。
// 首先从 middleBand 函数来进行调整,将其加上两个参数并给它改名
function withinBand(usage, bottom, top) {
return usage > bottom ? Math.min(usage, top) - bottom : 0
}
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 bookConcert(aCustomer, isPremium) {
if (isPremium) {
// logic for premium booking
} else {
// logic for regular booking
}
}
// 要预定一场高级音乐会,就得这样发起调用:
bookConcert(aCustomer, true);
// 标记参数也可能以枚举的形式出现:
bookConcert(aCustomer, CustomerType.PREMIUM);
// 或者是以字符串的形式出现
bookConcert(aCustomer, "premium");
我不喜欢标记参数,因为它们让人难以理解到底有哪些函数可以调用、应该怎么调用。使用这样的函数,我还得弄清楚有哪些可用的值。布尔型的标记尤其糟糕,应为他们不能清晰地传达其含义。如果明确用一个函数来完成一项单独的任务,其含义会清晰的多。
premiumBookConcert(aCustomer);
并非所有类似这样的参数都是标记参数,如果调用者传入的是程序中流动的数据,这样的参数不算标记参数:只有调用者直接传入字面量值,这才是标记参数。
做法
- 针对参数的每一种可能值,新建一个明确函数。
- 对于”用字面量值作为参数“的函数调用者,将其改为调用新建的明确函数。
范例
在浏览代码时,我发现多处代码在调用一个函数计算物流(shipment)的到货日期(delivery date)。一些调用代码类似这样:
// 一些调用代码类似这样
aShipment.deliveryDate = deliveryDate(anOrder, true);
// 一些调用代码则是这样,可以看到那个布尔值根本不知道是干嘛的
aShipment.deliveryDate = deliveryDate(anOrder, false);
// deliveryDate 函数主题如下
function deliveryDate(anOrder, isRush) {
if (isRush) {
let deliveryTime;
if (["MA", "CT"].includes(anOrder.deliveryState)) deliveryTime = 1;
else if (["NY", "NH"].includes(anOrder.deliveryState)) deliveryTime = 2;
else deliveryTime = 3;
return anOrder.placedOn.plusDays(1 + deliveryTime);
} else {
let deliveryTime;
if (["MA", "CT", "NY"].includes(anOrder.deliveryState)) deliveryTime = 2;
else if (["ME", "NH"].includes(anOrder.deliveryState)) deliveryTime = 3;
else deliveryTime = 4;
return anOrder.placedOn.plusDays(2 + deliveryTime);
}
}
原来调用者用这个布尔型的字面量来判断应该运行哪个分支的代码——典型的标记参数。此处最好是用明确函数的形式明确说出调用者的意图。对于这个例子,我可以使用分解条件表达式得到如下代码:
function deliveryDate(anOrder, isRush) {
if (isRush) return rushDeliveryDate(anOrder);
else return regulsrDeliveryDate(anOrder);
}
function rushDeliveryDate(anOrder) {
let deliveryTime;
if (["MA", "CT"].includes(anOrder.deliveryState)) deliveryTime = 1;
else if (["NY", "NH"].includes(anOrder.deliveryState)) deliveryTime = 2;
else deliveryTime = 3;
return anOrder.placedOn.plusDays(1 + deliveryTime);
}
function regularDeliveryDate(anOrder) {
let deliveryTime;
if (["MA", "CT", "NY"].includes(anOrder.deliveryState)) deliveryTime = 2;
else if (["ME", "NH"].includes(anOrder.deliveryState)) deliveryTime = 3;
else deliveryTime = 4;
return anOrder.placedOn.plusDays(2 + deliveryTime);
}
// 上面是两个函数能够更好地表达调用者的意图,现在我可以调用调用方代码了,调用代码
aShipment.deliveryDate = deliveryDate(anOrder, true);
aShipment.deliveryDate = rushDeliveryDate(anOrder, false);
如果所有调用 deliveryDate
函数的处理都像下面这样,那么我不会有任何意见
const isRush = determineIfRush(anOrder);
aShipment.deliveryDate = deliveryDate(anOrder, isRush);
直接拆分条件逻辑是实施本重构的好方法,但只有当”更具参数值做分发“的逻辑发生在函数最外层的时候,这一招才好用。函数内部也有可能以一种更纠结的方式使用标记参数,如下面这个版本的 deliveryDate
函数:
function deliveryDate(anOrder, isRush) {
let result
let deliveryTime
if (anOrder.deliveryState === 'MA' || anOrder.deliveryState === 'CT')
deliveryTime = isRush ? 1 : 2
else if (anOrder.deliveryState === 'NY' || anOrder.deliveryState === 'NH') {
deliveryTime = 2
if (anOrder.deliveryState === 'NH' && !isRush)
} else if (isRush) {
deliveryTime = 3
}
if (isRush) result = result.minusDays(1)
return result
}
在这种情况下,想把围绕 isRush
的分发逻辑剥离到顶层,需要的工作量可能会很大,所以我选择退而求其次,在 deliveryDate
之上添加两个函数:
function rushDeliveryDate(anOrder) {
return deliveryDate(anOrder, true);
}
function resularDeliveryDate(anOrder) {
return deliveryDate(anOrder, false);
}
这两个包装函数使用代码文本强行定义,同时我会限制原函数的可见性,让人一见便知不应直接使用这个函数。
保持对象完整
动机
如果我看见一个代码从一个记录结构中到处几个值,然后又把这几个值一起传递给一个函数,我会更愿意把则会那个记录传给这个函数,在函数内部到处所需的值。”传递整个记录“的方式能更好地应对变化。
也有是我不想采用本重构手法,因为我不想让被调函数依赖完整对象,尤其是两者不在同一个模块的时候。
从一个对象中抽取出几个值,单独对这几个值做某些逻辑操作,这是一种代码坏味道,通常标志着这段逻辑应该搬移到对象中。
做法
- 新建一个新函数,给它以期望中的参数列表。
- 在新函数体内调用旧函数,并把新参数映射到旧的参数列表。
- 执行静态检查。
- 逐一修改旧函数的调用者,令其使用新函数,每次修改之后执行测试。
- 给所有调用处都修改过来之后,使用内联函数把旧函数内联到新函数体内。
- 给新函数改名,从重构开始时的容易搜索的临时名字,改为使用就容易搜索的名字,同时修改所有调用处。
范例
我们想象一个室温监控系统,它负责记录房间每一天的最高温度和最低温度,然后将实际的温度范围与预先规定的温度控制计划相比较,如果当天温度不符合计划要求,就发出警告。
// 调用方...
const low = aRoom.daysTempRange.low
const high = aRoom.daysTempRange.high
if (!aPlan.withinRange(low, high))
alerts.push('room temperature went outside range')
class HeatingPlan...
withinRange(bottom, top) {
return (bottom >= this._temperatureRange.low) && (top <= this._temperatureRange.high)
}
其实我不必把“温度范围”的信息拆开来单独传递,只需将整个范围对象传递给 withinRange
函数即可。
// 首先我创建一个新函数调用现有的 withinRange 函数
class Heatingplan...
xxNEWwithinRange(aNumberRange) {
return this.withinRange(aNumberRange.low, aNumerRange.high)
}
// 找到调用现有函数的地方,将其改为调用新函数
// const low = aRoom.daysTempRange.low
// const high = aRoom.daysTemRange.high
if (!aPlan.xxNEWwithinRange(aRoom.daysTempRange))
alerts.push('room temperature went outside range')
之后使用内联函数将旧函数内联到新函数体内,同时将函数改名
class HeatingPlan...
withinRange(aNumberRange) {
return (aNumberRange.low >= this._temperatureRange.low) &&
(aNumberRange.high <= this._temperatureRange.high)
}
// 调用方
if (!aPlan.withinRange(aRoom.daysTempRange))
alerts.push('room temperature went outside range')
范例:换个方式创建新函数
在上面的示例中,我直接编写了新函数。大多数时候,这一步非常简单,也是创建新函数最容易的方式。不过有时还会用到另一种方式:可以完全通过重构手法的组合来得到新函数。
// 调用方...
const low = aRoom.daysTempRange.low;
const high = aRoom.daysTempRange.high;
if (!aPlan.withinRange(low, high))
alerts.push("room temperature went outside range");
我要先对代码做一些整理,以便使用提炼函数来创建新韩淑。目前的调用者代码还不具备可提炼的函数雏形,不过我可以先做几次提炼变量,使其轮廓显现出来。
// 调用方...
const low = aRoom.daysTempRange.low
const high = aRoom.daysTempRange.high
// 提炼变量
const isWithinRange = aPlan.withinRange(low, high)
if (!isWithinRange)
alerts.push('room temperature went outside range')
// 提炼输入参数
const tempRange = aRoom.daysTempRange
const low = tempRange.low
const high = tempRange.high
之后使用提炼函数创建新函数
// 调用方...
const tempRange = aRoom.daysTempRange
const isWithinRange = xxNEWwithinRange(aPlan, tempRange)
if (!isWithinRange)
alerts.push('room temperature went outside range')
function xxNEWwithinRange(aPlan, temRange) {
const low = tempRange.low
const high = tempRange.high
const isWithinRange = aPlan.withinRange(low, high)
return isWithinRange
}
// 由于旧函数属于另一个上下文 HeatingPlan 类,可以把新函数搬移过去
class HeatingPlan...
xxNEWwithinRange...
这种方式的好处在于:它完全是由其他重构手法组合而成。如果我使用的开发工具支持可靠的提炼和内联操作,用这种方式进行本重构会特别流畅。
以查询取代参数
动机
函数的参数列表应该总结该函数的可变性,标示处函数可能体现出行为差异的主要方式。和任何代码中的语句一样,参数列表应该避免重复,并且参数列表越短越容易理解。
如果调用函数时传入了一个值,二这个值由函数自己来获得也是同样的容易,这就是重复。这个本不必要的参数会增加调用者的难度,因为它不得不找出正确的参数值。
不适用一查询取代参数最常见的原因是,移除参数可能会给函数体增加不必要的依赖关系——迫使函数访问某个程序元素,而我原本不想让函数了解这个元素的存在。
如果想要去除的参数值只需要向另一个参数查询就能得到,这是使用以查询取代参数最安全的场景。
做法
- 如果有必要,使用提炼函数将参数的计算过程提炼到一个独立的函数中。
- 将函数体内引用该参数的地方改为调用新建的函数,每次修改后执行测试。
- 将全部替换完成后,使用改变函数声明将该参数去掉。
范例
考虑下列代码
class Order...
get finalPrice() {
const basePrice = this.quantity * this.itemPrice
let discountLevel
if (this.quantity > 100) discountLevel = 2
else discountLevel = 1
return this.discountedPrice(basePrice, discountLevel)
}
discountedPrice(basePrice, discountLevel) {
switch (discountLevel) {
case 1:
return basePrice * 0.95
case 2:
return basePrice * 0.9
}
}
此时可以用以查询取代临时变量将 discountLevel
提取出来。
class Order..
get finalPrice() {
const basePrice = this.quantity * this.itemPrice
return this.discountedPrice(basePrice, this.discountLevel)
}
get discountLevel() {
return (this.quantity > 100) ? 2 : 1
}
// 再把 discountedPrice 函数中用到这个参数的地方全都改为直接调用 discountLevel 函数
discountedPrice(basePrice, discountLevel) {
switch (this.discountLevel) {
case 1:
return basePrice * 0.95
case 2:
return basePrice * 0.9
}
}
然后把该参数从函数声明中移除
class Order..
get finalPrice() {
const basePrice = this.quantity * this.itemPrice
return this.discountedPrice(basePrice)
}
discountedPrice(basePrice) {
switch (this.discountLevel) {
case 1:
return basePrice * 0.95
case 2:
return basePrice * 0.9
}
}
以参数取代查询
动机
在浏览函数实现时,我有时会发现一些令人不快的引用关系,例如,引用一个全局变量,或是引用另一个我想要移除的元素。为了解决这些令人不快的引用,我需要将其替换为函数参数,从而将处理引用关系的责任转交给函数的调用者。
使用本重构的情况大多源于我想要改变代码的依赖关系——为了让函数不再依赖于某个元素,我把这个元素的值以参数形式传递给该函数。
如果一个函数具有相同的擦拭农户调用总是给出同样的结果,我们就说这个函数具有引用透明性。而如果一个函数使用了另一个元素,而后者不具有引用透明性,那么包含该元素的函数也就失去了引用透明性。此时只要把‘不具有引用透明性的元素‘变成参数引入,函数就能重获引用透明性。
有一个常见的模式是:在负责逻辑处的模块中只有纯函数,其外再包裹处理 I/O 和其他可变元素的逻辑代码。
做法
- 对执行查询操作的代码使用提炼变量,将其从函数体中分离出来。
- 现在函数体代码已经不再执行查询操作,对这部分代码使用提炼函数。
- 对原来的函数使用内联函数。
- 对新函数改名,改回原来函数的名字。
范例
有一个温度控制系统,用户可以从一个温控终端(thermostat)指定温度,但指定的目标温度必须在温度控制计划(heating plan)允许范围内。
class HeatingPlan...
get targetTemperature() {
if (thermostat.selectedTemperature > this._max) return this._max
else if (thermostat.selectedTemperature < this._min
return this._min
else return thermostat.selectedTemperature
}
// 调用方...
if (thePlan.targetTemperature > thermostat.currentTemperature) setToHeat()
else if (thePlan.targetTemperature < thermostat.currentTemperature) setToCool()
else setOff()
此时我担心 targetTemperature
依赖于全局的 thermostat
对象。我可以把这个对象提供的信息作为参数传入。
// 首先提炼参数后提炼函数
class HeatingPaln...
get targetTemperature() {
return this.xxNEWtargetTemperature(thermostat.selectedTemperature)
}
xxNEWtargetTemperature(selectedTemperature) {
if (selectedTemperature > this._max) return this._max
else if (selectedTemperature < this._min) return this._min
else return selectedTemperature
}
// 之后把调用该函数的地方改为使用新函数
// 调用方
if (thePlan.xxNEWtargetTemperature(thermostat.selectedTemperature) > thermostat.currentTemperature) setToHeat()
else if (thePlan.xxNEWtargetTemperature(thermostat.selectedTemperature) < thermostat.currentTemperature) setToCool()
else setOff()
之后删除旧函数并将新函数名的前缀去掉即可
移除设值函数
动机
如果为某个字段提供了设值函数,这就暗示这个字段可以被改变。如果不希望在对象创建之后此字段还有机会被改变,那就不要为它提供设值函数。这样一来,该字段就只能在构造函数中赋值,我“不想让它被修改”的意图就会更加清晰。
有两种情况需要讨论,一种情况是,有些人喜欢始终通过访问函数来读写字段值,包括在构造函数内也是如此。这会导致构造函数成为设置函数的唯一使用者。
另一种情况是对象是由客户端通过创建脚本构造出来,而不是只有一次简单的构造函数调用。所谓“创建脚本”,首先是调用构造函数,然后就是一系列设值函数的调用,共同完成新对象的构造。在创建脚本执行完成之后,这个新生对象的部分乃至全部字段就不应该再被修改。
做法
- 如果构造函数尚无法得到想要设入字段的值,就是用改变函数声明将这个值以参数形式传入构造函数。在构造函数中调用设值函数,对字段设值。
- 一处所有在构造函数之外对设值函数的调用,改为使用新的构造函数,每次修改之后就要测试。
- 使用内联函数消去设值函数,如果可能的话,把字段变为不可变。
- 测试。
范例
我有一个 person
类
class Person...
get name() {
return this._name
}
set name(arg) {
this._name = arg
}
get id() {
return this._id
}
set id(arg) {
this._id = arg
}
// 目前我会这样创建新对象
const martin = new Person()
martin.name = 'martin'
margin.id = '1234'
对象创建之后, name
字段可能会改变,但 id
字段不会。为了更清晰地表达这个设计意图,我希望移除对应 id
字段的设值函数。
class Person...
constructor(id) {
this.id = id
}
get name() {
return this._name
}
set name(arg) {
this._name = arg
}
get id() {
return this._id
}
set id(arg) {
this._id = arg
}
// 目前我会这样创建新对象
const martin = new Person('1234')
martin.name = 'martin'
以工厂函数取代构造函数
动机
很对面向对象语言都有特别的构造函数,专门用于对象的初始化。需要新建一个对象时,客户端通常会调用构造函数。但与一般的函数相比,构造函数又有一些丑陋的局限性,比如, Java
的构造函数只能返回当前所调用类的实例。
工厂函数就不受这些限制。工厂函数的实现内部可以调用构造函数,但也可以换成别的方式实现。
做法
- 新建一个工厂函数,让它调用现有的构造函数。
- 将调用构造函数的代码改为调用工厂函数。
- 每修改一处,就执行测试。
- 尽量缩小构造函数的可见范围。
范例
又是那个员工薪资系统,以 employee
类表示“员工”
class Employee...
constructor(name, typeCode) {
this._name = name
this._typeCode = typeCode
}
get name() {
return this._name
}
get type() {
return Employee.legalTypeCodes[this._typeCode]
}
static get legalTypeCodes() {
return {
'E': "Engeer",
"M": "Manager",
"S": "Salesman"
}
}
// 使用它的代码有这样的:
candidate = new Employee(document.name, documnet.empType)
// 也有这样的
const leadEngineer = new Employee(document.leadEngineer, 'E')
第一步是创建工厂函数,其中把对象创建的责任直接委派给构造函数
function createEmployee(name, typeCode) {
return new Employee(name, typeCode);
}
// 第一处调用
candidate = createEmployee(document.name, documnet.empType);
// 第二处调用
const leadEngineer = createEmployee(document.leadEngineer, "E");
但是这里以字符串字面量形式传入类型吗,一般来说都是坏味道,所以我更愿意再新建一个工厂函数,把“员工类别”的信息嵌在函数里体现。
// 调用方...
const leadEngineer = createEngineer(document.leadEngineer);
function createEngineer(name) {
return new Employee(name, "E");
}
以命令取代函数
动机
函数,不管是独立函数,还是以方法形式附着在对象上的函数,是程序设计的基本构造块。不过,将函数封装成自己的对象,有时也是一种有用的方法,这样的对象我称之为"命令对象",会简称“命令”。这种对象大多只服务于单一函数,获得对该函数的请求,执行该函数,这就是这种对象存在的意义。
与普通函数相比,命令对象提供了更大的控制灵活性和更强的表达能力。除了函数调用本身,命令对象还可以支持附加的操作,比如撤销操作。我可以通过命令对象提供的方法来设值命令的参数值,从而支持更丰富的生命周期观临崩离。我可以借助继承和钩子对函数行为加以定制。
但是命令对象的灵活性也是以复杂性作为代价的。所以只有当我特别需要命令对象提供的某种能力而普通函数无法提供这种能力时,我才会考虑使用命令对象。
做法
- 为想要包装的函数传概念一个空的类,根据该函数的名字为其命名。
- 使用搬移函数把函数移到空的类里。
- 可以考虑给每个参数创建一个字段,并在构造函数中添加对应的参数。
范例
JS 语言有很多缺点,丹巴函数作为一等公民对待,是它最正确的设计决策之一。在不具备这种能力的编程语言中,我经常要费力为很常见的任务创建命令对象,JS 则省去了这些麻烦。不过,即便在 JS 中,有时也需要用到命令对象。
一个典型的应用场景就是拆解复杂的函数,以便我理解和修改。要想真正展示这个重构手法的价值,我需要一个长而复杂的函数,所以这里展示的函数其实很短,下面的函数用于给一份保险申请评分。
function score(candidate, medicalExam, scoringGuide) {
let result = 0;
let healthLevel = 0;
let highMedicalRiskFlag = false;
if (medicalExam.isSmoke) {
healthLevel += 10;
highMedicalRiskFlag = true;
}
let certificationGrade = "regular";
if (scoringGuide.stateWithLowCertification(candidate.originState)) {
certificationGrade = "low";
result -= 5;
}
// lots of code like this
result == Math.max(healthLevel - 5, 0);
return result;
}
我首先创建一个空的类,用搬移函数把上述函数搬移到这个类里去
function score(candidate, medicalExam, scoringGuide) {
return new Scorer().execure(candidate, medicalExam, scoringGuide);
}
class Scorer {
execure(candidate, medicalExam, scoringGuide) {
let result = 0;
let healthLevel = 0;
let highMedicalRiskFlag = false;
if (medicalExam.isSmoke) {
healthLevel += 10;
highMedicalRiskFlag = true;
}
let certificationGrade = "regular";
if (scoringGuide.stateWithLowCertification(candidate.originState)) {
certificationGrade = "low";
result -= 5;
}
// lots of code like this
result == Math.max(healthLevel - 5, 0);
return result;
}
}
大多数时候,我更愿意在命令对象的构造函数中传入参数,而不让 execute
函数接收参数。在这样一个简单的拆解场景中,这一点带来的影响不大,但如果我要处理的命令需要更复杂的参数设置周期或大量定制,上述做法就会带来更多便利:多个命令类可以分别从各自的构造函数中获得各自不同的参数,然后又可以拍成队列挨个执行,因为它们的 execure
函数签名一样。
function score(candidate, medicalExam, scoringGuide) {
return new Scorer(candidate, medicalExam, scoringGuide).execute();
}
class Scorer {
constructor(candidate, medicalExam, scoringGuide) {
this._candidate = candidate;
this._medicalExam = medicalExam;
this._scoringGuide = scoringGuide;
}
execute(medicalExam, scoringGuide) {
let result = 0;
let healthLevel = 0;
let highMedicalRiskFlag = false;
if (this._medicalExam.isSmoke) {
healthLevel += 10;
highMedicalRiskFlag = true;
}
let certificationGrade = "regular";
if (
this._scoringGuide.stateWithLowCertification(this._candidate.originState)
) {
certificationGrade = "low";
result -= 5;
}
// lots of code like this
result == Math.max(healthLevel - 5, 0);
return result;
}
}
以命令取代函数的重构到此就结束了,不过之所以要做这个重构,是为了拆解复杂的函数,所以我还是大致展示一下如何拆解。下一步是把所有局部变量都变成字段。
class Scorer {
constructor(candidate, medicalExam, scoringGuide) {
this._candidate = candidate;
this._medicalExam = medicalExam;
this._scoringGuide = scoringGuide;
}
execute(medicalExam, scoringGuide) {
this._result = 0;
this._healthLevel = 0;
this._highMedicalRiskFlag = false;
if (this._medicalExam.isSmoke) {
this._healthLevel += 10;
this._highMedicalRiskFlag = true;
}
this._certificationGrade = "regular";
if (
this._scoringGuide.stateWithLowCertification(this._candidate.originState)
) {
this._certificationGrade = "low";
this._result -= 5;
}
// lots of code like this
this._result == Math.max(healthLevel - 5, 0);
return this._result;
}
}
之后使用提炼函数等手法来进行重构
class Scorer {
constructor(candidate, medicalExam, scoringGuide) {
this._candidate = candidate;
this._medicalExam = medicalExam;
this._scoringGuide = scoringGuide;
}
execute(medicalExam, scoringGuide) {
this._result = 0;
this._healthLevel = 0;
this._highMedicalRiskFlag = false;
this.scoreSmoking();
this._certificationGrade = "regular";
if (
this._scoringGuide.stateWithLowCertification(this._candidate.originState)
) {
this._certificationGrade = "low";
this._result -= 5;
}
// lots of code like this
this._result == Math.max(healthLevel - 5, 0);
return this._result;
}
scoreSmoking() {
if (this._medicalExam.isSmoke) {
this._healthLevel += 10;
this._highMedicalRiskFlag = true;
}
}
}
以函数取代命令
动机
命令对象为处理复杂计算提供了强大的机制。借助命令对象,可以轻松地将原本复杂的函数拆解为多个方法,彼此之间通过字段共享状态,但这种强大是有代价的,大多数时候,我只是想调用一个函数,让它完成自己的工作就好。
做法
- 运用提炼函数把创建并执行命令对象的代码提炼到一个函数中。
- 对命令对象在执行阶段用到的哈纳树,逐一使用内联函数。
- 使用改变函数声明,把构造函数的参数转移到执行函数。
- 对于所有字段,在执行函数中找到引用它的地方,并改为使用参数。每次修改后进行测试。
- 把调用构造函数和调用执行函数两步都内联到调用方。
- 测试。
- 用移除死代码把命令类消去。
范例
假设我有一个很小的命令对象
class ChargeCalculator {
constructor(customer, usage, provider) {
this._customer = customer;
this._usage = usage;
this._provider = provider;
}
get baseCharge() {
return this._customer.baseRage * this._usage;
}
get charge() {
return this.baseCharge + this._provider.connectionCharge;
}
}
// 调用方...
monthCharge = new ChargeCalculator(customer, usage, provider).charge;
首先我用提炼函数把命令对象的创建和调用过程包装到一个函数中
// 调用方...
monthCharge = charge(customer, usage, provider)
// 顶层作用域
function charge(customer, usage, provider) {
return new ChargeCalculator(customer, usage, provider).charge
}
将所有逻辑处理集中到一个函数
class ChargeCalculator...
charge(customer, usage, provider) {
const baseCharge = this._customer.baseRage * 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...
charge(customer, usage, provider) {
const baseCharge = customer.baseRage * usage
return baseCharge + provider.connectionCharge
}
之后删除搬空的类即可