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

138 阅读8分钟

处理继承关系

函数上移

反向重构

[[重构:改善既有的代码设计#函数下移|函数下移]]

动机

[!info] 如果某个函数在各个子类中的函数体都相同(它们很可能是通过复制粘贴得到的),这就是最显而易见的函数上移适用场合。

范例

重构前:

  class Employee extends Party{
	get annualCost() {
	  return this.monthlyCost * 12;
	}
  }
  class Department extends Party{
	get totalAnnualCost() {
	  return this.monthlyCost * 12;
	}
  }

重构后:

  class Party{
	get annualCost() {
	  return this.monthlyCost * 12;
	}
	get monthlyCost() {
	  throw new SubclassResponsibilityError();
	}
  }

字段上移

反向重构

[[重构:改善既有的代码设计#字段下移|字段下移]]

动机

[!info] 如果各子类是分别开发的,或者是在重构过程中组合起来的,你常会发现它们拥有重复特性,特别是字段更容易重复。这样的字段有时拥有近似的名字,但也并非绝对如此。

判断若干字段是否重复,唯一的办法就是观察函数如何使用它们。如果它们被使用的方式很相似,我就可以将它们提升到超类中去。

范例

重构前:

重构后:

构造函数本体上移

反向重构

动机

[!info] 如果子类的构造函数存在一段相同的代码,则考虑把这段代码抽取成一个新的构造函数放到超类中。

范例

重构前:

  class Party {}
  
  class Employee extends Party {
   constructor(name, id, monthlyCost) {
	super();
	this._id = id;
	this._name = name;
	this._monthlyCost = monthlyCost;
   }
   // rest of class...
  class Department extends Party {
   constructor(name, staff){
	super();
	this._name = name;
	this._staff = staff;
   }
   // rest of class...
}

重构后:

  class Party {
	constructor(name){
	  this._name = name;
	}
  }
  
  class Employee extends Party {
   constructor(name, id, monthlyCost) {
	super(name);
	this._id = id;
	this._monthlyCost = monthlyCost;
   }
   // rest of class...
  class Department extends Party {
   constructor(name, staff){
	super(name);
	this._staff = staff;
   }
   // rest of class...
}

函数下移

反向重构

[[重构:改善既有的代码设计#函数上移|函数上移]]

动机

[!info] 如果超类中的某个函数只与一个(或少数几个)子类有关,那么最好将其从超类中挪走,放到真正关心它的子类中去。

这项重构手法只有在超类明确知道哪些子类需要这个函数时适用。如果超类不知晓这个信息,那我就得用 以多态取代条件表达式 只留些共用的行为在超类。

范例

重构前:

重构后:

字段下移

反向重构

[[重构:改善既有的代码设计#字段上移|字段上移]]

动机

[!info] 如果某个字段只被一个子类(或者一小部分子类)用到,就将其搬移到需要该字段的子类中。

范例

重构前:

重构后:

以子类取代类型码

反向重构

[[重构:改善既有的代码设计#移除子类|移除子类]]

动机

[!info] 软件系统需要表现“相似但又不同的东西”,可以使用类型码字段来实现分类关系。类型码可以实现为枚举、符号、字符串或数字,取值通常来自外部服务。

引入子类可以使用多态来处理条件逻辑,以多态取代条件表达式。类可以用于将特定字段或函数放到合适的子类中去,以及确保只有当类型码取值正确时才使用该字段。

在使用以子类取代类型码时需要考虑应该直接处理携带类型码的类还是处理类型码本身。

范例

重构前:

  class Employee{
	constructor(name, type){
	  this.validateType(type);
	  this._name = name;
	  this._type = type;
	}
	validateType(arg) {
	  if (!["engineer", "manager", "salesman"].includes(arg))
		throw new Error(`Employee cannot be of type ${arg}`);
	}
	toString() {return `${this._name} (${this._type})`;}
  }

重构后:

  class Employee{
	constructor(name){
	  this._name = name;
	}
	
	toString() {return `${this._name} (${this._type})`;}
  }
  
  function createEmployee(name,type) {
	 switch (type) {
	  case "engineer": return new Engineer(name);
	  case "salesman": return new Salesman(name);
	  case "manager": return new Manager (name);
	  default: throw new Error(`Employee cannot be of type ${type}`);
	 }
   }
  
  class Engineer extends Employee {
	get type() {
	  return "engineer";
	}
  }
  
  class Salesman extends Employee {
	get type() {
	  return "salesman";
	}
  }
  
  class Manager extends Employee {
	get type() {
	  return "manager";
	}
  }

移除子类

反向重构

[[重构:改善既有的代码设计#以子类取代类型码|以子类取代类型码]]

动机

[!info] 子类很有用,它们为数据结构的多样和行为的多态提供支持,它们是针对差 异编程的好工具。但随着软件的演化,子类所支持的变化可能会被搬移到别处, 甚至完全去除,这时子类就失去了价值。

子类存在着就有成本,阅读者要花心思去理解它的用意,所以如果子类的用 处太少,就不值得存在了。此时,最好的选择就是移除子类,将其替换为超类中 的一个字段。

范例

重构前:

class Person{
	constructor(name) {
	 this._name = name;
	}
	get name()    {return this._name;}
	get genderCode() {return "X";}
	// snip
}

class Male extends Person {
 get genderCode() {return "M";}
}

class Female extends Person {
 get genderCode() {return "F";}
}
// 客户端
const numberOfMales = people.filter(p => p instanceof Male).length;

重构后:

class Person{
	constructor(name, genderCode) {
	  this._name = name;
	  this._genderCode = genderCode || "X";
	}
	get name()    {return this._name;}
	get genderCode() {return "X";}
	get isMale() {return "M" === this._genderCode;}
	// snip
}
function createPerson(aRecord) {
  switch (aRecord.gender) {
    case "M":
      return new Person(aRecord.name, "M");
    case "F":
      return new Person(aRecord.name, "F");
    default:
      return new Person(aRecord.name, "X");
  }
}
function loadFromInput(data) {
  return data.map(aRecord => createPerson(aRecord));
}
// 客户端
const numberOfMales = people.filter(p => p.isMale).length;

提炼超类

反向重构

动机

[!info] 如果我看见两个类在做相似的事,可以利用基本的继承机制把它们的相似之 处提炼到超类。我可以用[[重构:改善既有的代码设计#字段上移|字段上移]]把相同的数据搬到超类,用[[重构:改善既有的代码设计#函数上移|函数上移]]搬移相同的行为。

另一种选择就是[[重构:改善既有的代码设计#提炼类|提炼类]]。这两种方案之间的选择,其实就是继承和 委托之间的选择,总之目的都是把重复的行为收拢一处。提炼超类通常是比较简 单的做法,所以我会首选这个方案。

范例

重构前

class Employee {
 constructor(name, id, monthlyCost) {
  this._id = id;
  this._name = name;
  this._monthlyCost = monthlyCost;
 }
 get monthlyCost() {return this._monthlyCost;}
 get name() {return this._name;}
 get id() {return this._id;}

 get annualCost() {
  return this.monthlyCost * 12;
 }
}

class Department {
 constructor(name, staff){
  this._name = name;
  this._staff = staff;
 }
 get staff() {return this._staff.slice();}
 get name() {return this._name;}

 get totalMonthlyCost() {
  return this.staff
   .map(e => e.monthlyCost)
   .reduce((sum, cost) => sum + cost);
 }
 get headCount() {
  return this.staff.length;
 }
 get totalAnnualCost() {
  return this.totalMonthlyCost * 12;
 }
}

重构后

class Party {
	constructor(name){
	  this._name = name;
	}
	get name() {return this._name;}
	get annualCost() {
	  return this.monthlyCost * 12;
	}
}

class Employee extends Party {
 constructor(name, id, monthlyCost) {
  super(name);
  this._id = id;
  this._monthlyCost = monthlyCost;
 }
 // rest of class...
class Department extends Party {
 constructor(name, staff){
  super(name);
  this._staff = staff;
  get monthlyCost() { ... }
 }
 // rest of class...

折叠继承体系

反向重构

动机

[!info] 在重构类继承体系时,我经常把函数和字段上下移动。随着继承体系的演 化,我有时会发现一个类与其超类已经没多大差别,不值得再作为独立的类存 在。此时我就会把超类和子类合并起来。

范例

重构前

重构后

以委托取代子类

反向重构

动机

[!info] 继承有两个不太好的地方:

  1. 行为不同的原因可能有多种,但继承只能用于处理一个方向上的变化。
  2. 继承给类之间引入了非常紧密的关系。在超类上做任何修改,都很可能破坏子类,所以我们必须非常小心,并且充分理解子类如何从超类派生。

这两个问题用委托都能解决。对于不同的变化原因,我可以委托给不同的类。委托是对象之间常规的关系。与继承关系相比,使用委托关系时接口更清晰、耦合更少。

范例

重构前

class Booking{
	constructor(show, date) {
	  this._show = show;
	  this._date = date;
	}
	get hasTalkback() {
	 return this._show.hasOwnProperty('talkback') && !this.isPeakDay;
	}
	get basePrice() {
	  let result = this._show.price;
	  if (this.isPeakDay) result += Math.round(result * 0.15);
	  return result;
	}
}

class PremiumBooking extends Booking{
	constructor(show, date, extras) {
	  super(show, date);
	  this._extras = extras;
	}
	get hasTalkback() {
	  return this._show.hasOwnProperty('talkback');
	}
	get basePrice() {
	  return Math.round(super.basePrice + this._extras.premiumFee);
	}
	get hasDinner() {
	  return this._extras.hasOwnProperty('dinner') && !this.isPeakDay;
	}
}

//进行普通预定的客户端
aBooking = new Booking(show,date);
// 进行高级预定的客户端
aBooking = new PremiumBooking(show, date, extras);

重构后

function createBooking(show, date) {
  return new Booking(show, date);
}
function createPremiumBooking(show, date, extras) {
  const result = new PremiumBooking(show, date, extras);
  result._bePremium(extras);
  return result;
}
class PremiumBookingDelegate{
	constructor(hostBooking, extras) {
	  this._host = hostBooking;
	  this._extras = extras;
	}
	get hasTalkback() {
	  return this._host._show.hasOwnProperty('talkback');
	}
	extendBasePrice(base) {
	  return Math.round(base + this._extras.premiumFee);
	}
	get hasDinner() {
	  return this._extras.hasOwnProperty('dinner') && !this._host.isPeakDay;
	}
}
class Booking{
	constructor(show, date) {
	  this._show = show;
	  this._date = date;
	}
	get hasTalkback() {
	 return this._show.hasOwnProperty('talkback') && !this.isPeakDay;
	}
	get basePrice() {
	  let result = this._show.price;
	  if (this.isPeakDay) result += Math.round(result * 0.15);
	  return (this._premiumDelegate)
	    ? this._premiumDelegate.extendBasePrice(result)
	    : result;
	}

	get hasDinner() {
	  return (this._premiumDelegate)
	    ? this._premiumDelegate.hasDinner
	    : undefined;
	}
	
	_bePremium(extras) {
	  this._premiumDelegate = new PremiumBookingDelegate(this, extras);
	}
}

范例:取代继承体系

重构前

function createBird(data) {
 switch (data.type) {
  case 'EuropeanSwallow':
   return new EuropeanSwallow(data);
  case 'AfricanSwallow':
   return new AfricanSwallow(data);
  case 'NorweigianBlueParrot':
   return new NorwegianBlueParrot(data);
  default:
   return new Bird(data);
 }
}

class Bird {
 constructor(data) {
  this._name = data.name;
  this._plumage = data.plumage;
 }
 get name()  {return this._name;}

 get plumage() {
  return this._plumage || "average";
 }
 get airSpeedVelocity() {return null;}
}

class EuropeanSwallow extends Bird {
 get airSpeedVelocity() {return 35;}
}

class AfricanSwallow extends Bird {
 constructor(data) {
  super (data);
  this._numberOfCoconuts = data.numberOfCoconuts;
 }
 get airSpeedVelocity() {
  return 40 - 2 * this._numberOfCoconuts;
 }
}

class NorwegianBlueParrot extends Bird {
 constructor(data) {
  super (data);
  this._voltage = data.voltage;
  this._isNailed = data.isNailed;
 }

 get plumage() {
  if (this._voltage > 100) return "scorched";
  else return this._plumage || "beautiful";
 }
 get airSpeedVelocity() {
  return (this._isNailed) ? 0 : 10 + this._voltage / 10;
 }
}

重构后

function createBird(data) {
 switch (data.type) {
 
  case 'NorweigianBlueParrot':
   return new NorwegianBlueParrot(data);
  default:
   return new Bird(data);
 }
}

class Bird {
	constructor(data) {
	 this._name = data.name;
	 this._plumage = data.plumage;
	 this._speciesDelegate = this.selectSpeciesDelegate(data);
	}
	
	 selectSpeciesDelegate(data) {
		 switch(data.type) {
		  case 'EuropeanSwallow':
		   return new EuropeanSwallowDelegate(data,this);
		  case 'AfricanSwallow':
		   return new AfricanSwallowDelegate(data,this);
		  case 'NorweigianBlueParrot':
		   return new NorwegianBlueParrotDelegate(data,this);
		  default: return new SpeciesDelegate(data, this);
		 }
	}
 
	 get name()  {return this._name;}

	 get plumage() {return this._speciesDelegate.plumage;}
	 get airSpeedVelocity() {return this._speciesDelegate.airSpeedVelocity;}
}

class SpeciesDelegate {
 constructor(data, bird) {
  this._bird = bird;
 }
 get plumage() {
  return this._bird._plumage || "average";
 }
 get airSpeedVelocity() {return null;}
}

class EuropeanSwallowDelegate extends SpeciesDelegate {
	get airSpeedVelocity() {return 35;}
}

class AfricanSwallowDelegate extends SpeciesDelegate{
	constructor(data,bird) {
		super(data,bird)
		this._numberOfCoconuts = data.numberOfCoconuts;
	}
	get airSpeedVelocity() {
	  return 40 - 2 * this._numberOfCoconuts;
	}
}

class NorwegianBlueParrotDelegate extends SpeciesDelegate{
	constructor(data,bird) {
		super(data,bird)
		this._voltage = data.voltage;
		this._isNailed = data.isNailed;
	}
	get airSpeedVelocity() {
	  return (this._isNailed) ? 0 : 10 + this._voltage / 10;
	}
	get plumage(){
		if (this._voltage > 100) return "scorched";
		else return this._bird._plumage || "beautiful";
	}
}

以委托取代超类

反向重构

动机

[!info] 合理的继承应该符合这个重要特征:子类的所有实例都应该是超类的实例,通过超类的接口来使用子类的实例应 该完全不出问题。 即便在子类继承是合理的建模方式的情况下,如果子类与超类之间的耦合过 强,超类的变化很容易破坏子类的功能,我还是会使用以委托取代超类。

范例

重构前

class CatalogItem{
	constructor(id, title, tags) {
	 this._id = id;
	 this._title = title;
	 this._tags = tags;
	}
	
	get id() {return this._id;}
	get title() {return this._title;}
	hasTag(arg) {return this._tags.includes(arg);}
}

class Scroll extends CatalogItem{
	constructor(id, title, tags, dateLastCleaned) {
	 super(id, title, tags);
	 this._lastCleaned = dateLastCleaned;
	}
	
	needsCleaning(targetDate) {
	 const threshold = this.hasTag("revered") ? 700 : 1500;
	 return this.daysSinceLastCleaning(targetDate) > threshold ;
	}
	daysSinceLastCleaning(targetDate) {
	 return this._lastCleaned.until(targetDate, ChronoUnit.DAYS);
	}
}

重构后

class CatalogItem{
	constructor(id, title, tags) {
	 this._id = id;
	 this._title = title;
	 this._tags = tags;
	}
	
	get id() {return this._catalogItem.id;}
	get title() {return this._catalogItem.title;}
	hasTag(aString) {return this._catalogItem.hasTag(aString);}
}

class Scroll {
	constructor(id, title, tags, dateLastCleaned) {
	 this._catalogItem = new CatalogItem(id, title, tags);
	 this._lastCleaned = dateLastCleaned;
	}
	get id() {return this._id;}
	get title() {return this._catalogItem.title;}
	hasTag(aString) {return this._catalogItem.hasTag(aString);}
	
	needsCleaning(targetDate) {
	 const threshold = this.hasTag("revered") ? 700 : 1500;
	 return this.daysSinceLastCleaning(targetDate) > threshold ;
	}
	daysSinceLastCleaning(targetDate) {
	 return this._lastCleaned.until(targetDate, ChronoUnit.DAYS);
	}
}