《重构:改善既有的代码设计》读书笔记(六)

229 阅读7分钟

简化条件逻辑

分解条件表达式

反向重构

动机

[!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) &amp;&amp; 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) &amp;&amp; dolore(magna.aliqua);
   ut.enim.ad(minim.veniam);
   return someFinalComputation();
  }

范例:将条件反转

重构前:

  function adjustedCapital(anInstrument) {
   let result = 0;
   if (anInstrument.capital > 0) {
	if (anInstrument.interestRate > 0 &amp;&amp; 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" &amp;&amp; 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" &amp;&amp; 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" &amp;&amp; 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);
	  }
	}
  }