《重构2》第一章学习笔记

99 阅读2分钟

前言

今天,学习记录一下重构的第一章

原本实现代码

下面一段代码,传入一个演出的数据,计算打印出每一种类型节目的收费和观看人数,并计算出演出完之后的总费用和总观众积分

// 每一场演出的观众人数
const invoice = {
  customer: 'BigCo',
  performances: [
    {
      playID: 'hamlet',
      audience: 55
    },
    {
      playID: 'as-like',
      audience: 35
    },
    {
      playID: 'othello',
      audience: 40
    }
  ]
}

// 每场演出对应的戏剧类型和名字
const plays = {
  hamlet: {
    name: 'Hamlet',
    type: 'tragedy'
  },
  'as-like': {
    name: 'As You Like It',
    type: 'comedy'
  },
  othello: {
    name: 'Othello',
    type: 'tragedy'
  }
}

function statement(invoice, plays) {
  let totalAmount = 0;
  let volumeCredits = 0;
  let result = `Statement for ${invoice.customer}\n`
  const format = new Intl.NumberFormat('en-us', {
    style: 'currency',
    currency: 'USD',
    minimumFractionDigits: 2
  }).format
  for (let perf of invoice.performances) {
    const play = plays[perf.playID]
    let thisAmount = 0;

    switch (play.type) {
      case 'tragedy': // 悲剧
        thisAmount = 40000
        if (perf.audience > 30) {
          thisAmount += 1000 * (perf.audience - 30)
        }
        break;
      case 'comedy': // 喜剧
        thisAmount = 30000
        if (perf.audience > 20) {
          thisAmount += 10000 + 500 * (perf.audience - 20)
        }
        thisAmount += 300 * perf.audience
        break;
      default:
        throw new Error(`unknow type: ${play.type}`)
    }

    volumeCredits += Math.max(perf.audience - 30, 0)
    if (play.type === 'comedy') {
      volumeCredits += Math.floor(perf.audience / 5)
    }

    result += ` ${play.name}: ${format(thisAmount / 100)} (${perf.audience} seats)\n`
    totalAmount += thisAmount
  }
  result += `Amount owed is ${format(totalAmount / 100)}\n`
  result += `You earned ${volumeCredits} credits\n`
  return result
}

第一版优化

  • 提取函数
  • 减少临时变量

计算总的演出费用

function playFor(aPerformance) {
  return plays[aPerformance.playID]
}

// 提取函数:计算每场节目的演出费用
  function amountFor(aPerformance) {
      let result = 0;
      switch (playFor(aPerformance).type) {
        case 'tragedy': // 悲剧
          result = 40000
          if (aPerformance.audience > 30) {
            result += 1000 * (aPerformance.audience - 30)
          }
          break;
        case 'comedy': // 喜剧
          result = 30000
          if (aPerformance.audience > 20) {
            result += 10000 + 500 * (aPerformance.audience - 20)
          }
          result += 300 * aPerformance.audience
          break;
        default:
          throw new Error(`unknow type: ${playFor(aPerformance).type}`)
      }
      return result
 }
 
 // 提取函数:计算所有的节目演出费用
 function totalVolumeCredits() {
      let volumeCredits = 0
      for (let perf of invoice.performances) {
        volumeCredits += volumeCreditsFor(perf)
      }
      return volumeCredits
}

计算观众量积分

 // 提取函数:计算每场观众量积分
    function volumeCreditsFor(perf) {
      let result = 0;
      result += Math.max(perf.audience - 30, 0)
      if (playFor(perf).type === 'comedy') {
        result += Math.floor(perf.audience / 5)
      }
      return result
    }
    
    // 提取函数:计算总得观众量积分
    function totalVolumeCredits() {
      let volumeCredits = 0
      for (let perf of invoice.performances) {
        volumeCredits += volumeCreditsFor(perf)
      }
      return volumeCredits
    }

打印结果

// format变量提取为函数
function usd(aNumner) {
  return new Intl.NumberFormat('en-us', {
    style: 'currency',
    currency: 'USD',
    minimumFractionDigits: 2
  }).format(aNumner / 100)
} 

// 拼接结果
function renderPlainText(invoice) {
  let result = `Statement for ${invoice.customer}\n`
  for (let perf of invoice.performances) {
    result += ` ${playFor(perf).name}: ${usd(amountFor(perf))} (${perf.audience} seats)\n`
  }
  result += `Amount owed is ${usd(totalAmount())}\n`
  result += `You earned ${totalVolumeCredits()} credits\n`
  return result
}

// 执行入口
function statement(invoice) {
  return renderPlainText(invoice)
}

第2版优化

将数据处理和字符串拼接分开处理,甚至可以分文件引入

