30个重构技巧

发布时间 2023-11-27 10:39:54作者: 咖啡机(K.F.J)

  所有技巧来源于《重构:改善既有代码的设计(第2版)

第一组重构

1)提炼函数

  

  “将意图与实现分开”:如果你需要花时间浏览一段代码才能弄清它到底在干什么,那么就应该将其提炼到一个函数中,并根据它所做的事为其命名。

function printOwing(invoice) {
  printBanner();
  let outstanding = calculateOutstanding();
  //print details
  console.log(`name: ${invoice.customer}`);
  console.log(`amount: ${outstanding}`);
}
function printOwing(invoice) {
  printBanner();
  let outstanding = calculateOutstanding();
  printDetails(outstanding);
  function printDetails(outstanding) {
    console.log(`name: ${invoice.customer}`);
    console.log(`amount: ${outstanding}`);  
  }
}

2)内联函数

  有时候你会遇到某些函数,其内部代码和函数名称同样清晰易读。也可能你重构了该函数的内部实现,使其内容和其名称变得同样清晰。若果真如此,你就应该去掉这个函数,直接使用其中的代码。间接性可能带来帮助,但非必要的间接性总是让人不舒服。

function getRating(driver) {
  return moreThanFiveLateDeliveries(driver) ? 2 : 1; 
}
function moreThanFiveLateDeliveries(driver) {
  return driver.numberOfLateDeliveries > 5;
}
function getRating(driver) {
  return (driver.numberOfLateDeliveries > 5) ? 2 : 1; 
}

3)提炼变量

  表达式有可能非常复杂而难以阅读。这种情况下,局部变量可以帮助我们将表达式分解为比较容易管理的形式。

return order.quantity * order.itemPrice -
 Math.max(0, order.quantity - 500) * order.itemPrice * 0.05 +
 Math.min(order.quantity * order.itemPrice * 0.1, 100);
const basePrice = order.quantity * order.itemPrice;
const quantityDiscount = Math.max(0, order.quantity - 500) * order.itemPrice * 0.05;
const shipping = Math.min(basePrice * 0.1, 100);
return basePrice - quantityDiscount + shipping;

4)内联变量

  有时候,这个变量并不比表达式本身更具表现力。还有些时候,变量可能会妨碍重构附近的代码。

let basePrice = anOrder.basePrice;
return (basePrice > 1000);
return anOrder.basePrice > 1000;

5)改变函数声明

  一个好名字能让我一眼看出函数的用途,而不必查看其实现代码。

function circum(radius) {...}
function circumference(radius) {...}

6)封装变量

  函数相对容易调整一些,因为函数只有一种用法,就是调用。在改名或搬移函数的过程中,总是可以比较容易地保留旧函数作为转发函数(即旧代码调用旧函数,旧函数再调用新函数)。

let defaultOwner = {firstName: "Martin", lastName: "Fowler"};
let defaultOwnerData = {firstName: "Martin", lastName: "Fowler"};
export function defaultOwner() {
  return defaultOwnerData;
}
export function setDefaultOwner(arg) {
  defaultOwnerData = arg;
}

7)变量改

  好的命名是整洁编程的核心。变量可以很好地解释一段程序在干什么——如果变量名起得好的话。

let a = height * width;
let area = height * width;

8)参数对象

  将数据组织成结构是一件有价值的事,因为这让数据项之间的关系变得明晰。使用新的数据结构,参数列表也能缩短。并且经过重构之后,所有使用该数据结构的函数都会通过同样的名字来访问其中的元素,从而提升代码的一致性。

function amountInvoiced(startDate, endDate) {...}
function amountReceived(startDate, endDate) {...}
function amountOverdue(startDate, endDate) {...}
function amountInvoiced(aDateRange) {...}
function amountReceived(aDateRange) {...}
function amountOverdue(aDateRange) {...}

9)函数组合成类

  

  如果发现一组函数形影不离地操作同一块数据(通常是将这块数据作为参数传递给函数),我就认为,是时候组建一个类了。类能明确地给这些函数提供一个共用的环境,在对象内部调用这些函数可以少传许多参数,从而简化函数调用,并且这样一个对象也可以更方便地传递给系统的其他部分。

