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

65 阅读5分钟

搬移特性

搬移函数

反向重构

动机

[!info] 搬移函数最直接的一个动因是,它频繁引用其他上下文中的元素,而对自身上下文中的元素却关心甚少。此时,让它去与那些更亲密的元素相会,通常能取得更好的封装效果,因为系统别处就可以减少对当前模块的依赖。 范例:搬移内嵌函数至顶层

范例

重构前:

  function trackSummary(points) {
   const totalTime = calculateTime();
   const totalDistance = calculateDistance();
   const pace = totalTime / 60 / totalDistance ;
   return {
	time: totalTime,
	distance: totalDistance,
	pace: pace
   };
  
   function calculateDistance() {
	let result = 0;
	for (let i = 1; i < points.length; i++) {
	 result += distance(points[i-1],  points[i]);
	}
	return result;
   }
  
   function distance(p1,p2) { ... }
   function radians(degrees) { ... }
   function calculateTime() { ... }
  
  }

重构后:

   function trackSummary(points) {
	 const totalTime = calculateTime();
	 const pace = totalTime / 60 / totalDistance(points) ;
	 return {
	  time: totalTime,
	  distance: totalDistance(points),
	  pace: pace
	 };
  }
  
  function totalDistance(points) {
   let result = 0;
   for (let i = 1; i < points.length; i++) {
	result += distance(points[i-1], points[i]);
   }
   return result;
  }
  
  function trackSummary(points) { ... }
  function totalDistance(points) { ... }
  function distance(p1,p2) { ... }
  function radians(degrees) { ... }

范例:在类之间搬移函数

重构前:

  class Account{
	get bankCharge() {
	 let result = 4.5;
	 if (this._daysOverdrawn > 0) result += this.overdraftCharge;
	 return result;
	}
  
	get overdraftCharge() {
	 if (this.type.isPremium) {
	  const baseCharge = 10;
	  if (this.daysOverdrawn <= 7)
	   return baseCharge;
	  else
	   return baseCharge + (this.daysOverdrawn - 7) * 0.85;
	 }
	 else
	  return this.daysOverdrawn * 1.75;
	}
  }

重构后:

  class Account{
	get bankCharge() {
	 let result = 4.5;
	 if (this._daysOverdrawn > 0) result += this.overdraftCharge;
	 return result;
  }
  
	get overdraftCharge() {
	 return this.type.overdraftCharge(this.daysOverdrawn);
	}
  }
  
  class AccountType{
	overdraftCharge(daysOverdrawn) {
	 if (this.isPremium) {
	  const baseCharge  =  10;
	  if (daysOverdrawn <= 7)
	   return baseCharge;
	  else
	   return baseCharge + (daysOverdrawn - 7) * 0.85;
	 }
	 else
	  return daysOverdrawn * 1.75;
	}
  }

搬移字段

反向重构

动机

[!info] 如果每当调用某个函数时,除了传入一个记录参数,还总是需要同时传入另一条记录的某个字段一起作为参数。那么总是一同出现、一同作为函数参数传递的数据,最好是规整到同一条记录中,以体现它们之间的联系。

范例

重构前:

  class Customer{
	constructor(name, discountRate) {
	 this._name = name;
	 this._discountRate = discountRate;
	 this._contract = new CustomerContract(dateToday());
	}
	get discountRate() {return this._discountRate;}
	becomePreferred() {
	 this._discountRate += 0.03;
	 // other nice things
	}
	applyDiscount(amount) {
	 return amount.subtract(amount.multiply(this._discountRate));
	}
  }
  
  class CustomerContract{
	constructor(startDate) {
	  this._startDate = startDate;
	}
  }
			  

重构后:

  class Customer{
	  constructor(name, discountRate) {
		this._name = name;
		this._setDiscountRate(discountRate);
		this._contract = new CustomerContract(dateToday());
	  }
	  get discountRate() {return this._contract.discountRate;}
	_setDiscountRate(aNumber) {this._contract.discountRate = aNumber;}
  }
  
  class CustomerContract{
	constructor(startDate, discountRate) {
	   this._startDate = startDate;
	   this._discountRate = discountRate;
	  }
	  get discountRate()  {return this._discountRate;}
	  set discountRate(arg) {this._discountRate = arg;}
  }

范例:搬移字段到共享对象

