一、根底重构

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