function base(aReading) {...}
function taxableCharge(aReading) {...}
function calculateBaseCharge(aReading) {...}
class Reading {
  base() {...}
  taxableCharge() {...}
  calculateBaseCharge() {...}
}

10)函数组合成变换

  

  我更愿意把所有计算派生数据的逻辑收拢到一处,这样始终可以在固定的地方找到和更新这些逻辑,避免到处重复。

function base(aReading) {...}
function taxableCharge(aReading) {...}
function enrichReading(argReading) {
  const aReading = _.cloneDeep(argReading);
  aReading.baseCharge = base(aReading);
  aReading.taxableCharge = taxableCharge(aReading);
  return aReading;
}

11)拆分阶段

  

  每当看见一段代码在同时处理两件不同的事,我就想把它拆分成各自独立的模块,因为这样到了需要修改的时候,我就可以单独处理每个主题,而不必同时在脑子里考虑两个不同的主题。

const orderData = orderString.split(/\s+/);
const productPrice = priceList[orderData[0].split("-")[1]];
const orderPrice = parseInt(orderData[1]) * productPrice;
const orderRecord = parseOrder(order);
const orderPrice = price(orderRecord, priceList);
function parseOrder(aString) {
  const values = aString.split(/\s+/);
  return ({
    productID: values[0].split("-")[1],
    quantity: parseInt(values[1]),
  });
}
function price(order, priceList) {
  return order.quantity * priceList[order.productID]; 
}

搬移特性

12)搬移语句到函数

  

  要维护代码库的健康发展,需要遵守几条黄金守则,其中最重要的一条当属“消除重复”。如果我发现调用某个函数时,总有一些相同的代码也需要每次执 行,那么我会考虑将此段代码合并到函数里头。这样,日后对这段代码的修改只需改一处地方,还能对所有调用者同时生效。

result.push(`<p>title: ${person.photo.title}</p>`);
result.concat(photoData(person.photo));
function photoData(aPhoto) {
 return [
  `<p>location: ${aPhoto.location}</p>`,   
  `<p>date: ${aPhoto.date.toDateString()}</p>`,
 ]; 
}
result.concat(photoData(person.photo));
function photoData(aPhoto) {
 return [
  `<p>title: ${aPhoto.title}</p>`,   
  `<p>location: ${aPhoto.location}</p>`,   
  `<p>date: ${aPhoto.date.toDateString()}</p>`,  
 ];
}

13)以函数调用取代内联代码

  

  善用函数可以帮助我将相关的行为打包起来,这对于提升代码的表达力大有裨益—— 一个命名良好的函数,本身就能极好地解释代码的用途,使读者不必了解其细节。

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

14)移动语句

  让存在关联的东西一起出现,可以使代码更容易理解。

const pricingPlan = retrievePricingPlan();
const order = retreiveOrder();
let charge;
const chargePerUnit = pricingPlan.unit;
const pricingPlan = retrievePricingPlan();
const chargePerUnit = pricingPlan.unit;
const order = retreiveOrder();
let charge;

15)拆分循环

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

  

let averageAge = 0;
let totalSalary = 0;
for (const p of people) {
  averageAge += p.age;
  totalSalary += p.salary;
}
averageAge = averageAge / people.length;
let totalSalary = 0;
for (const p of people) {
  totalSalary += p.salary; 
}
let averageAge = 0;
for (const p of people) {
  averageAge += p.age;
}
averageAge = averageAge / people.length;

16)以管道取代循环

  一些逻辑如果采用集合管道来编写,代码的可读性会更强——我只消从头到尾阅读一遍代码,就能弄清对象在管道中间的变换过程。

  

const names = [];
for (const i of input) {
  if (i.job === "programmer")
    names.push(i.name);
}
const names = input
  .filter(i => i.job === "programmer")
  .map(i => i.name)