重构前

  class Account{
	constructor(number, type, interestRate) {
	 this._number = number;
	 this._type = type;
	 this._interestRate = interestRate;
	}
	get interestRate() {return this._interestRate;}
  }
  
  class AccountType{
	constructor(nameString) {
	  this._name = nameString;
	}
  }

重构后

  class Account{
	constructor(number, type, interestRate) {
	  this._number = number;
	  this._type = type;
	}
	get interestRate() {return this._type.interestRate;}
  }
  
  class AccountType{
	constructor(nameString, interestRate) {
	  this._name = nameString;
	  this._interestRate = interestRate;
	}
	get interestRate() {return this._interestRate;}
  }

搬移语句到函数

反向重构

[[重构:改善既有的代码设计#搬移语句到调用者|搬移语句到调用者]]

动机

[!info] 如果调用某个函数时,总有一些相同的代码也需要每次执行,这是就需要考虑将此段代码合并到函数里头。 如果某些语句与一个函数放在一起更像一个整体,并且更有助于理解,那就可以将语句搬移到函数里去。

范例

重构前:

  function renderPerson(outStream, person) {
	const result = [];
	result.push(`<p>${person.name}</p>`);
	result.push(renderPhoto(person.photo));
	result.push(`<p>title: ${person.photo.title}</p>`);
	result.push(emitPhotoData(person.photo));
	return result.join("\n");
  }
  function photoDiv(p) {
	return [
	  "<div>",
	  `<p>title:  ${p.title}</p>`,
	  emitPhotoData(p),
	  "</div>",
	].join("\n");
  }
  
  function emitPhotoData(aPhoto) {
	const result = [];
	result.push(`<p>location: ${aPhoto.location}</p>`);
	result.push(`<p>date: ${aPhoto.date.toDateString()}</p>`);
	return result.join("\n");
  }

重构后:

  function renderPerson(outStream, person) {
	const result = [];
	result.push(`<p>${person.name}</p>`);
	result.push(renderPhoto(person.photo));
	result.push(emitPhotoData(person.photo));
	return result.join("\n");
  }
  
  function photoDiv(aPhoto) {
	return ["<div>", emitPhotoData(aPhoto), "</div>"].join("\n");
  }
  
  function emitPhotoData(aPhoto) {
	return [
	  `<p>title: ${aPhoto.title}</p>`,
	  `<p>location: ${aPhoto.location}</p>`,
	  `<p>date: ${aPhoto.date.toDateString()}</p>`,
	].join("\n");
  }

搬移语句到调用者

反向重构

[[重构:改善既有的代码设计#搬移语句到函数|搬移语句到函数]]

动机

[!info] 如果以往在多个地方共用的行为,如今需要在某些调用点面前表现出不同的行为。那么我们得把表现不同的行为从函数里挪出,并搬移到其调用处。这个重构手法比较适合处理边界仅有些许偏移的场景。

范例

重构前:

	function renderPerson(outStream, person) {
   outStream.write(`<p>${person.name}</p>\n`);
   renderPhoto(outStream, person.photo);
   emitPhotoData(outStream, person.photo);
  }
  
  function listRecentPhotos(outStream, photos) {
   photos
	.filter(p => p.date > recentDateCutoff())
	.forEach(p => {
	 outStream.write("<div>\n");
	 emitPhotoData(outStream, p);
	 outStream.write("</div>\n");
	});
  }
  
  function emitPhotoData(outStream, photo) {
   outStream.write(`<p>title: ${photo.title}</p>\n`);
   outStream.write(`<p>date: ${photo.date.toDateString()}</p>\n`);
   outStream.write(`<p>location: ${photo.location}</p>\n`);
  }

重构后:

  function renderPerson(outStream, person) {
   outStream.write(`<p>${person.name}</p>\n`);
   renderPhoto(outStream, person.photo);
   emitPhotoData(outStream, person.photo);
   outStream.write(`<p>location: ${person.photo.location}</p>\n`);
  }
  
  function listRecentPhotos(outStream, photos) {
   photos
	.filter(p => p.date > recentDateCutoff())
	.forEach(p => {
	 outStream.write("<div>\n");
	 emitPhotoData(outStream, p);
	 outStream.write(`<p>location: ${p.location}</p>\n`);
	 outStream.write("</div>\n");
	});
  }
  
  function emitPhotoData(outStream, photo) {
   outStream.write(`<p>title: ${photo.title}</p>\n`);
   outStream.write(`<p>date: ${photo.date.toDateString()}</p>\n`);
  }

以函数调用取代内联代码

反向重构

动机

[!info] 借助一些库,很多语句能够直接用简洁的函数替代。

范例

重构前:

  let appliesToMass = false;
  for (const s of states) {
	if (s === "MA") appliesToMass = true;
  }

重构后:

  appliesToMass = states.includes("MA");

移动语句

反向重构

动机

[!info] 如果有几行代码取用了同一个数据结构,那么最好是让它们在一起出现,而不是夹杂在取用其他数据结构的代码中间。

范例: 包含条件逻辑的移动

重构前:

  let result;
  if (availableResources.length === 0) {
	result = createResource();
	allocatedResources.push(result);
  } else {
	result = availableResources.pop();
	allocatedResources.push(result);
  }
  return result;

重构后:

  let result;
  if (availableResources.length === 0) {
	result = createResource();
  } else {
	result = availableResources.pop();
  }
  allocatedResources.push(result);
  return result;

拆分循环

反向重构

动机

[!info] 如果代码在一次循环中做了两件不同的事,那么每当需要修改循环时,都得同时理解这两件事情。如果能够将循环拆分,让一个循环只做一件事情,那就能确保每次修改时只需要理解要修改的那块代码的行为就可以了。

范例

重构前:

  let youngest = people[0] ? people[0].age : Infinity;
  let totalSalary = 0;
  for (const p of people) {
   if (p.age < youngest) youngest = p.age;
   totalSalary += p.salary;
  }
  
  return `youngestAge: ${youngest}, totalSalary: ${totalSalary}`;

重构后:

  function totalSalary() {
   return people.reduce((total,p) => total + p.salary, 0);
  }
  function youngestAge() {
   return Math.min(...people.map(p => p.age));
  }
  
  return `youngestAge: ${youngestAge()}, totalSalary: ${totalSalary()}`;

以管道取代循环

反向重构

动机

[!info] 类似于map,filter这类函数即为管道函数。使用管道函数的好处有两点:1. 管道函数返回的依然是一个集合,因此可以进行链式调用,这也是所谓“管道”的意义所在。2. 使用管道函数能够很好的提供代码可读性。

范例

重构前:

  function acquireData(input) {
	const lines = input.split("\n");
	let firstLine = true;
	const result = [];
	for (const line of lines) {
	  if (firstLine) {
		firstLine = false;
		continue;
	  }
	  if (line.trim() === "") continue;
	  const record = line.split(",");
	  if (record[1].trim() === "India") {
		result.push({ city: record[0].trim(), phone: record[2].trim() });
	  }
	}
	return result;
  }

重构后:

  function acquireData(input) {
   const lines = input.split("\n");
   return lines
	  .slice (1)
	  .filter (line => line.trim() !== "")
	  .map   (line => line.split(","))
	  .filter (fields => fields[1].trim() === "India")
	  .map   (fields => ({city: fields[0].trim(), phone: fields[2].trim()}))
	  ;
  }

移除死代码

反向重构

动机

[!info] 事实上,我们部署到生产环境甚至是用户设备上的代码,从来未因代码量太大而产生额外费用。就算有几行用不上的代码,似乎也不会因此拖慢系统速度,或者占用过多的内存,大多数现代的编译器还会自动将无用的代码移除。但当你尝试阅读代码、理解软件的运作原理时,无用代码确实会带来很多额外的思维负担。它们周围没有任何警示或标记能告诉程序员,让他们能够放心忽略这段函数,因为已经没有任何地方使用它了。当程序员花费了许多时间,尝试理解它的工作原理时,却发现无论怎么修改这段代码都无法得到期望的输出。

一旦代码不再被使用,我们就该立马删除它。有可能以后又会需要这段代码,但我从不担心这种情况;就算真的发生,我也可以从版本控制系统里再次将它翻找出来。如果我真的觉得日后它极有可能再度启用,那还是要删掉它,只不过可以在代码里留一段注释,提一下这段代码的存在,以及它被移除的那个提交版本号——但老实讲,我已经记不得我上次撰写这样的注释是什么时候了,当然也未曾因为不写而感到过遗憾。

在以前,业界对于死代码的处理态度多是注释掉它。在版本控制系统还未普及、用起来又不太方便的年代,这样做有其道理;但现在版本控制系统已经相当普及。如今哪怕是一个极小的代码库我都会把它放进版本控制,这些无用代码理应可以放心清理了。

范例

重构前:

重构后: