简化条件逻辑
分解条件表达式
反向重构
动机
[!info] 和任何大块头代码一样,我可以将它分解为多个独立的函数,根据每个小块代码的用途,为分解而得的新函数命名,并将原函数中对应的代码改为调用新函数,从而更清楚地表达自己的意图。
对于条件逻辑,将每个分支条件分解成新函数还可以带来更多好处:可以突出条件逻辑,更清楚地表明每个分支的作用,并且突出每个分支的原因。
本重构手法其实只是 提炼函数 的一个应用场景。但我要特别强调这个场景,因为我发现它经常会带来很大的价值。
范例
重构前:
if (!aDate.isBefore(plan.summerStart) && !aDate.isAfter(plan.summerEnd))
charge = quantity * plan.summerRate;
else
charge = quantity * plan.regularRate + plan.regularServiceCharge;
重构后:
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;
}
合并条件表达式
反向重构
动机
[!info] 有时我会发现这样一串条件检查:检查条件各不相同,最终行为却一致。如果发现这种情况,就应该使用“逻辑或”和“逻辑与”将它们合并为一个条件表达式。
首先,合并后的条件代码会表述“实际上只有一次条件检查,只不过有多个并列条件需要检查而已”,从而使这一次检查的用意更清晰。
其次,这项重构往往可以为使用 提炼函数 做好准备。将检查条件提炼成一个独立的函数对于厘清代码意义非常有用,因为它把描述“做什么”的语句换成了“为什么这样做”。
范例
重构前:
function disabilityAmount(anEmployee) {
if (anEmployee.seniority < 2) return 0;
if (anEmployee.monthsDisabled > 12) return 0;
if (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));
}
}
以卫语句取代嵌套条件表达式
反向重构
动机
[!info] 这里的卫语句即为return语句。 以卫语句取代嵌套条件表达式的精髓就是:给某一条分支以特别的重视。
范例
重构前:
function payAmount(employee) {
let result;
if(employee.isSeparated) {
result = {amount: 0, reasonCode:"SEP"};
}
else {
if (employee.isRetired) {
result = {amount: 0, reasonCode: "RET"};
}
else {
// logic to compute amount
lorem.ipsum(dolor.sitAmet);1
consectetur(adipiscing).elit();
sed.do.eiusmod = tempor.incididunt.ut(labore) && dolore(magna.aliqua);
ut.enim.ad(minim.veniam);
result = someFinalComputation();
}
}
return result;
}
重构后:
function payAmount(employee) {
let result;
if (employee.isSeparated) return {amount: 0, reasonCode: "SEP"};
if (employee.isRetired) return {amount: 0, reasonCode: "RET"};
// logic to compute amount
lorem.ipsum(dolor.sitAmet);
consectetur(adipiscing).elit();
sed.do.eiusmod = tempor.incididunt.ut(labore) && dolore(magna.aliqua);
ut.enim.ad(minim.veniam);
return someFinalComputation();
}
范例:将条件反转
重构前:
function adjustedCapital(anInstrument) {
let result = 0;
if (anInstrument.capital > 0) {
if (anInstrument.interestRate > 0 && anInstrument.duration > 0) {
result = (anInstrument.income / anInstrument.duration) * anInstrument.adjustmentFactor;
}
}
return result;
}
重构后:
function adjustedCapital(anInstrument) {
if ( anInstrument.capital <= 0
|| anInstrument.interestRate <= 0
|| anInstrument.duration <= 0) return 0;
return (anInstrument.income / anInstrument.duration) * anInstrument.adjustmentFactor;
}
以多态取代条件表达式
反向重构
动机
[!info] 如果有好几个函数都有基于类型代码的switch语句。若果真如此,我就可以针对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 (bird.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 (bird.type) {
case 'EuropeanSwallow':
return 35;
case 'AfricanSwallow':
return 40 - 2 * bird.numberOfCoconuts;
case 'NorwegianBlueParrot':
return (bird.isNailed) ? 0 : 10 + bird.voltage / 10;
default:
return null;
}
}
重构后:
function plumages(birds) {
return new Map(birds
.map(b => createBird(b))
.map(bird => [bird.name, bird.plumage]));
}
function speeds(birds) {
return new Map(birds
.map(b => createBird(b))
.map(bird => [bird.name, 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 {
get plumage() {
return "average";
}
get airSpeedVelocity() {
return 35;
}
}
class AfricanSwallow extends Bird {
get plumage() {
return (this.numberOfCoconuts > 2) ? "tired" : "average";
}
get airSpeedVelocity() {
return 40 - 2 * this.numberOfCoconuts;
}
}
class NorwegianBlueParrot extends Bird {
get plumage() {
return (this.voltage > 100) ? "scorched" : "beautiful";
}
get airSpeedVelocity() {
return (this.isNailed) ? 0 : 10 + this.voltage / 10;
}
}
范例:用多态处理变体逻辑
重构前
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;
}
重构后
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.historyLengthFactor;
result += this.voyageLengthFactor;
return result;
}
get voyageLengthFactor() {
return (this.voyage.length > 14) ? - 1: 0;
}
get historyLengthFactor() {
return (this.history.length > 8) ? 1 : 0;
}
}
class ExperiencedChinaRating extends Rating {
get captainHistoryRisk() {
const result = super.captainHistoryRisk - 2;
return Math.max(result, 0);
}
get voyageLengthFactor() {
let result = 0;
if (this.voyage.length > 12) result += 1;
if (this.voyage.length > 18) result -= 1;
return result;
}
get historyLengthFactor() {
return (this.history.length > 10) ? 1 : 0;
}
get voyageProfitFactor() {
return super.voyageProfitFactor + 3;
}
}
function rating(voyage, history) {
return createRating(voyage, history).value;
}
function createRating(voyage, history) {
if (voyage.zone === "china" && history.some(v => "china" === v.zone))
return new ExperiencedChinaRating(voyage, history);
else return new Rating(voyage, history);
}
引入特例
反向重构
动机
[!info] 一种常见的重复代码是这种情况:一个数据结构的使用者都在检查某个特殊的值,并且当这个特殊值出现时所做的处理也都相同。如果我发现代码库中有多处以同样方式应对同一个特殊值,我就会想要把这个处理逻辑收拢到一处。
处理这种情况的一个好办法是使用“特例”(Special Case)模式:创建一个特例元素,用以表达对这种特例的共用行为的处理。这样我就可以用一个函数调用取代大部分特例检查逻辑。
范例
重构前:
class Site{
get customer() {return this._customer;}
}
class Customer{
get name() {
//...
}
get billingPlan() {
//...
}
set billingPlan(arg) {
//...
}
get paymentHistory() {
//...
}
}
// 客户端1
const aCustomer = site.customer;
// ... lots of intervening code ...
let customerName;
if (aCustomer === "unknown") customerName = "occupant";
else customerName = aCustomer.name;
// 客户端2
const plan =
aCustomer === "unknown" ? registry.billingPlans.basic : aCustomer.billingPlan;
//客户端3
if (aCustomer !== "unknown") aCustomer.billingPlan = newPlan;
//客户端4
const weeksDelinquent =
aCustomer === "unknown"
? 0
: aCustomer.paymentHistory.weeksDelinquentInLastYear;
重构后:
class Site{
get customer() {
return (this._customer === "unknown") ? new UnknownCustomer() : this._customer;
}
}
class Customer{
get name() {
//...
}
get billingPlan() {
//...
}
set billingPlan(arg) {
//...
}
get paymentHistory() {
//...
}
get isUnknown() {return false;}
}
class UnknownCustomer {
get isUnknown() {
return true;
}
get name(){
return "occupant"
}
get billingPlan() {return registry.billingPlans.basic;}
set billingPlan(arg) { /* ignore */ }
get paymentHistory() {return new NullPaymentHistory();}
}
class NullPaymentHistory{
get paymentHistory() {return new NullPaymentHistory();}
}
// 客户端1
const customerName=aCustomer.name
// 客户端2
const plan = aCustomer.billingPlan;
// 客户端3
aCustomer.billingPlan = newPlan;
// 客户端4
const weeksDelinquent = aCustomer.paymentHistory.weeksDelinquentInLastYear;
范例:使用对象字面量
重构前:
class Site{
get customer() {return this._customer;}
}
class Customer{
get name() {
//...
}
get billingPlan() {
//...
}
set billingPlan(arg) {
//...
}
get paymentHistory() {
//...
}
}
// 客户端1
const aCustomer = site.customer;
// ... lots of intervening code ...
let customerName;
if (aCustomer === "unknown") customerName = "occupant";
else customerName = aCustomer.name;
// 客户端2
const plan =
aCustomer === "unknown" ? registry.billingPlans.basic : aCustomer.billingPlan;
//客户端3
const weeksDelinquent =
aCustomer === "unknown"
? 0
: aCustomer.paymentHistory.weeksDelinquentInLastYear;
重构后:
class Site{
get customer() {
return (this._customer === "unknown") ? createUnknownCustomer() : this._customer;
}
}
class Customer{
get name() {
//...
}
get billingPlan() {
//...
}
set billingPlan(arg) {
//...
}
get paymentHistory() {
//...
}
get isUnknown() {return false;}
}
function createUnknownCustomer() {
return {
isUnknown: true,
name: "occupant",
billingPlan: registry.billingPlans.basic,
paymentHistory: {
weeksDelinquentInLastYear: 0,
},
};
}
class NullPaymentHistory{
get paymentHistory() {return new NullPaymentHistory();}
}
// 客户端1
const customerName=aCustomer.name
// 客户端2
const plan = aCustomer.billingPlan;
// 客户端3
const weeksDelinquent = aCustomer.paymentHistory.weeksDelinquentInLastYear;
范例:使用变换
重构前:
class Site{
get customer() {return this._customer;}
}
class Customer{
get name() {
//...
}
get billingPlan() {
//...
}
set billingPlan(arg) {
//...
}
get paymentHistory() {
//...
}
}
// 客户端1
const aCustomer = site.customer;
// ... lots of intervening code ...
let customerName;
if (aCustomer === "unknown") customerName = "occupant";
else customerName = aCustomer.name;
// 客户端2
const plan =
aCustomer === "unknown" ? registry.billingPlans.basic : aCustomer.billingPlan;
//客户端3
const weeksDelinquent =
aCustomer === "unknown"
? 0
: aCustomer.paymentHistory.weeksDelinquentInLastYear;
重构后:
class Site{
get customer() {
return this._customer;
}
}
class Customer{
get name() {
//...
}
get billingPlan() {
//...
}
set billingPlan(arg) {
//...
}
get paymentHistory() {
//...
}
get isUnknown() {return false;}
}
function isUnknown(aCustomer) {
if (aCustomer === "unknown") return true;
else return aCustomer.isUnknown;
}
function enrichSite(aSite) {
const result = _.cloneDeep(aSite);
const unknownCustomer = {
isUnknown: true,
name: "occupant",
billingPlan: registry.billingPlans.basic,
paymentHistory: {
weeksDelinquentInLastYear: 0,
},
};
if (isUnknown(result.customer)) result.customer = unknownCustomer;
else result.customer.isUnknown = false;
return result;
}
class NullPaymentHistory{
get paymentHistory() {return new NullPaymentHistory();}
}
// 客户端1
const customerName=aCustomer.name
// 客户端2
const plan = aCustomer.billingPlan;
// 客户端3
const weeksDelinquent = aCustomer.paymentHistory.weeksDelinquentInLastYear;
引入断言
反向重构
动机
[!info] 断言是一种很有价值的交流形式——它们告诉阅读者,程序在执行到这一点时,对当前状态做了何种假设。另外断言对调试也很有帮助。
范例
重构前:
class Customer{
applyDiscount(aNumber) {
return (this.discountRate)
? aNumber - (this.discountRate * aNumber)
: aNumber;
}
}
重构后:
class Customer{
applyDiscount(aNumber) {
if (!this.discountRate) return aNumber;
else {
assert(this.discountRate >= 0);
return aNumber - (this.discountRate * aNumber);
}
}
}