17)移除死代码

  当你尝试阅读代码、理解软件的运作原理时,无用代码确实会带来很多额外的思维负担。它们周围没有任何警示或标记能告诉程序员,让他们能够放心忽略这段函数,因为已经没有任何地方使用它了。当程序员花费了许多时间,尝试理解它的工作原理时,却发现无论怎么修改这段代码都无法得到期望的输出。

if(false) {
   doSomethingThatUsedToMatter();
}

重新组织数据

18)拆分变量

  有很多变量用于保存一段冗长代码的运算结果,以便稍后使用。这种变量应该只被赋值一次。如果它们被赋值超过一次,就意味它们在函数中承担了一个以上的责任。如果变量承担多个责任,它就应该被替换(分解)为多个变量,每个变量只承担一个责任。

let temp = 2 * (height + width);
console.log(temp);
temp = height * width;
console.log(temp);
const perimeter = 2 * (height + width);
console.log(perimeter);
const area = height * width;
console.log(area);

19)将引用对象改为值对象

  如果将内部对象视为引用对象,在更新其属性时,我会保留原对象不动,更新内部对象的属性;如果将其视为值对象,我就会替换整个内部对象,新换上的对象会有我想要的属性值。

  

class Product {
  applyDiscount(arg) {
    this._price.amount -= arg;
  }
class Product {
  applyDiscount(arg) {
    this._price = new Money(this._price.amount - arg, this._price.currency);
  }

20)将值对象改为引用对象

  如果我想在几个对象之间共享一个对象,以便几个对象都能看见对共享对象的修改,那么这个共享的对象就应该是引用。如果共享的数据需要更新,将其复制多份的做法就会遇到巨大的困难。

  

let customer = new Customer(customerData);
let customer = customerRepository.get(customerData.id);

简化条件逻辑

21)分解条件表达式

  复杂的条件逻辑是最常导致复杂度上升的地点之一。对于条件逻辑,将每个分支条件分解成新函数还可以带来更多好处:可以突出条件逻辑,更清楚地表明每个分支的作用,并且突出每个分支的原因。

if (!aDate.isBefore(plan.summerStart) && !aDate.isAfter(plan.summerEnd))
  charge = quantity * plan.summerRate;
else
  charge = quantity * plan.regularRate + plan.regularServiceCharge;
if (summer())
  charge = summerCharge();
else
  charge = regularCharge();

22)合并条件表达式

  检查条件各不相同,最终行为却一致。如果发现这种情况,就应该使用“逻辑或”和“逻辑与”将它们合并为一个条件表达式。

if (anEmployee.seniority < 2) return 0;
if (anEmployee.monthsDisabled > 12) return 0;
if (anEmployee.isPartTime) return 0;
if (isNotEligibleForDisability()) return 0;
function isNotEligibleForDisability() {
  return ((anEmployee.seniority < 2)
        || (anEmployee.monthsDisabled > 12)      
        || (anEmployee.isPartTime));
}

23)以卫语句取代嵌套条件表达式

  以卫语句取代嵌套条件表达式的精髓就是:给某一条分支以特别的重视。如果使用if-then-else结构,你对if分支和else分支的重视是同等的。这样的代码结构传递给阅读者的消息就是:各个分支有同样的重要性。卫语句就不同了,它告诉阅读者:“这种情况不是本函数的核心逻辑所关心的,如果它真发生了,请做一些必要的整理工作,然后退出。”

function getPayAmount() {
 let result;
 if (isDead)
  result = deadAmount();
  else {
    if (isSeparated)
      result = separatedAmount();
    else {
      if (isRetired)
        result = retiredAmount();    
      else
        result = normalPayAmount();
    }
  }
  return result;
}
function getPayAmount() {
  if (isDead) return deadAmount();
  if (isSeparated) return separatedAmount();  
  if (isRetired) return retiredAmount();  
  return normalPayAmount();
}

重构API

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

  任何有返回值的函数,都不应该有看得到的副作用—— 命令与查询分离。如果某个函数只是提供一个值,没有任何看得到的副作用,那么这是一个很有价值的东西。我可以任意调用这个函数,也可以把调用动作搬到调用函数的其他地方。这种函数的测试也更容易。

  

