一、基础重构

1. 提炼函数 (Extract Function)

1.1 使用场景

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

1.2 使用步骤
  • 创造一个新函数,根据这个函数的意图来对它命名
  • 将待提炼的代码从源函数复制到新建的目标函数中。
  • 仔细检查提炼出的代码,看看其中是否引用了作用域限于源函数、在提炼出的新函数中访问不到的变量。若是,以参数的形式将它们传递给新函数。
  • 所有变量都处理完之后,编译。
  • 在源函数中,将被提炼代码段替换为对目标函数的调用。
  • 测试。
  • 查看其他代码是否有与被提炼的代码段相同或相似之 处。如果有,考虑使用以函数调用取代内联代码,令其调用提炼出的新函数。
1.3 范例
function printOwing(invoice) {  
  let outstanding = 0;

  // calculate outstanding
  for (const o of invoice.orders) {   
    outstanding += o.amount;
  }
  
  // record due date
  const today = Clock.today;
  invoice.dueDate = new Date(today.getFullYear(), today.getMonth(), today.getDate( ) + 30);
  console.log(`name: ${invoice.customer}`);   
  console.log(`amount: ${outstanding}`);
  console.log(`due: ${invoice.dueDate.toLocaleDateString()}`); 
}


// 重构
function printOwing(invoice) {  
  let outstanding = calculateOutstanding(invoice)
  recordDueDate(invoice)
  printDetails(invoice, outstanding)
}

function calculateOutstanding(invoice) {  
  let outstanding = 0;
  for (const o of invoice.orders) {   
    outstanding += o.amount;
  }
     return outstanding;
}

function recordDueDate(invoice) {
  const today = Clock.today;
  invoice.dueDate = new Date(today.getFullYear(), today.getMonth(), today.getDate( ) + 30);
}

function printDetails(invoice, outstanding) {
  console.log(`name: ${invoice.customer}`);   
  console.log(`amount: ${outstanding}`);
  console.log(`due: ${invoice.dueDate.toLocaleDateString()}`); 
}

2. 内联函数 (Inline Function)

2.1 使用场景

如果代码中有太多间接层,使得系统中的所有函数都似乎只是对另一个函数的简单委托,造成我在这些委托动作之间晕头转向,那么我通常都会使用内联函数。

2.2 使用步骤
  • 检查函数,确定它不具多态性。
  • 找出这个函数的所有调用点。
  • 将这个函数的所有调用点都替换为函数本体。
  • 每次替换之后,执行测试。
  • 删除该函数的定义。
2.3 范例
function reportLines(aCustomer) {  
  const lines = [];  
  gatherCustomerData(lines, aCustomer);  
  return lines;
}

function gatherCustomerData(out, aCustomer) {  
  out.push(["name", aCustomer.name]);  
  out.push(["location", aCustomer.location]); 
}


// 重构
function reportLines(aCustomer) {
     const lines = [];
     lines.push(["name", aCustomer.name]);  
  lines.push(["location", aCustomer.location]);  
  return lines;
}

3. 提炼变量 (Extract Variable)

3.1 使用场景

表达式有可能非常复杂而难以阅读。

3.2 使用步骤
  • 确认要提炼的表达式没有副作用。
  • 声明一个不可修改的变量,把你想要提炼的表达式复制 一份,以该表达式的结果值给这个变量赋值。
  • 用这个新变量取代原来的表达式。
  • 测试。
3.3 范例
在一个函数中,将它们提炼成变量
function price(order) {
 return order.quantity * order.itemPrice - Math.max(0, order.quantity - 500) * order.itemPrice * 0.05 +           Math.min(order.quantity * order.itemPrice * 0.1, 100);
}


// 重构
function price(order) {
  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;
}
在一个类中,将它们提炼成方法
class Order {  
 constructor(aRecord) {   
   this._data = aRecord;  
 }
  
 get quantity() {
   return this._data.quantity;
 }  
  
 get itemPrice() {
   return this._data.itemPrice;
 }
   
 get price() {
  return this.quantity * this.itemPrice - Math.max(0, this.quantity - 500) * this.itemPrice * 0.05 +    Math.min(this.quantity * this.itemPrice * 0.1, 100);
 }
}


// 重构
class Order {
  constructor(aRecord) {
    this._data = aRecord;
  }

  get quantity() {
    return this._data.quantity;
  }  

  get itemPrice() {
    return this._data.itemPrice;
  }

  get price() {
    return this.basePrice - this.quantityDiscount + this.shipping;
  }

  get basePrice() {
    return this.quantity * this.itemPrice;
  }

  get quantityDiscount() {
    return Math.max(0, this.quantity - 500) * this.itemPrice * 0.05;
  }

  get shipping() {
    return Math.min(this.basePrice * 0.1, 100);
  } 
}

4. 内联变量 (Inline Variable)

4.1 使用场景

