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

183 阅读7分钟

重构API

将查询函数和修改函数分离

反向重构

动机

[!info] 明确表现出“有副作用”与“无副作用”两种函数之间的差异,是个很好的想法。下面是一条好规则:任何有返回值的函数,都不应该有看得到的副作用——命令与查询分离

范例

重构前:

  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 "";
  }
  function alertForMiscreant(people) {
	if (findMiscreant(people) !== "") setOffAlarms();
  }

函数参数化

反向重构

动机

[!info] 如果我发现两个函数逻辑非常相似,只有一些字面量值不同,可以将其合并成一个函数,以参数的形式传入不同的值,从而消除重复。这个重构可以使函数更有用,因为重构后的函数还可以用于处理其他的值。

范例

重构前:

  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;
  }

重构后:

  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 withinBand(usage, bottom, top) {
	return usage > bottom ? Math.min(usage, top) - bottom : 0;
  }

移除标记参数

反向重构

动机

[!info] “标记参数”是这样的一种参数:调用者用它来指示被调函数应该执行哪一部分逻辑。

标记参数会让人难以理解函数调用中的差异性,而且需要知道标记参数有哪些可用的值。

布尔型的标记尤其糟糕,因为它们不能清晰地传达其含义——在调用一个函数时,我很难弄清true到底是什么意思。

范例

重构前:

  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);
	}
  }
  
  // 客户端1
  aShipment.deliveryDate = deliveryDate(anOrder, true);
  // 客户端2
  aShipment.deliveryDate = deliveryDate(anOrder, false);
			  

重构后:

			  
  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" &amp;&amp; !isRush)
	 deliveryTime = 3;
   }
   else if (isRush)
	deliveryTime = 3;
   else if (anOrder.deliveryState === "ME")
	deliveryTime = 3;
   else
	deliveryTime = 4;
   result = anOrder.placedOn.plusDays(2 + deliveryTime);
   if (isRush) result = result.minusDays(1);
   return result;
  }
  function rushDeliveryDate(anOrder) {
	return deliveryDate(anOrder, true);
  }
  function regularDeliveryDate(anOrder) {
	return deliveryDate(anOrder, false);
  }
  
  // 客户端1
  rushDeliveryDate(anOrder)
  // 客户端2
  regularDeliveryDate(anOrder)

保持对象完整

反向重构

动机

[!info] 如果需要从一个记录结构中导出几个值,然后又把这几个值一起传递给一个函数,那么更好的做法是,直接把整个记录传给这个函数。

范例

重构前:

  class HeatingPlan{
	withinRange(bottom, top) {
	 return (bottom >= this._temperatureRange.low) &amp;&amp; (top <= this._temperatureRange.high);
	}
  }
  // 调用方
  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(aNumberRange) {
	 return (aNumberRange.low >= this._temperatureRange.low) &amp;&amp;
	  (aNumberRange.high <= this._temperatureRange.high);
	}
  }
  // 调用方
  if (!aPlan.withinRange(aRoom.daysTempRange))
	alerts.push("room temperature went outside range");

以查询取代参数

反向重构

[[重构:改善既有的代码设计#以参数取代查询|以参数取代查询]]

动机

[!info] 函数的参数列表应该总结该函数的可变性,标示出函数可能体现出行为差异的主要方式。和任何代码中的语句一样,参数列表应该尽量避免重复,并且参数列表越短就越容易理解。

如果调用函数时传入了一个值,而这个值由函数自己来获得也是同样容易,这就是重复。这个本不必要的参数会增加调用者的难度,因为它不得不找出正确的参数值,其实原本调用者是不需要费这个力气的。

范例

重构前:

  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;
	 }
	}
  }

重构后:

  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;
	 }
	}
  }

以参数取代查询

反向重构