// 演出费用
function amountFor(aPerformance) {
  let result = 0;
  switch (playFor(aPerformance).type) {
    case 'tragedy': // 悲剧
      result = 40000
      if (aPerformance.audience > 30) {
        result += 1000 * (aPerformance.audience - 30)
      }
      break;
    case 'comedy': // 喜剧
      result = 30000
      if (aPerformance.audience > 20) {
        result += 10000 + 500 * (aPerformance.audience - 20)
      }
      result += 300 * aPerformance.audience
      break;
    default:
      throw new Error(`unknow type: ${playFor(aPerformance).type}`)
  }
  return result
}

// 观众积分
function volumeCreditsFor(perf) {
  let result = 0;
  result += Math.max(perf.audience - 30, 0)
  if (playFor(perf).type === 'comedy') {
    result += Math.floor(perf.audience / 5)
  }
  return result
}
    
// 处理数据
function createStatementData(invoice) {
  const result = {}
  result.customer = invoice.customer
  
  // 计算每一项的费用、观众积分、类型
  result.performances = invoice.performances.map(v => {
    const result = Object.assign({}, v)
    result.play = playFor(result)
    result.amount = amountFor(result)
    result.volumeCredits = volumeCreditsFor(result)
    return result
  })
  
  // 总费用
  result.totalAmount = result.performances.reduce((total, p) => total + p.amount, 0)
  
  // 总观众积分
  result.totalVolumeCredits = result.performances.reduce((total, p) => total + p.volumeCredits, 0)
  return result
}

// 拼接文本
function renderPlainText(data) {
  let result = `Statement for ${data.customer}\n`
  for (let perf of data.performances) {
    result += ` ${data.play.name}: ${usd(perf.amount)} (${perf.audience} seats)\n`
  }
  result += `Amount owed is ${usd(data.totalAmount)}\n`
  result += `You earned ${data.totalVolumeCredits} credits\n`
  return result
}

第三版

从代码的可维护性:针对不同节目类型,通过类来分别计算费用和观众积分,便于后期维护和新增喜剧类型

抽出一个演出节目的基类,用于不同类型戏剧的继承

class PerformanceCalculator {
  constructor(aPerformance, aPlay) {
    this.performance = aPerformance
    this.play = aPlay
  }

  // 费用由子方法内部实现
  get amount() {
    throw new Error(`subclass responsibility`)
  }

  // 计算观众积分的通用求值
  get volumeCredits() {
    return Math.max(this.performance.audience - 30, 0)
  }
}

抽取悲剧类

  • 观众积分求值用父类,自己实现了费用计算方法
class TragedyCalculator extends PerformanceCalculator {
  get amount() {
    let result = 40000;
    if (this.performance.audience > 30) {
      result += 1000 * (this.performance.audience - 30)
    }
    return result
  }
}

抽取喜剧类

class ComedyCalculator extends PerformanceCalculator {
  get amount() {
    let result = 30000;
    if (this.performance.audience > 20) {
      result += 10000 + 500 * (this.performance.audience - 20)
    }
    result += 300 * this.performance.audience
    return result
  }
  get volumeCredits() {
    return super.volumeCredits + Math.floor(this.performance.audience / 5)
  }
}

创建戏剧计算器

function createPerformanceCalculator(aPerformance, aPlay) {
  switch (aPlay.type) {
    case 'tragedy':
      return new TragedyCalculator(aPerformance, aPlay)
    case 'comedy':
      return new ComedyCalculator(aPerformance, aPlay)
    default:
      throw new Error(`unknown type: ${aPlay.type}`)
  }
}

方法调用

// 处理数据
function createStatementData(invoice, plays) {
  const result = {}
  result.customer = invoice.customer
  // 计算每一项的演出费用、观众积分
  result.performances = invoice.performances.map(v => {
    // 调用创建演出戏剧的实例
    const calculator = createPerformanceCalculator(v, plays[v.playID])
    const result = Object.assign({}, v)
    result.play = calculator.play
    result.amount = calculator.amount
    result.volumeCredits = calculator.volumeCredits
    return result
  })
  result.totalAmount = result.performances.reduce((t, p) => t + p.amount, 0)
  result.totalVolumeCredits = result.performances.reduce((t, p) => t + p.volumeCredits, 0)
  return result
}

// 格式化数据成最终字符串结果
function renderPlainText(data) {
  let result = `Statement for ${data.customer}\n`
  for (let perf of data.performances) {
    result += ` ${perf.play.name}: ${usd(perf.amount)} (${perf.audience} seats)\n`
  }
  result += `Amount owed is ${usd(data.totalAmount)}\n`
  result += `You earned ${data.totalVolumeCredits} credits\n`
  return result
}

// 函数调用
function statement(invoice, plays) {
  return renderPlainText(createStatementData(invoice, plays))
}

最后

学习记录《重构2》的第一章,用到了提取函数、减少临时变量、搬移函数、多态取代条件表达式等方法。直接看好像代码量变多了,数据多循环了几次,但是代码可维护性变低了,在数据量小的情况下,多循环的次数造成的性能可以忽略不计。印象深刻的是一句话是:编程时,需要遵循营地法则:保证你离开时代码库一定比你来时更健康