function getTotalOutstandingAndSendBill() {
  const result = customer.invoices.reduce((total, each) => each.amount + total, 0);
  sendBill();
  return result;
}
function totalOutstanding() {
  return customer.invoices.reduce((total, each) => each.amount + total, 0);
}
function sendBill() {
  emailGateway.send(formatBill(customer));
}

25)函数参数化

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

  

function tenPercentRaise(aPerson) {
  aPerson.salary = aPerson.salary.multiply(1.1);
}
function fivePercentRaise(aPerson) {
  aPerson.salary = aPerson.salary.multiply(1.05);
}
function raise(aPerson, factor) {
  aPerson.salary = aPerson.salary.multiply(1 + factor);
}

26)移除标记参数

  “标记参数”是这样的一种参数:调用者用它来指示被调函数应该执行哪一部分逻辑。我不喜欢标记参数,因为它们让人难以理解到底有哪些函数可以调用、应该怎么调用。拿到一份API以后,我首先看到的是一系列可供调用的函数,但标记参数却隐藏了函数调用中存在的差异性。使用这样的函数,我还得弄清标记参数有哪些可用的值。布尔型的标记尤其糟糕,因为它们不能清晰地传达其含义—— 在调用一个函数时,我很难弄清true到底是什么意思。如果明确用一个函数来完成一项单独的任务,其含义会清晰得多。

  

function setDimension(name, value) {
  if (name === "height") {
    this._height = value;
    return;
  }
  if (name === "width") {
    this._width = value;   
    return;
  }
}
function setHeight(value) {this._height = value;}
function setWidth (value) {this._width = value;}

27)保持对象完整

  如果我看见代码从一个记录结构中导出几个值,然后又把这几个值一起传递给一个函数,我会更愿意把整个记录传给这个函数,在函数体内部导出所需的值。“传递整个记录”的方式能更好地应对变化:如果将来被调的函数需要从记录中导出更多的数据,我就不用为此修改参数列表。并且传递整个记录也能缩短参数列表,让函数调用更容易看懂。如果有很多函数都在使用记录中的同一组数据,处理这部分数据的逻辑常会重复,此时可以把这些处理逻辑搬移到完整对象中去。

const low = aRoom.daysTempRange.low;
const high = aRoom.daysTempRange.high;
if (aPlan.withinRange(low, high))
if (aPlan.withinRange(aRoom.daysTempRange))

28)以查询取代参数

  参数列表应该尽量避免重复,并且参数列表越短就越容易理解。如果调用函数时传入了一个值,而这个值由函数自己来获得也是同样容易,这就是重复。

availableVacation(anEmployee, anEmployee.grade);
function availableVacation(anEmployee, grade) {
  // calculate vacation...
availableVacation(anEmployee)
function availableVacation(anEmployee) {
  const grade = anEmployee.grade;
  // calculate vacation...

29)以命令取代函数

  

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

function score(candidate, medicalExam, scoringGuide) {
  let result = 0;
  let healthLevel = 0;
  // long body code
}
class Scorer {
  constructor(candidate, medicalExam, scoringGuide) {
    this._candidate = candidate;
    this._medicalExam = medicalExam;
    this._scoringGuide = scoringGuide;
  }
  execute() {
    this._result = 0;
    this._healthLevel = 0;
    // long body code
  } 
}

30)以函数取代命令

  借助命令对象,可以轻松地将原本复杂的函数拆解为多个方法,彼此之间通过字段共享状态;拆解后的方法可以分别调用;开始调用之前的数据状态也可以逐步构建。但这种强大是有代价的。大多数时候,我只是想调用一个函数,让它完成自己的工作就好。如果这个函数不是太复杂,那么命令对象可能显得费而不惠,我就应该考虑将其变回普通的函数。

class ChargeCalculator {
 constructor (customer, usage){
   this._customer = customer;
   this._usage = usage;
 }
 execute() {
   return this._customer.rate * this._usage;  
 }
}
function charge(customer, usage) {
  return customer.rate * usage;
}