有些时候,变量并不比表达式本身更具表现力,可能会妨碍重构附近的代码。若果真如此,就应该通过内联的手法消除变量。

4.2 使用步骤
  • 检查确认变量赋值语句的右侧表达式没有副作用。
  • 如果变量没有被声明为不可修改,先将其变为不可修改,并执行测试。
  • 找到第一处使用该变量的地方,将其替换为直接使用赋值语句的右侧表达式。
  • 测试。
  • 重复前面两步,逐一替换其他所有使用该变量的地方。
  • 删除该变量的声明点和赋值语句。
  • 测试。
4.3 范例
let basePrice = anOrder.basePrice;
return (basePrice > 1000);


// 重构
return anOrder.basePrice > 1000;

5. 改变函数声明 (Change Function Declaration)

5.1 使用场景

如果我一个函数、函数的参数的名字不能一眼看出它的用途,一旦发现了就得尽快给它改名。

5.2 使用步骤
  • 先写一句注释描述这个函数的用途。
  • 再把这句注释变成函数的名字。
  • 先完成函数改名。
  • 测试。
  • 然后添加参数。
  • 测试。
5.3 范例
function inNewEngland(aCustomer) {
  return ["MA", "CT", "ME", "VT", "NH", "RI"].includes(aCustomer.address.state);
}

 const newEnglanders = someCustomers.filter(c => inNewEngland(c));


// 重构
function inNewEngland(stateCode) {
  return ["MA", "CT", "ME", "VT", "NH", "RI"].includes(stateCode);
}

const newEnglanders = someCustomers.filter(c => inNewEngland(c.address.state));

6. 封装变量 (Encapsulate Variable)

6.1 使用场景

对于所有可变的数据,只要它的作用域超出单个函数,就将其封装起来,只允许通过函数访问。

6.2 使用步骤
  • 创建封装函数,在其中访问和更新变量值。
  • 执行静态检查。
  • 逐一修改使用该变量的代码,将其改为调用合适的封装函数。每次替换之后,执行测试。
  • 限制变量的可见性。
  • 测试。
  • 如果变量的值是一个记录,考虑使用封装记录
6.3 范例

7. 变量改名 (Rename Variable)

7.1 使用场景

变量要可以很好地解释一段程序在干什么,对于作用域超出一次函数调用的字段,则需要更用心命名。

7.2 使用步骤
  • 如果变量被广泛使用,考虑运用封装变量将其封装起来。
  • 找出所有使用该变量的代码,逐一修改。
  • 测试。
7.3 范例

8. 引入参数对象 (Introduce Parameter Object)

8.1 使用场景

变量要可以很好地解释一段程序在干什么,对于作用域超出一次函数调用的字段,则需要更用心命名。

8.2 使用步骤
  • 如果暂时还没有一个合适的数据结构,就创建一个。
  • 测试。
  • 使用改变函数声明给原来的函数新增一个参数, 类型是新建的数据结构。
  • 测试。
  • 调整所有调用者,传入新数据结构的适当实例。每修改 一处,执行测试。
  • 用新数据结构中的每项元素,逐一取代参数列表中与之 对应的参数项,然后删除原来的参数。
  • 测试。
8.3 范例
const station = { 
  name: "ZB1",
  readings: [
    {temp: 47, time: "2016-11-10 09:10"},           
    {temp: 53, time: "2016-11-10 09:20"},           
    {temp: 58, time: "2016-11-10 09:30"},           
    {temp: 53, time: "2016-11-10 09:40"},           
    {temp: 51, time: "2016-11-10 09:50"},          
  ]
};

function readingsOutsideRange(station, min, max) {  
  return station.readings.filter(r => r.temp < min || r.temp > max);
}

const alerts = readingsOutsideRange(station, operatingPlan.temperatureFloor, operatingPlan.temperatureCeiling);


// 重构
function readingsOutsideRange(station, range) {  
  return station.readings.filter(r => !range.contains(r.temp));
}

function contains(arg) {
  return (arg >= this.min && arg <= this.max);
}

const alerts = readingsOutsideRange(station, range);

9. 函数组合成类 (Combine Functions into Class)

9.1 使用场景

如果发现一组函数形影不离地操作同一块数据(通常是将这块数据作为参数传递给函数),就需要组建一个类了。

9.2 使用步骤
  • 运用封装记录对多个函数共用的数据记录加以封装。
  • 对于使用该记录结构的每个函数,运用搬移函数将其移入新类。
  • 用以处理该数据记录的逻辑可以用提炼函数提炼出来,并移入新类。
9.3 范例
const reading = {customer: "ivan", quantity: 10, month: 5, year: 2017};
const aReading = acquireReading();
const baseCharge = baseRate(aReading.month, aReading.year) * aReading.quantity;

const base = (baseRate(aReading.month, aReading.year) * aReading.quantity);
const taxableCharge = Math.max(0, base - taxThreshold(aReading.year));