[[重构:改善既有的代码设计#以查询取代参数|以查询取代参数]]

动机

[!info] 需要使用本重构的情况大多源于我想要改变代码的依赖关系——为了让目标函数不再依赖于某个元素,我把这个元素的值以参数形式传递给该函数。

这里需要注意权衡:如果把所有依赖关系都变成参数,会导致参数列表冗长重复;如果作用域之间的共享太多,又会导致函数间依赖过度。我一向不善于微妙的权衡,所以“能够可靠地改变决定”就显得尤为重要,这样随着我的理解加深,程序也能从中受益。

范例

重构前:

  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();

重构后:

  class HeatingPlan{
	targetTemperature(selectedTemperature) {
	 if (selectedTemperature > this._max) return this._max;
	 else if (selectedTemperature < this._min) return this._min;
	 else return selectedTemperature;
	}
  }
  // 调用方
  if (thePlan.targetTemperature(thermostat.selectedTemperature) >
	 thermostat.currentTemperature)
   setToHeat();
  else if (thePlan.targetTemperature(thermostat.selectedTemperature) <
	   thermostat.currentTemperature)
   setToCool();
  else
   setOff();

移除设值函数

反向重构

动机

[!info] 如果为某个字段提供了设值函数,这就暗示这个字段可以被改变。如果不希望在对象创建之后此字段还有机会被改变,那就不要为它提供设值函数(同时将该字段声明为不可变)。

这样一来,该字段就只能在构造函数中赋值,我“不想让它被修改”的意图会更加清晰,并且可以排除其值被修改的可能性——这种可能性往往是非常大的。

范例

重构前:

  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";
  martin.id = "1234";

重构后:

  class Person{
	constructor(id) {
	this._id = id;
  }
	get name() {return this._name;}
	set name(arg) {this._name = arg;}
	get id() {return this._id;}
  }
  // 调用方
  const martin = new Person("1234");
  martin.name = "martin";

以工厂函数取代构造函数

反向重构

动机

[!info] 很多面向对象语言都有特别的构造函数,专门用于对象的初始化。需要新建一个对象时,客户端通常会调用构造函数。但与一般的函数相比,构造函数又常有一些丑陋的局限性。

工厂函数就不受这些限制。工厂函数的实现内部可以调用构造函数,但也可以换成别的方式实现。

范例

重构前:

  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": "Engineer", "M": "Manager", "S": "Salesman"};
	}
  }
  // 调用方1
  const candidate = new Employee(document.name, document.empType);
  // 调用方2
  const leadEngineer = new Employee(document.leadEngineer, 'E');

重构后:

  function createEmployee(name, typeCode) {
	return new Employee(name, typeCode);
  }
  function createEngineer(name) {
	return new Employee(name, "E");
  }
  // 调用方1
  const candidate = createEmployee(document.name, document.empType);
  // 调用方2
  const const leadEngineer = createEngineer(document.leadEngineer);

以命令取代函数

反向重构

[[重构:改善既有的代码设计#以函数取代命令|以函数取代命令]]

动机

[!info] 与普通的函数相比,命令对象提供了更大的控制灵活性和更强的表达能力。除了函数调用本身,命令对象还可以支持附加的操作,例如撤销操作。我可以通过命令对象提供的方法来设值命令的参数值,从而支持更丰富的生命周期管理能力。我可以借助继承和钩子对函数行为加以定制。

不过我们不能忘记,命令对象的灵活性也是以复杂性作为代价的。所以,如果要在作为一等公民的函数和命令对象之间做个选择,95%的时候我都会选函数。只有当我特别需要命令对象提供的某种能力而普通的函数无法提供这种能力时,我才会考虑使用命令对象。

范例

重构前:

  function score(candidate, medicalExam, scoringGuide) {
	let result = 0;
	let healthLevel = 0;
	let highMedicalRiskFlag = false;
  
	if (medicalExam.isSmoker) {
	  healthLevel += 10;
	  highMedicalRiskFlag = true;
	}
	let certificationGrade = "regular";
	if (scoringGuide.stateWithLowCertification(candidate.originState)) {
	  certificationGrade = "low";
	  result -= 5;
	} // lots more 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 () {
	 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 more code like this
	 this._result -= Math.max(this._healthLevel - 5, 0);
	 return this._result;
	 }
	scoreSmoking() {
	 if (this._medicalExam.isSmoker) {
	  this._healthLevel += 10;
	  this._highMedicalRiskFlag = true;
	 }
	}
  }

以函数取代命令

反向重构

[[重构:改善既有的代码设计#以命令取代函数|以命令取代函数]]

动机

[!info] 命令对象为处理复杂计算提供了强大的机制。借助命令对象,可以轻松地将原本复杂的函数拆解为多个方法,彼此之间通过字段共享状态;拆解后的方法可以分别调用;开始调用之前的数据状态也可以逐步构建。

但这种强大是有代价的。大多数时候,我只是想调用一个函数,让它完成自己的工作就好。如果这个函数不是太复杂,那么命令对象可能显得费而不惠,我就应该考虑将其变回普通的函数。

范例

重构前:

  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;
			  

重构后:

  function charge(customer, usage, provider) {
	const baseCharge = customer.baseRate * usage;
	return baseCharge + provider.connectionCharge;
  }
  
  // 调用方
  monthCharge = charge(customer, usage, provider);