程序的复杂度大多来自条件逻辑。因此可以借助重构把条件逻辑变得更容易理解。
- 使用分解条件表达式处理复杂的条件表达式
- 用合并条件表达式理清逻辑组合
- 用以卫语句取代嵌套条件表达式表达"在主要处理逻辑之前先做检查"的意图
- 以多态取代条件表达式可以处理switch逻辑
很多条件逻辑是用于处理特殊情况的,如处理null值。
- 可以用引入特例处理逻辑大多相同的情况,消除重复代码
分解条件表达式(Decompose Conditional)
合并条件表达式(Consolidate Conditional Expression)
以卫语句取代嵌套条件表达式(Replace Nested Conditional with Guard Clauses)
以多态取代条件表达式(Replace Conditional with Polymorphism)
程序之中,复杂的条件逻辑是最常导致复杂度上升的地点之一。大型函数本身就会使代码的可读性下降,而条件逻辑则会使代码更难阅读。
对于复杂逻辑的函数,和任何大块头代码一样,可以将它分解为多个独立的函数,根据每个小块代码的用途,为分解而得的新函数命名,并将原函数中对应的代码改为调用新函数,从而更清楚地表达自己的意图。对于条件逻辑,将每个分支条件分解成新函数还可以带来更多好处:可以突出条件逻辑,更清楚地表明每个分支的作用,并且突出每个分支的原因。
例子
// 重构前
if (!aDate.isBefore(plan.summerStart) && !aDate.isAfter(plan.summerEnd))
charge = quantity * plan.summerRate;
else
charge = quantity * plan.regularRate + plan.regularServiceCharge;
对上面的例子,可以将条件抽成一个函数,判断为真的分支抽成一个函数,判断为假的分支抽成一个函数。
if (summer())
charge = summerCharge();
else
charge = regularCharge();
function summer() {
return !aDate.isBefore(plan.summerStart) && !aDate.isAfter(plan.summerEnd);
}
function summerCharge() {
return quantity * plan.summerRate;
}
function regularCharge() {
return quantity * plan.regularRate + plan.regularServiceCharge;
}
最后,可以将原来的if语句改成三元运算符安排条件语句
// 重构后
charge = summer() ? summerCharge() : regularCharge();
function summer() {
return !aDate.isBefore(plan.summerStart) && !aDate.isAfter(plan.summerEnd);
}
function summerCharge() {
return quantity * plan.summerRate;
}
function regularCharge() {
return quantity * plan.regularRate + plan.regularServiceCharge;
}
如果一串检查条件各不相同,但最终行为一致,就应该使用"逻辑或"和"逻辑与"将它们合并为一个条件表达式。
例子
// 重构前
function disabilityAmount(anEmployee) {
if (anEmployee.seniority < 2) return 0;
if (anEmployee.monthsDisabled > 12) return 0;
if (anEmployee.isPartTime) return 0;
// compute the disability amount
}
例子中三个条件最终的返回都是0,可以直接用逻辑或运算符来合并
function disabilityAmount(anEmployee) {
if ((anEmployee.seniority < 2)
|| (anEmployee.monthsDisabled > 12)
|| (anEmployee.isPartTime))
return 0;
// compute the disability amount
}
最后,对这个条件表达式使用提炼函数
// 重构后
function disabilityAmount(anEmployee) {
if (isNotEligableForDisability()) return 0;
// compute the disability amount
}
function isNotEligableForDisability() {
return ((anEmployee.seniority < 2)
|| (anEmployee.monthsDisabled > 12)
|| (anEmployee.isPartTime));
}
条件表达式通常有两种风格。第一种风格是:两个条件分支都属于正常行为。第二种风格是:只有一个条件分支是正常行为,另一个分支则是异常的情况。
如果两条分支都是正常行为,就应该使用形如if … else …的条件表达式;如果某个条件极其罕见,就应该单独检查该条件,并在该条件为真时从函数中返回。这样的单独检查常常被称为"卫语句"(guard clauses)。
卫语句的精髓就是:给某一条分支以特别的重视。如果使用if … else …结构,对if分支和else分支的重视都是同等的。卫语句不同,告诉读者:"这种情况不是本函数的核心逻辑所关心的,如果真的发生了,请做一些必要的整理工作,然后退出"。
例1
// 重构前
function payAmount(employee) {
let result;
if (employee.isSeparated) {
result = { amount: 0, reasonable: 'SEP' };
} else {
if (employee.isRetired) {
result = { amount: 0, reasonable: 'RET' };
} else {
// logic to compute amount
result = someFinalComputation();
}
}
return result;
}
上面嵌套的条件逻辑看不清代码真实的含义。只有当前两个条件表达式都不为真的时候,这段代码才真正开始它的主要工作。
使用卫语句,去除else分支,并去掉局部变量result
// 重构后
function payAmount(employee) {
if (employee.isSeparated) return { amount: 0, reasonable: 'SEP' };
if (employee.isRetired) return { amount: 0, reasonable: 'RET' };
return someFinalComputation();
}
如果有好几个函数都有基于类型代码的switch语句,完全可以针对这种分支逻辑创建一个类,用多态来承载各个类型特有的行为,从而去除重复的分支逻辑。
基础逻辑可能是最常用的,也可能是最简单的。可以把基础逻辑放进超类,这样就首先可以理解在和部分逻辑,暂时不管各种变体,然后可以把每种变体逻辑单独放进一个子类,其中的代码着重强调与基础逻辑的差异。
例子
// 重构前
function plumages(birds) {
return new Map(birds.map(b => [b.name, plumage(b)]));
}
function speeds(birds) {
return new Map(birds.map(b => [b.name, airSpeedVelocity(b)]));
}
function plumage(bird) {
switch (brid.type) {
case 'EuropeanSwallow':
return 'average';
case 'AfricanSwallow':
return (bird.numberOfCoconuts > 2) ? 'tired' : 'average';
case 'NorwegianBlueParrot':
return (bird.voltage > 100) ? 'scorched' : 'beautiful';
default:
return 'unknown';
}
}
function airSpeedVelocity(bird) {
switch (brid.type) {
case 'EuropeanSwallow':
return 35;
case 'AfricanSwallow':
return 40 - 2 * bird.numberOfCoconuts;
case 'NorwegianBlueParrot':
return (bird.isNailed > 100) ? 0 : 10 + bird.voltage / 10;
default:
return null;
}
}
以上示例可以用多态处理各类型特有的行为进行重构
先将airSpeedVelocity和plumage两个函数使用函数组合成类。
function plumage(bird) {
return new Bird(bird).plumage;
}
function airSpeedVelocity(bird) {
return new Bird(bird).airSpeedVelocity;
}
class Bird {
constructor(birdObject) {
Object.assign(this, birdObject);
}
get plumage() {
switch (this.type) {
case 'EuropeanSwallow':
return 'average';
case 'AfricanSwallow':
return (this.numberOfCoconuts > 2) ? 'tired' : 'average';
case 'NorwegianBlueParrot':
return (this.voltage > 100) ? 'scorched' : 'beautiful';
default:
return 'unknown';
}
}
get airSpeedVelocity() {
switch (this.type) {
case 'EuropeanSwallow':
return 35;
case 'AfricanSwallow':
return 40 - 2 * this.numberOfCoconuts;
case 'NorwegianBlueParrot':
return (this.isNailed > 100) ? 0 : 10 + this.voltage / 10;
default:
return null;
}
}
}
接着对每种鸟创建一个子类,用一个工厂函数来实例化合适的子类对象:
function plumage(bird) {
return new Bird(bird).plumage;
}
function airSpeedVelocity(bird) {
return new Bird(bird).airSpeedVelocity;
}
function createBird(bird) {
switch (bird.type) {
case 'EuropeanSwallow':
return new EuropeanSwallow(bird);
case 'AfricanSwallow':
return new AfricanSwallow(bird);
case 'NorwegianBlueParrot':
return new NorwegianBlueParrot(bird);
default:
return new Bird(bird);
}
}
class Bird {
constructor(birdObject) {
Object.assign(this, birdObject);
}
get plumage() {
switch (this.type) {
case 'EuropeanSwallow':
return 'average';
case 'AfricanSwallow':
return (this.numberOfCoconuts > 2) ? 'tired' : 'average';
case 'NorwegianBlueParrot':
return (this.voltage > 100) ? 'scorched' : 'beautiful';
default:
return 'unknown';
}
}
get airSpeedVelocity() {
switch (this.type) {
case 'EuropeanSwallow':
return 35;
case 'AfricanSwallow':
return 40 - 2 * this.numberOfCoconuts;
case 'NorwegianBlueParrot':
return (this.isNailed > 100) ? 0 : 10 + this.voltage / 10;
default:
return null;
}
}
}
class EuropeanSwallow extends Bird {}
class AfricanSwallow extends Bird {}
class NorwegianBlueParrot extends Bird {}
接着将Bird类中的airSpeedVelocity、plumage的实现移入到子类中,父类中只留下默认的返回
// 重构后
function plumage(bird) {
return new Bird(bird).plumage;
}
function airSpeedVelocity(bird) {
return new Bird(bird).airSpeedVelocity;
}
function createBird(bird) {
switch (bird.type) {
case 'EuropeanSwallow':
return new EuropeanSwallow(bird);
case 'AfricanSwallow':
return new AfricanSwallow(bird);
case 'NorwegianBlueParrot':
return new NorwegianBlueParrot(bird);
default:
return new Bird(bird);
}
}
class Bird {
constructor(birdObject) {
Object.assign(this, birdObject);
}
get plumage() {
return 'unknown';
}
get airSpeedVelocity() {
return null;
}
}
class EuropeanSwallow extends Bird {
constructor(birdObject) {
Object.assign(this, birdObject);
}
get plumage() {
return 'average';
}
get airSpeedVelocity() {
return 35;
}
}
class AfricanSwallow extends Bird {
constructor(birdObject) {
Object.assign(this, birdObject);
}
get plumage() {
return (this.numberOfCoconuts > 2) ? 'tired' : 'average';
}
get airSpeedVelocity() {
return 40 - 2 * this.numberOfCoconuts;
}
}
class NorwegianBlueParrot extends Bird {
constructor(birdObject) {
Object.assign(this, birdObject);
}
get plumage() {
return (this.voltage > 100) ? 'scorched' : 'beautiful';
}
get airSpeedVelocity() {
return (this.isNailed > 100) ? 0 : 10 + this.voltage / 10;
}
}
例2 用多态处理变体逻辑
存在一种继承使用情况:某个对象与另一个对象大体类似,但又有一些不同之处。
// 重构前
function rating(voyage, history) {
const vpf = voyageProfitFactor(voyage, history);
const vr = voyageRisk(voyage);
const chr = captainHistoryRisk(voyage, history);
if (vpf * 3 > (vr + chr * 2)) return 'A';
else return 'B';
}
function voyageRisk(voyage) {
let result = 1;
if (voyage.length > 4) result += 2;
if (voyage.length > 8) result += voyage.length - 8;
if (['china', 'east-indies'].includes(voyage.zone)) result += 4;
return Math.max(result, 0);
}
function captainHistoryRisk(voyage, history) {
let result = 1;
if (history.length < 5) result += 4;
result += history.filter(v => v.profit < 0).length;
if (voyage.zone === 'china' && hasChina(history)) result -= 2;
return Math.max(result, 0);
}
function hasChina(history) {
return history.some(v => 'china' === v.zone);
}
function voyageProfitFactor(voyage, history) {
let result = 2;
if (voyage.zone === 'china') result += 1;
if (voyage.zone === 'east-indies') result += 1;
if (voyage.zone === 'china' && hasChina(history)) {
result += 3;
if (history.length > 10) result += 1;
if (voyage.length > 12) result += 1;
if (voyage.length > 18) result -= 1;
} else {
if (history.length > 8) result += 1;
if (voyage.length > 14) result -= 1;
}
return result;
}
注意到在captainHistoryRisk、voyageProfitFactor两个函数中voyage.zone === 'china' && hasChina(history)
出现了两次。可以将这个逻辑从基础逻辑中抽离出来当做子类继承。
首先,需要引入多态,建立类结构,使用函数组合成类
function rating(voyage, history) {
return new Rating(voyage, history).value;
}
class Rating {
constructor(voyage, history) {
this.voyage = voyage;
this.history = history;
}
get value() {
const vpf = this.voyageProfitFactor;
const vr = this.voyageRisk;
const chr = this.captainHistoryRisk;
if (vpf * 3 > (vr + chr * 2)) return 'A';
else return 'B';
}
get voyageRisk() {
let result = 1;
if (this.voyage.length > 4) result += 2;
if (this.voyage.length > 8) result += this.voyage.length - 8;
if (['china', 'east-indies'].includes(this.voyage.zone)) result += 4;
return Math.max(result, 0);
}
get captainHistoryRisk() {
let result = 1;
if (this.history.length < 5) result += 4;
result += this.history.filter(v => v.profit < 0).length;
if (this.voyage.zone === 'china' && this.hasChinaHistory) result -= 2;
return Math.max(result, 0);
}
get voyageProfitFactor() {
let result = 2;
if (this.voyage.zone === 'china') result += 1;
if (this.voyage.zone === 'east-indies') result += 1;
if (this.voyage.zone === 'china' && this.hasChinaHistory) {
result += 3;
if (this.this.history.length > 10) result += 1;
if (this.voyage.length > 12) result += 1;
if (this.voyage.length > 18) result -= 1;
} else {
if (this.history.length > 8) result += 1;
if (this.voyage.length > 14) result -= 1;
}
return result;
}
get hasChinaHistory() {
return this.history.some(v => 'china' === v.zone);
}
}
接着,创建一个空的子类,用来存储与超类不同的行为;然后,建立工厂函数,替换原来直接调用构造函数的方式
class ExperienceChinaRating extends Rating {}
function createRating(voyage, history) {
if (voyage.zone === 'china' && history.some(v => 'china' === v.zone)) {
return new ExperienceChinaRating(voyage, history);
}
return new Rating(voyage, history);
}
function rating(voyage, history) {
return createRating(voyage, history).value;
}
移植超类中需要移入子类的逻辑,需要注意voyageProfitFactor
函数,可以先用提炼函数将整个条件逻辑块提炼出来
get voyageProfitFactor() {
let result = 2;
if (this.voyage.zone === 'china') result += 1;
if (this.voyage.zone === 'east-indies') result += 1;
result += this.voyageAndHistoryLengthFactor;
return result;
}
get voyageAndHistoryLengthFactor() {
let result = 0;
if (this.voyage.zone === 'china' && this.hasChinaHistory) {
result += 3;
if (this.this.history.length > 10) result += 1;
if (this.voyage.length > 12) result += 1;
if (this.voyage.length > 18) result -= 1;
} else {
if (this.history.length > 8) result += 1;
if (this.voyage.length > 14) result -= 1;
}
return result;
}
接下来,可以拆解voyageAndHistoryLengthFactor
使其子类化
class Rating {
get voyageAndHistoryLengthFactor() {
let result = 0;
if (this.history.length > 8) result += 1;
if (this.voyage.length > 14) result -= 1;
return result;
}
}
class ExperienceChinaRating {
get voyageAndHistoryLengthFactor() {
let result = 0;
result += 3;
if (this.this.history.length > 10) result += 1;
if (this.voyage.length > 12) result += 1;
if (this.voyage.length > 18) result -= 1;
return result;
}
}
最后,整合所有修改
// 重构后
function createRating(voyage, history) {
if (voyage.zone === 'china' && history.some(v => 'china' === v.zone)) {
return new ExperienceChinaRating(voyage, history);
}
return new Rating(voyage, history);
}
function rating(voyage, history) {
return createRating(voyage, history).value;
}
class Rating {
constructor(voyage, history) {
this.voyage = voyage;
this.history = history;
}
get value() {
const vpf = this.voyageProfitFactor;
const vr = this.voyageRisk;
const chr = this.captainHistoryRisk;
if (vpf * 3 > (vr + chr * 2)) return 'A';
else return 'B';
}
get voyageRisk() {
let result = 1;
if (this.voyage.length > 4) result += 2;
if (this.voyage.length > 8) result += this.voyage.length - 8;
if (['china', 'east-indies'].includes(this.voyage.zone)) result += 4;
return Math.max(result, 0);
}
get captainHistoryRisk() {
let result = 1;
if (this.history.length < 5) result += 4;
result += this.history.filter(v => v.profit < 0).length;
return Math.max(result, 0);
}
get voyageProfitFactor() {
let result = 2;
if (this.voyage.zone === 'china') result += 1;
if (this.voyage.zone === 'east-indies') result += 1;
result += this.voyageAndHistoryLengthFactor;
return result;
}
get voyageAndHistoryLengthFactor() {
let result = 0;
if (this.history.length > 8) result += 1;
if (this.voyage.length > 14) result -= 1;
return result;
}
get hasChinaHistory() {
return this.history.some(v => 'china' === v.zone);
}
}
class ExperienceChinaRating extends Rating {
get captainHistoryRisk() {
let result = 1;
if (this.history.length < 5) result += 4;
result += this.history.filter(v => v.profit < 0).length;
if (this.voyage.zone === 'china' && this.hasChinaHistory) result -= 2;
return Math.max(result, 0);
}
get voyageAndHistoryLengthFactor() {
let result = 0;
result += 3;
if (this.this.history.length > 10) result += 1;
if (this.voyage.length > 12) result += 1;
if (this.voyage.length > 18) result -= 1;
return result;
}
}
在作者书中,接下来还是会继续重构简化函数,但已经脱离了对这个重构方法的理解,不再继续下去。
有一种常见的重复代码的情况:一个数据结构的使用者都在检查某个特殊的值,并且当这个特殊值出现时所做的处理也都相同。如果发现代码库中有多处以同样方式应对同一个特殊值,应该把这个处理逻辑收拢在一起。
可以创建一个特例元素,用以表达对这种特例的公用行为的处理。
如果只需要从这个对象读取数据,可以提供一个字面量对象(literal object),其中所有的值都是预先填充好的。如果除简单的数值之外还需要更多的行为,就需要创建一个特殊对象,其中包含所有公用行为所对应的函数。
一个通常需要特殊处理的值就是null,这个模式常被叫做"Null对象"(Null Object)模式。