const basicChargeAmount = calculateBaseCharge(aReading);
function calculateBaseCharge(aReading) {
  return baseRate(aReading.month, aReading.year) * aReading.quantity;
}


// 重构
class Reading {
  constructor(data) {   
    this._customer = data.customer;   
    this._quantity = data.quantity;
    this._month = data.month;   
    this._year = data.year;
  }

  get customer() {
    return this._customer;
  }  
  
  get quantity() {
    return this._quantity;
  }  
  
  get month() {
    return this._month;
  }  
  
  get year() {
    return this._year;
  }
  
  get calculateBaseCharge() {
    return baseRate(this.month, this.year) * this.quantity;
  }
  
  get baseCharge() {
    return baseRate(this.month, this.year) * this.quantity;
  }
  
  get taxableCharge() {
    return Math.max(0, this.baseCharge - taxThreshold(this.year));
  }
}

10. 函数组合成变换 (Combine Functions into Transform)

10.1 使用场景

如果代码中会对源数据做更新,那么使用类要好得多; 如果使用变换,派生数据会被存储在新生成的记录中,一旦源数据被修改,我就会遭遇数据不一致。

10.2 使用步骤
  • 创建一个变换函数,输入参数是需要变换的记录,并直接返回该记录的值。
  • 挑选一块逻辑,将其主体移入变换函数中,把结果作为字段添加到输出记录中。修改客户端代码,令其使用这个新字段。
  • 测试。
  • 针对其他相关的计算逻辑,重复上述步骤。
10.3 范例
const reading = {customer: "ivan", quantity: 10, month: 5, year: 2017};
const aReading = acquireReading();
const baseCharge = baseRate(aReading.month, aReading.year) * aReading.quantity;

const base = (baseRate(aReading.month, aReading.year) * aReading.quantity);
const taxableCharge = Math.max(0, base - taxThreshold(aReading.year));

const basicChargeAmount = calculateBaseCharge(aReading);
function calculateBaseCharge(aReading) {
  return baseRate(aReading.month, aReading.year) * aReading.quantity;
}


// 重构
function enrichReading(original) {
  const result = _.cloneDeep(original);
  result.baseCharge = calculateBaseCharge(result);
  result.taxableCharge = Math.max(0, result.baseCharge - taxThreshold(result.year));
  return result;
}

const rawReading = acquireReading();
const aReading = enrichReading(rawReading);
const base = aReading.baseCharge;
const taxableCharge = aReading.taxableCharge;

11. 拆分阶段 (Split Phase)

11.1 使用场景

一段代码在同时处理两件不同的事,就要把它拆分成各自独立的模块。可能你有一段处理逻辑,其输入数据的格式不符合计算逻辑的要求,所以你得先对输入数据做一番调整,使其便于处理。也可能是你把数据处理逻辑分成顺序执行的多个步骤,每个步骤负责的任务全然不同。

11.2 使用步骤
  • 将第二阶段的代码提炼成独立的函数。
  • 测试。
  • 引入一个中转数据结构,将其作为参数添加到提炼出的新函数的参数列表中。
  • 测试。
  • 逐一检查提炼出的“第二阶段函数”的每个参数。如果某个参数被第一阶段用到,就将其移入中转数据结构。每次搬移之后都要执行测试。
  • 对第一阶段的代码运用提炼函数,让提炼出的函 数返回中转数据结构。
11.3 范例
function priceOrder(product, quantity, shippingMethod) {
   const basePrice = product.basePrice * quantity;
   const discount = Math.max(quantity - product.discountThreshold, 0) * product.basePrice * product.discountRate;
   const shippingPerCase = (basePrice > shippingMethod.discountThreshold) 
      ? shippingMethod.discountedFee 
      : shippingMethod.feePerCase;  
   const shippingCost = quantity * shippingPerCase;
   const price = basePrice - discount + shippingCost;
   return price;
}


// 重构
function priceOrder(product, quantity, shippingMethod) {  
  const priceData = calculatePricingData(product, quantity);  
  return applyShipping(priceData, shippingMethod);
}

function calculatePricingData(product, quantity) {
  const basePrice = product.basePrice * quantity;
  const discount = Math.max(quantity - product.discountThreshold, 0) * product.basePrice * product.discountRate;
  return {
    basePrice: basePrice, 
    quantity: quantity, 
    discount:discount
  }
};
  
function applyShipping(priceData, shippingMethod) {
  const shippingPerCase = (priceData.basePrice > shippingMethod.discountThreshold) 
  ? shippingMethod.discountedFee 
  : shippingMethod.feePerCase;
  const shippingCost = priceData.quantity * shippingPerCase;
  return priceData.basePrice - priceData.discount + shippingCost;
}

朦胧之月
3 声望1 粉丝

爱编程、爱生活


« 上一篇
设计模式