Web开发

《重构:改善既有代码的设计》- 手法

2018-10-18  本文已影响10人  nimw

重新组织函数

一. Extract Method(提炼函数)

介绍

  1. 场景
    你有一段代码可以被组织在一起并独立出来。
  2. 手法
    将这段代码放进一个独立的函数中,并让函数名称解释该函数的作用。

动机

  1. 一段过长的函数或者需要注释才能让人理解用途的函数可以放进一个独立的函数中。
  2. 使用简短且命名良好的函数的好处
    ① 细粒度函数易于复用。
    ② 细粒度函数易于复写。
    ③ 高层函数读起来像一系列注释。
  3. 函数名好坏的关键在于函数名称与函数本体之间的语义距离,而不是函数名称的长短。

做法

① 创造新函数,以函数的意图命名。
② 将提炼出的代码从源函数复制到新建的目标函数中。
③ 检查提炼出的代码是否是否有源函数局部变量和参数。
④ 检查提炼出的代码是否有仅用于被提炼代码段的临时变量。
⑤ 检查提炼出的代码是否有源函数的任何局部变量的值被它改变。

范例

重构前

  printOwing() {
    const arr = [1,2,3]
    let sum = 0

    //print banner
    console.log(`--------------`)
    console.log(`Customer Owes`)
    console.log(`--------------`)

    arr.forEach(item => {
      sum += item
    })
    
    //print details
    console.log(`name: ${this._name}`)
    console.log(`amount: ${sum}`)
  }

重构后

 printOwing() {
    const sum = this.getOutStanding()
    this.printBanner()
    this.printDetails(sum)
  }

  getOutStanding() {
    const arr = [1,2,3]
    let sum = 0
    arr.forEach(item => {
      sum += item
    })
    return sum
  }

  printBanner() {
    console.log(`--------------`)
    console.log(`Customer Owes`)
    console.log(`--------------`)
  }

  printDetails(sum) {
    console.log(`name: ${this._name}`)
    console.log(`amount: ${sum}`)
  }

二. Inline Method(内联函数)

介绍

  1. 场景
    一个函数的本体与名称同样清楚易懂。
  2. 手法
    在函数调用点插入函数本体,然后移除该函数。

范例

重构前

class InlineMethod{
  constructor() {
    this._numberOfLateDeliveries = 6
  }

  getRatine() {
    return this.moreThanFiveLateDeliveries() ? 2 : 1
  }

  moreThanFiveLateDeliveries() {
    return this._numberOfLateDeliveries > 5
  }
}

重构后

class InlineMethod{
  constructor() {
    this._numberOfLateDeliveries = 6
  }

  getRatine() {
    return this._numberOfLateDeliveries > 5 ? 2 : 1
  }
}

三. Inline Temp(内联临时变量)

介绍

  1. 场景
    你有一个临时变量,只被一个简单表达式赋值一次,而它妨碍了其他重构手法。
  2. 手法
    将所有对该变量的引用动作替换为对它赋值的那个表达式本身。

范例

重构前

const basePrice = this.anOrder.basePrice();
return basePrice > 1000

重构后

return  this.anOrder.basePrice() > 1000

四. Replace Temp with Query(以查询取代临时变量)

介绍

  1. 场景
    你的程序以一个临时变量保存某一表达式的运算结果。
  2. 手法
    将这个表达式提炼到一个独立函数中,将这个临时变量的所有引用点替换为对新函数的调用。此后,新函数就可以被其他函数使用。

动机

  1. 临时变量只能在所属函数中使用,查询方法可以被用一个类中所有方法调用。
  2. 局部变量会使代码难以被提炼,所以应该尽可能把他们替换成查询方法。

范例

重构前

class ReplaceTempWithQuery {
  getPrice() {
    const basePrice = this._quantity * this._itemPrice
    let discountFactory
    if(basePrice > 1000) {
      discountFactor = 0.95
    } else {
      discountFactor = 0.98
    }
    return basePrice * discountFactor
  }
}

重构后

class ReplaceTempWithQuery {
  getPrice() { 
    return basePrice() * discountFactor()
  }

  basePrice() {
    return this._quantity * this._itemPrice
  }

  discountFactor() {
    return this.basePrice() > 1000 ? 0.95 : 0.98
  }
}

五. Introduce Explaining Variable(引入解释性变量)

介绍

  1. 场景
    你有一个复杂的表达式。
  2. 手法
    将该复杂表达式(或其中一部分)的结果放进一个临时变量,以此变量名称来解释表达式用途。

动机

  1. 临时变量可以帮助你将复杂且难以理解的表达式分解为容易管理的形式。
  2. 我不用常使用该重构手法,我比较喜欢使用 6.1 Extract Method(提炼函数),因为同一个对象中的任何部分,都可以根据自己的需要取用这些提炼出来的函数。

范例

重构前

  price() {
    //price is base price - quantity discount + shipping
    return this._quantity * this._itemPrice - 
    Math.max(0, this._quantity - 500) * this._itemPrice * 0.05 +
    Math.min(this._quantity * this._itemPrice * 0.1, 100.0)
  }

重构后:Introduce Explaining Variable(引入解释性变量)

  price() {
    const basePrice = this._quantity * this._itemPrice
    const quantityDiscount = Math.max(0, this._quantity - 500) * this._itemPrice * 0.05
    const shipping = Math.min(basePrice * 0.1, 100.0)
    return basePrice - quantityDiscount + shipping
  }

重构后:Extract Method(提炼函数)

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

  basePrice() {
    return this._quantity * this._itemPrice
  }

  quantityDiscount(){
    return Math.max(0, this._quantity - 500) * this._itemPrice * 0.05
  }

  shipping() {
    Math.min(basePrice * 0.1, 100.0)
  }

六. Split Temporary Varibale(分解临时变量)

介绍

  1. 场景
    你的程序有某个临时变量被赋值超过一次,它既不是循环变量,也不被用于收集计算结果。
  2. 手法
    针对每次赋值,创造一个独立、对应的临时变量。

动机

  1. 临时变量有各种不同用途,其中某些用途会很自然地导致多次赋值。“循环变量”和“结果收集变量”就是两个典型例子。
  2. 除了这两种情况,如果临时变量承担多个责任,它就应该被替换(分解)为多个临时变量,每个变量只被赋值一次,只承担一个责任。

范例

重构前

let temp = 2 * (_width + _height)
console.log(temp)
temp = _height * _width
console.log(temp)

重构后

const perimeter = 2 * (_width + _height)
console.log(perimeter)
const area = _height * _width
console.log(area)

七. Remove Assignments to Parameters(移除对参数的赋值)

介绍

  1. 场景
    代码对一个参数进行赋值。
  2. 手法
    以一个临时变量取代该参数的位置。

动机

  1. 对象的引用是按值传递的。因此,我们可以修改参数对象的内部状态,但对参数对象重新赋值是没有意义的。
  2. 在按值传递的情况下,对参数的重新赋值会降低代码的清晰度,而且混用了按值传递和按引用传递这两种参数传递方式。

范例

重构前

const discount = (inputVal, quantity, yearToDate) => {
  if(inputVal > 50) inputVal -= 2
  if(quantity > 100) inputVal -= 1
  if(yearToDate > 1000) inputVal -= 4
  return inputVal
}

重构后

const discount = (inputVal, quantity, yearToDate) => {
  let result = inputVal
  if(inputVal > 50) result -= 2
  if(quantity > 100) result -= 1
  if(yearToDate > 1000) result -= 4
  return result
}

八. Replace Method with Method Object(以函数对象取代函数)

介绍

  1. 场景
    你有一个大型函数,其中对局部变量的使用使你无法采用Extract Method(提炼函数)
  2. 手法
    将这个函数放进一个单独对象中,如此一来局部变量就成了对象内的字段。然后你可以在同一个对象中将这个大型函数分解为多个小型函数。

动机

  1. 将相对独立的代码从大型函数中提炼出来,可以大大提高代码的可读性。但是,局部变量的存在会增加函数分解难度。
  2. 该重构手法会将所有局部变量都变成函数对象的字段。然后你就可以对象这个新对象使用Extract Method(提炼函数)创造出新函数,从而将原本的大型函数拆解。

范例

重构前

class Account {
  gamma(inputVal, quantity, yearToDate) {
    let importantValue1 = (inputVal * quantity) + this.delta()
    let importantValue2 = (inputVal * yearToDate) + 100
    if(yearToDate - importantValue1 > 100) {
      importantValue2 -= 20
    }
    let importantValue3 = importantValue2 * 7
    return importantValue3 - 2 * importantValue1
  }
}

重构后

class Account {
  gamma(inputVal, quantity, yearToDate) {
    return new Gamma(this, inputVal, quantity, yearToDate).compute()
  }
}

class Gamma {
  _account;
  inputVal;
  quantity;
  yearToDate;
  importantValue1;
  importantValue2;
  importantValue3;

  constructor(account, inputVal, quantity, yearToDate) {
    this._account = account
    this.inputVal = inputVal
    this.quantity = quantity
    this.yearToDate = yearToDate
  }

  compute() {
    this.importantValue1 = (this.inputVal * this.quantity) + this._account.delta()
    this.importantValue2 = (this.inputVal * this.yearToDate) + 100
    if(this.yearToDate - this.importantValue1 > 100) {
      this.importantValue2 -= 20
    }
    this.importantValue3 = importantValue2 * 7
    return this.importantValue3 - 2 * this.importantValue1
  }
 }

现在可以轻松地对compute()函数采取6.1 Extract Method(提炼函数),不必担心参数传递的问题。

九. Substitute Algorithm(替换算法)

介绍

  1. 场景
    你想要把某个算法替换为另一个更清晰的算法。
  2. 手法
    将函数本体替换为另一个算法。

动机

  1. 如果你发现做一件事情可以有更清晰的方式,就应该以较清晰的方式取代复杂的方式。
  2. 替换一个巨大而复杂的算法是非常困难的,只要先将它分解为教简单的小型函数,然后你才能很有把握地进行算法替换工作。

范例

重构前

const foundPerson = (people) => {
  for(let i = 0; i < people.length; i++) {
    if(people[i] === 'Don') {
      return 'Don'
    }
    if(people[i] === 'John') {
      return 'John'
    }
    if(people[i] === 'Kent') {
      return 'Kent'
    }
    return ''
  }
}

重构后

const foundPerson = (people) => {
  let candidates = ['Don', 'John', 'Kent']
  for(let i = 0; i < people.length; i++) {
    if(candidates.includes(people[i])) {
      return people[i]
    }
  }
  return ''
}

在对象之间搬移特性

一. Move Method(搬移函数)

介绍

  1. 场景
    你的程序中,有个函数与其所驻类之外的另一个类进行更多交流:调用后者,或被后者调用。
  2. 手法
    在该函数最常引用的类中建立一个有着类似行为的新函数。将旧函数变成一个单纯的委托函数,或是将旧函数完全移除。

动机

  1. “搬移函数”是重构理论的支柱,可以使系统中的类更简单。
  2. 如果一个类有太多行为,或如果一个类与另一个类有太多合作而形成高度耦合,我就会搬移函数。

范例

重构前

class Account {
  overdraftCharge() {
    if(this._type.isPremium()) {
      let result = 10
      if(this._daysOverdrawn > 7) {
        result += (this._daysOverdrawn -7) * 0.85
      }
      return result
    } else {
      return this._daysOverdrawn * 1.75
    }
  }

  bankCharge() {
    const result = 4.5 
    if(this._daysOverdrawn > 0) {
      result += this.overdraftCharge()
    }
    return result
  }
}

重构后

class Account {
  bankCharge() {
    const result = 4.5 
    if(this._daysOverdrawn > 0) {
      result += this._type.overdraftCharge(this._daysOverdrawn)
    }
    return result
  }
}

class AccountType {
  overdraftCharge(daysOverdrawn) {
    if(this.isPremium()) {
      let result = 10
      if(daysOverdrawn > 7) {
        result += (daysOverdrawn -7) * 0.85
      }
      return result
    } else {
      return daysOverdrawn * 1.75
    }
  }
}

如果被搬移函数调用了Account中的另一个函数,我就不能简单地处理。这种情况下必须将源对象传递给目标函数。

class AccountType {
  overdraftCharge(account) {
    if(this.isPremium()) {
      let result = 10
      if(account.getDaysOverdrawn() > 7) {
        result += (account.getDaysOverdrawn() -7) * 0.85
      }
      return result
    } else {
      return account.getDaysOverdrawn() * 1.75
    }
  }
}

二. Move Field(搬移字段)

介绍

  1. 场景
    在你的程序中,某个字段被其所驻类之外的另一个类更多地用到。
  2. 手法
    在目标类新建一个字段,修改源字段的所有用户,令他们改用新字段。

动机

  1. 在类之间移动状态和行为,是重构过程中必不可少的措施。
  2. 使用Extract Class(提炼类)时,我也可能需要搬移字段。此时我会先搬移字段,然后再搬移函数。

范例

  1. 搬移只有一个函数使用的字段

重构前

class Account {
  _type: AccountType;
  _interestRate: number;

  interestForAmount_days(amount, days) {
    return this._interestRate * amount * days / 365
  }
}

重构后

class Account {
  _type: AccountType;

  interestForAmount_days(amount, days) {
    return this._type.getInterestRate() * amount * days / 365
  }
}

class AccountType {
  _interestRate: number;

  setInterestRate(arg) {
    this._interestRate = arg
  }

  getInterestRate() {
    return this._interestRate
  }
}
  1. 搬移有多个函数使用的字段

重构前

class Account {
  _interestRate: number;
  _type: AccountType;

  interestForAmount_days(amount, days) {
    return this.getInterestRate() * amount * days / 365
  }

  getInterestRate() {
    return this._interestRate
  }

  setInterestRate(arg) {
    this._interestRate = arg
  }
}

重构后

class Account {
  _type: AccountType;

  interestForAmount_days(amount, days) {
    return this.getInterestRate() * amount * days / 365
  }

  getInterestRate() {
    return this._type.getInterestRate()
  }

  setInterestRate(arg) {
    this._type.setInterestRate(arg)
  }
}

class AccountType {
  _interestRate: number;
  
  setInterestRate(arg) {
    this._interestRate = arg
  }

  getInterestRate() {
    return this._interestRate
  }
}

三. Extract Class(提炼类)

介绍

  1. 场景
    某个类做了应该由两个类做的事。
  2. 手法
    建立一个新类,将相关的字段和函数从旧类搬移到新类。

动机

  1. 一个类应该是一个清楚的抽象,处理一些明确的责任。
  2. 给类添加一项新责任时,你会觉得不值得为这项责任分离出一个单独的类。随着责任不断增加,这个类会变得过分复杂,成为一团乱麻。
  3. 如果某些数据和某些函数总是一起出现,某些数据经常同时变化甚至彼此相依,这就表示你应该将他们分离出去。

范例

重构前

class Person{
  _name: string;
  _officeAreaCode: string;
  _officeNumber: string;

  getName() {
    return this._name
  }

  getTelephoneNumber() {
    return `(${this._officeAreaCode})${this._officeNumber}`
  }

  getOfficeAreaCode() {
    return this._officeAreaCode
  }

  setOfficeAreaCode(arg) {
    this._officeAreaCode = arg
  }

  getOfficeNumber() {
    return this._officeNumber
  }

  setOfficeNumber(arg) {
    this._officeNumber = arg
  }
}

重构后

class Person {
  _name: string;
  _officeTelephone = new TelephoneNumber()

  getName() {
    return this._name
  }

  getTelephoneNumber() {
    return this._officeTelephone.getTelephoneNumber()
  }

  getOfficeTelephone() {
    return this._officeTelephone
  }
}

class TelephoneNumber {
  _areaCode: string;
  _number: string;

  getTelephoneNumber() {
    return `(${this._areaCode})${this._number}`
  }

  getAreaCode() {
    return this._areaCode
  }

  setAreaCode(arg) {
    this._areaCode = arg
  }

  getNumber() {
    return this._number
  }

  setNumber(arg) {
    this._number = arg
  }
}

四. Inline Class(将类内联化)

介绍

  1. 场景
    某个类没有做太多事情。
  2. 手法
    将这个类的所有特性搬移到另一个类中,然后移除原类。

动机

  1. Inline Class(将类内联化)正好与Extract Class(提炼类)相反。
  2. 如果一个类不再承担足够责任、不再有单独存在的理由,我们就会挑选这一“萎缩类”的最频繁用户(也是个类),以Inline Class手法将“萎缩类”塞进另一个类中。

范例

重构前

class Person {
  _name: string;
  _officeTelephone = new TelephoneNumber()

  getName() {
    return this._name
  }

  getTelephoneNumber() {
    return this._officeTelephone.getTelephoneNumber()
  }

  getOfficeTelephone() {
    return this._officeTelephone
  }
}

class TelephoneNumber {
  _areaCode: string;
  _number: string;

  getTelephoneNumber() {
    return `(${this._areaCode})${this._number}`
  }

  getAreaCode() {
    return this._areaCode
  }

  setAreaCode(arg) {
    this._areaCode = arg
  }

  getNumber() {
    return this._number
  }

  setNumber(arg) {
    this._number = arg
  }
}

重构后

class Person{
  _name: string;
  _officeAreaCode: string;
  _officeNumber: string;

  getName() {
    return this._name
  }

  getTelephoneNumber() {
    return `(${this._officeAreaCode})${this._officeNumber}`
  }

  getOfficeAreaCode() {
    return this._officeAreaCode
  }

  setOfficeAreaCode(arg) {
    this._officeAreaCode = arg
  }

  getOfficeNumber() {
    return this._officeNumber
  }

  setOfficeNumber(arg) {
    this._officeNumber = arg
  }
}

五. Hide Delegate(隐藏“委托关系”)

介绍

  1. 场景
    客户通过一个委托类来调用另一个对象。
  2. 手法
    在服务类上建立客户所需的所有函数,用以隐藏委托关系。

动机

  1. “封装”即使不是对象的最关键特征,也是最关键特征之一。
  2. “封装”意味每个对象都应该尽可能少了解系统的其他部分,一旦发生变化,需要了解这一变化的对象就会比较少。
  3. 隐藏“委托关系”,当委托关系发生变化时,变化也将被限制在服务对象中,不会波及客户。
  4. 一旦你对所有客户都隐藏了委托关系,就不再需要在服务对象的接口中公开被委托对象了。

范例

重构前

class Person {
  _department: Department;

  getDepartment() {
    return this._department
  }

  setDepartment(arg) {
    this._department = arg
  }
}

class Department {
  _chargeCode: string;
  _manager: Person;

  Department(manager) {
    this._manager = manager
  }

  getManager() {
    return this._manager
  }
}

const m = john.getDepartment().getManager()

重构后

class Person {
  _department: Department;

  getManager() {
    return this._department.getManager()
  }

  setDepartment(arg) {
    this._department = arg
  }
}

const m = john.getManager()

六. Remove Middle Man(移除中间人)

介绍

  1. 场景
    某个类做了过多的简单委托动作。
  2. 手法
    让客户直接调用受托类。

动机

  1. “封装受委托对象”的代价就是:每当客户要使用受托类的新特性时,必须在服务类添加一个简单委托函数。
  2. 随着受托类特性越来越复杂,委托函数越来越多,服务类完全成了一个“中间人”,此时你就应该让客户直接调用受托类。
  3. 重构的意义就在于:你永远不必说对不起——只要把出问题的地方修补好就行了。

范例

重构前

class Person {
  _department: Department;

  getManager() {
    return this._department.getManager()
  }

  setDepartment(arg) {
    this._department = arg
  }
}

class Department {
  _chargeCode: string;
  _manager: Person;

  Department(manager) {
    this._manager = manager
  }

  getManager() {
    return this._manager
  }
}

const m = john.getManager()

重构后

class Person {
  _department: Department;

  getDepartment() {
    return this._department
  }

  setDepartment(arg) {
    this._department = arg
  }
}

const m = john.getDepartment().getManager()

七. Introduce Foreign Method(引入外加函数)

介绍

  1. 场景
    你需要为提供服务的类增加一个函数,但你无法修改这个类。
  2. 手法
    在客户类中建立一个函数,并以第一参数形式传入一个服务类实例。

范例

重构前

//创建一个日期的下一天
const newStart = new Date(date.getFullYear(), date.getMonth(), date.getDate() + 1)

重构后

const nextDay = (date) => {
  return new Date(date.getFullYear(), date.getMonth(), date.getDate() + 1)
}

const newStart = nextDay(date)

八. Introduce Local Extension(引入本地扩展)

介绍

  1. 场景
    你需要为服务类提供一些额外函数,但你无法修改这个类。
  2. 手法
    建立一个新类,使它包含这些额外函数。让这个扩展品成为源类的子类或包装类。

动机

  1. 你需要为提供服务的类增加多个函数,但你无法修改这个类。
  2. 你需要将这些函数组织在一起,放到一个合适的地方去。子类化(subclassing)和包装(wrapping)是两种常用的本地扩展。
  3. 本地扩展是一个独立的类,但也是被扩展类的子类型:它提供源类的一切特性,同时额外添加新特性。

范例

使用子类

class MfDateSub extends Date{
  nextDay() {
    return new Date(this.getFullYear(), this.getMonth(), this.getDate() + 1)
  }
}

const mySubDate = new MfDateSub(2018, 9, 10)
console.log(mySubDate.nextDay())

注释:该代码只是为了演示使用子类扩展方式的原理,运行会报错。
使用包装类

class MfDateWrap {
  constructor() {
    this._original = new Date(...arguments)
  }

  getFullYear() {
    return this._original.getFullYear()
  }

  getMonth() {
    return this._original.getMonth()
  }

  getDate() {
    return this._original.getDate()
  }

  nextDay() {
    return new Date(this.getFullYear(), this.getMonth(), this.getDate() + 1)
  }
}

const mfDateWrap = new MfDateWrap(2018, 9, 10)
console.log(mfDateWrap.nextDay())

注释:使用包装类时需要为原始类(Date)的所有函数提供委托函数,这里只展示了三个函数,其他函数的处理依此类推。

重新组织数据

一. Self Encapsulate Field(自封装字段)

介绍

  1. 场景
    你直接访问一个字段,但与字段之间的耦合关系逐渐变得笨拙。
  2. 手法
    为这个字段建立取值/设置函数,并且只以这些函数来访问字段。

动机

  1. 在“字段访问方式”这个问题上,存在两种截然不同的观点:其中一派认为,在该变量定义所在的类中,你可以自由访问它;另一派认为,即使在这个类中你也应该只使用访问函数间接访问。
  2. 间接访问的好处是,子类可以通过覆写一个函数而改变获取数据的途径;它还支持更灵活的数据管理方式,例如延迟初始化。直接访问的好处是,代码比较容易阅读。
  3. 我比较喜欢先使用直接访问方式,直到这种方式给我带来麻烦为止,此时我就会转而使用间接访问方式。重构给我改变主意的自由。
  4. 如果你想访问超类中的一个字段,却又想在子类中将对这个变量的访问改为一个计算后的值,这就是最该使用Self Encapsulate Field(自封装字段)的时候。

范例

重构前

class IntRange {
  constructor(low, high) {
    this._low = low
    this._high = high
  }

  includes(arg) {
   return arg > this._low && arg < this._high 
  }

  grow(factor) {
    this._high *= factor
  }
}

重构后

class IntRange {
  constructor(low, high) {
    this.initialize(low, high)
  }

  initialize(low, high) {
    this._low = low
    this._high = high
  }

  includes(arg) {
    return arg > this.getLow() && arg < this.getHigh()
  }

  grow(factor) {
    this.setHigh(factor * this.getHigh())
  }

  getLow() {
    return this._low
  }

  setLow(arg) {
    this._low = arg
  }

  getHigh() {
    return this._high
  }

  setHigh(arg) {
    this._high = arg
  }
}

class CappedRange extends IntRange {
  constructor(low, high, cap) {
    super(low, high)
    this._cap = cap
  }

  getCap() {
    return this._cap
  }

  getHigh() {
    return Math.min(super.getHigh(), this.getCap())
  }
}

可以在CappedRange中覆写getHigh(),从而加入对“范围上限”(cap)的考虑,而不必修改IntRange的任何行为。

二. Replace Data Value with Object(以对象取代数据值)

介绍

  1. 场景
    你有一个数据项,需要与其他数据和行为一起使用才有意义。
  2. 手法
    将数据项变成对象。

范例

重构前

class Order {
  constructor(customer) {
    this._customer = customer
  }

  getCustomer() {
    return this._customer
  }

  setCustomer(arg) {
    this._customer = arg
  }
}

重构后

class Customer {
  constructor(name) {
    this._name = name
  }

  getName() {
    return this._name
  }
}

class Order {
  constructor(customerName) {
    this._customer = new Customer(customerName)
  }

  getCustomerName() {
    return this._customer.getName()
  }

  setCustomer(customerName) {
    this._customer = new Customer(customerName)
  }
}

三. Change Value to Reference(将值对象改为引用对象)

介绍

  1. 场景
    你从一个类衍生出许多彼此相等的实例,希望将他们替换为同一个对象。
  2. 手法
    将这个值对象变成引用对象。

动机

  1. 有时候,你会从一个简单的值对象开始,在其中保存少量不可修改的数据。
  2. 而后,你可能会希望给这个对象加入一些可修改数据,并确保对任何一个对象的修改都能影响到所有引用此一对象的地方。这时候你就需要将这个对象变成一个引用对象。

范例

重构前

class Customer {
  constructor(name) {
    this._name = name
  }

  getName() {
    return this._name
  }
}

class Order {
  constructor(customerName) {
    this._customer = new Customer(customerName)
  }

  getCustomerName() {
    return this._customer.getName()
  }

  setCustomer(customerName) {
    this._customer = new Customer(customerName)
  }
}

重构后

class Customer {
  static _instances = {};

  static loadCustomers() {
    new Customer('Lemon Car Hire').store()
    new Customer('Associated Coffee Mathines').store()
    new Customer('Bilston Gasworks').store()
  }

  static getNamed(name) {
    return Customer._instances[name]
  }

  constructor(name) {
    this._name = name
  }

  store() {
    Customer._instances[this.getName()] = this
  }

  getName() {
    return this._name
  }
}

Customer.loadCustomers()

class Order {
  constructor(customerName) {
    this._customer = Customer.getNamed(customerName)
  }

  getCustomerName() {
    return this._customer.getName()
  }

  setCustomer(customerName) {
    this._customer = new Customer(customerName)
  }
}

四. Change Reference to Value(将引用对象改为值对象)

介绍

  1. 场景
    你有一个引用对象,很小且不可变,而且不易管理。
  2. 手法
    将它变成一个值对象。

动机

  1. 值对象有一个非常重要的特性:他们应该是不可变的。
  2. 不可变的含义为该对象本身不可改变,但你可以使用另一个对象来取代现有的对象,而不是在现有对象上修改。其他对象与该对象之间的关系可以改变。

范例

重构前

class Currency {
  constructor(code) {
    this._code = code
  }

  getCode() {
    return this._code
  }

  equals(arg) {
    return this === arg
  }
}

console.log(new Currency('USD').equals(new Currency('USD'))); //false 

重构后

class Currency {
  constructor(code) {
    this._code = code
  }

  getCode() {
    return this._code
  }

  equals(arg) {
    if(!(arg instanceof Currency)) {
      return false
    }
    return this._code === arg._code
  }
}

console.log(new Currency('USD').equals(new Currency('USD'))); //true

五. Replace Array with Object(以对象取代数组)

介绍

  1. 场景
    你有一个数组,其中的元素各自代表不同的东西。
  2. 手法
    以对象替换数组。对于数组中的每个元素,以一个字段来表示。

范例

重构前

const row = []
row[0] = 'Liverpool'  //队名
row[1] = 15 //获胜场次

重构后

const row = {}
row['name'] = 'Liverpool'
row['win'] = 15

六. Duplicate Observed Data(复制“被监视数据”)

介绍

  1. 场景
    你有一些领域数据置身于GUI控件中,而领域函数需要访问这些数据。
  2. 手法
    将该数据复制到一个领域对象中。建立一个Observer模式,用以同步领域对象和GUI对象内的重复数据。

范例

JavaScript中无Observer类,无法用类似代码演示Observer模式。

七. Change Unidirectional Association to Bidirectional(将单向关联改为双向关联)

介绍

  1. 场景
    两个类都需要使用对方特性,但其间只有一条单向连接。
  2. 手法
    添加一个反向指针,并使修改函数能够同时更新两条连接。

范例

重构前

class Customer{}

class Order {
  getCustomer() {
    return this._customer
  }

  setCustomer(arg) {
    this._customer = arg
  }
}

重构后

class Customer {
  _orders = new Set()

  friendOrders() {
    return this._orders
  }

  addOrder(arg) {
    arg.setCustomer(this)
  }
}

class Order {
  getCustomer() {
    return this._customer
  }

  /**
   * 控制函数
   * @param {} arg 
   */
  setCustomer(arg) {
    if(arg) {
      this._customer.friendOrders().delete(this)
    }
    this._customer = arg
    if(this._customer) {
      this._customer.friendOrders().add(this)
    }
  }
}

我比较喜欢让单个类来负责控制关联关系,这样可以将所有处理关联关系的逻辑集中安置于一地。
① 如果关联是“一对多”,那么久由“拥有单一引用”的那一方承担“控制者”角色。
② 如果某个对象是另一个对象的部件,那么由后者负责控制关联关系。
③ 如果关联是“多对多”关系,那么随便其中哪个对象来控制关联关系,都无所谓。

八. Change Bidirectional Association to Unidirectional(将双向关联改为单向关联)

介绍

  1. 场景
    两个类之间有双向关联,但其中一个类如今不再需要另一个类的特性。
  2. 手法
    去除不必要的关联。

动机

  1. 维护双向连接、确保对象被正确创建和删除会增加复杂度。
  2. 双向连接容易造成“僵尸对象”:某个对象本身已经该死亡了,却仍然保留在系统中,因为对它的引用还没有完全清除。
  3. 双向关联迫使两个类之间有了依赖:对其中任何一个类的任何修改,都可能引发另一个类的变化。
  4. 只有在真正需要双向关联的时候,才应该使用它。如果发现双向关联不再有存在价值,就应该去掉其中不必要的一条关联。

范例

重构前

class Customer {
  _orders = new Set()

  friendOrders() {
    return this._orders
  }

  addOrder(arg) {
    arg.setCustomer(this)
  }

  getPriceFor(order) {
    return order.getDiscountedPrice()
  }
}

class Order {
  getCustomer() {
    return this._customer
  }

  /**
   * 控制函数
   * @param {} arg 
   */
  setCustomer(arg) {
    if(arg) {
      this._customer.friendOrders().delete(this)
    }
    this._customer = arg
    if(this._customer) {
      this._customer.friendOrders().add(this)
    }
  }

  getDiscountedPrice() {
    return this.getGrossPrice() * (1- this._customer.getDiscount())
  }
}

重构后

class Customer {
  _orders = new Set()

  friendOrders() {
    return this._orders
  }

  addOrder(arg) {
    arg.setCustomer(this)
  }

  getPriceFor(order) {
    return order.getDiscountedPrice(this)
  }
}

class Order {
  getDiscountedPrice(customer) {
    return this.getGrossPrice() * (1- customer.getDiscount())
  }
}

九. Replace Magic Number with Symbolic Constant(以字面常量取代魔法数)

介绍

  1. 场景
    你有一个字面数值,带有特别含义。
  2. 手法
    创造一个常量,根据其意义为它命令,并将上述的字面数值替换为这个常量。

范例

重构前

const potentialEnergy = (mass, height) => {
  return mass * 9.81 * height
} 

重构后

const GRAVITATIONAL_CONSTANT = 9.81

const potentialEnergy = (mass, height) => {
  return mass * GRAVITATIONAL_CONSTANT * height
} 

十. Encapsulate Field(封装字段)

介绍

  1. 场景
    你的类中存在一个public字段。
  2. 手法
    将它声明为private,并提供相应的访问函数。

范例

JavaScript中,没有访问权限修饰符,该重构手法无法演示。

十一. Encapsulate Collection(封装集合)

介绍

  1. 场景
    有个函数返回一个集合。
  2. 手法
    让这个函数返回该集合的一个只读副本,并在这个类中提供添加/移除集合元素的函数。

动机

  1. 取值函数不应该返回集合自身,因为这会让用户得以修改集合内容而集合拥有者却一无所知。
  2. 不应该为整个集合提供一个设值函数,但应该提供用以为集合添加/移除元素的函数。这样,集合拥有者(对象)就可以控制集合元素的添加和移除。

范例

重构前

class Course {
  constructor(name, isAdvanced) {
    this._name = name
    this._isAdvanced = isAdvanced
  }

  isAdvanced() {
    return this._isAdvanced
  }
}

class Person {
  getCourses() {
    return this._courses
  }

  setCourses(arg) {
    this._courses = arg
  }
}

重构后

class Course {
  constructor(name, isAdvanced) {
    this._name = name
    this._isAdvanced = isAdvanced
  }

  isAdvanced() {
    return this._isAdvanced
  }
}

class Person {
  constructor() {
    this._courses = []
  }

  addCourse(arg) {
    return this._courses.push(arg)
  }

  removeCourse(arg) {
    this._courses.filter(item => item !== arg)
  }

  initializeCourses(arg) {
    this._courses = this._courses.concat(arg)
  }

  getCourses() {
    return this._courses.map(item => item)
  }
}

十二. Replace Record with Data Class(以数据类取代记录)

介绍

  1. 场景
    你需要面对传统编程环境中的记录结构。
  2. 手法
    为该记录创建一个“哑”数据对象。

十三. Replace Type Code with Class(以类取代类型码)

介绍

  1. 场景
    类之中有一个数值类型码,但它并不影响类的行为。
  2. 手法
    以一个新的类替换该数值类型码。

范例

重构前

class Person {
  static O = 0;
  static A = 1;
  static B = 2;
  static AB = 3;

  constructor(bloodGroup) {
    this._bloodGroup = bloodGroup
  }

  setBloodGroup(arg) {
    this._bloodGroup = arg
  }

  getBloodGroup() {
    return this._bloodGroup
  }
}

重构后

class BloodGroup{
  static O = new BloodGroup(0);
  static A = new BloodGroup(1);
  static B = new BloodGroup(2);
  static AB = new BloodGroup(3);
  static _values = [O, A, B, AB]

  constructor(code) {
    this._code = code
  }

  getCode() {
    return this._code
  }

  static code(arg) {
    return BloodGroup._values[arg]
  }
}

class Person {
  constructor(bloodGroup) {
    this._bloodGroup = bloodGroup
  }

  setBloodGroup(bloodGroup) {
    this._bloodGroup = bloodGroup
  }

  getBloodGroup() {
    return this._bloodGroup
  }
}

十四. Replace Type Code with Subclass(以子类取代类型码)

介绍

  1. 场景
    你有一个不可变的类型码,它会影响类的行为。
  2. 手法
    以子类取代这个类型码。

范例

重构前

class Employee {
  static ENGINEER = 0; //工程师
  static SALESMAN = 1; //销售员
  static MANAGER = 2; //管理者

  constructor(type) {
    this._type = type
  }
}

重构后

class Employee {
  static ENGINEER = 0; //工程师
  static SALESMAN = 1; //销售员
  static MANAGER = 2; //管理者

  static create(type) {
    switch(type) {
      case Employee.ENGINEER:
        return new Engineer()
      case Employee.SALESMAN:
        return new Saleseman()
      case Employee.MANAGER:
        return new Manager()
      default:
        throw new Error('Incorrect type code value')
    }
  }

  constructor(type) {
    this._type = type
  }

  getType() {
    return this._type
  }
}

class Engineer extends Employee {
  getType() {
    return Employee.ENGINEER
  }
}

class Saleseman extends Employee {
  getType() {
    return Employee.SALESMAN
  }
}

class Manager extends Employee {
  getType() {
    return Employee.MANAGER
  }
}

注释:类中添加create静态工厂方法。
应该进一步重构,将与特定种类雇员相关的函数和字段推到相关的子类去。

十五. Replace Type Code with State/Strategy(以State/Strategy取代类型码)

介绍

  1. 场景
    你有一个类型码,它会影响类的行为,但你无法通过继承手法消除它。
  2. 手法
    以状态对象取代类型码。

动机

  1. 如果“类型码的值在对象生命周期中发生变化”或“其他原因使得宿主类不能被继承”,你可以使用本重构手法。
  2. 本重构使用State模式或Strategy模式【Gang of Four】。

范例

重构前

class Employee {
  static ENGINEER = 0; //工程师
  static SALESMAN = 1; //销售员
  static MANAGER = 2; //管理者

  constructor(type) {
    this._type = type
  }

  payAmount() {
    switch(this._type) {
      case Employee.ENGINEER:
        return this._monthlySalary
      case Employee.SALESMAN:
        return this._monthlySalary + this._commission
      case Employee.MANAGER:
        return this._monthlySalary + this._bonus
      default:
        throw new Error('Incorrect Employee') 
    }
  }
}

重构后

class Employee {
  constructor(type) {
    this._type = type
  }

  getType() {
    return this._type.getTypeCode()
  }

  setType(arg) {
    this._type = EmployeeType.newType(arg)
  }

  payAmount() {
    switch(this.getType()) {
      case EmployeeType.ENGINEER:
        return this._monthlySalary
      case EmployeeType.SALESMAN:
        return this._monthlySalary + this._commission
      case EmployeeType.MANAGER:
        return this._monthlySalary + this._bonus
      default:
        throw new Error('Incorrect Employee') 
    }
  }
}

class EmployeeType{
  static ENGINEER = 0; //工程师
  static SALESMAN = 1; //销售员
  static MANAGER = 2; //管理者

  getTypeCode() {}

  newType() {
    switch(arg) {
      case EmployeeType.ENGINEER:
        return new Engineer()
      case EmployeeType.SALESMAN:
        return new Saleman()
      case EmployeeType.MANAGER:
        return new Manager()
      default:
        throw new Error('Incorrect Employee Code')
    }
  }
}

class Engineer extends EmployeeType {
  getTypeCode() {
    return Employee.ENGINEER
  }
}

class Manager extends EmployeeType {
  getTypeCode() {
    return Employee.MANAGER
  }
}

class Saleman extends EmployeeType{
  getTypeCode() {
    return Engineer.SALESMAN
  }
}

注释:将工厂方法写在父类中,与项目中的widgetFactory工厂对比。

十六. Replace Subclass with Fields(以字段取代子类)

介绍

  1. 场景
    你的各个子类的唯一差别只在“返回常量数据”的函数身上。
  2. 手法
    修改这些函数,使他们返回超类中的某个(新增)字段,然后销毁子类。

动机

  1. 建立子类的目的,是为了增加新特性或变化其行为。
  2. 若子类中只有常量函数,你可以在超类中设计一个与常量函数返回值相应的字段,从而完全去除这样的子类。这样可以避免因继承而带来的额外复杂性。

范例

重构前

class Person{
  isMale() {}
  getCode() {}
}

class Male extends Person {
  isMale() {
    return true
  }

  getCode() {
    return 'M'
  }
}

class Female extends Person {
  isMale() {
    return false
  }

  getCode() {
    return 'F'
  }
}

重构后

class Person{
  static createMale() {
    return new Person(true, 'M')
  }

  static createFemale() {
    return new Person(false, 'F')
  }

  constructor(isMale, code) {
    this._isMale= isMale
    this._code = code
  }

  isMale() {
    return this._isMale
  }
  getCode() {
    return this._code
  }
}

注释:createXXX静态工厂方法。

简化条件表达式

一. Decompose Conditional(分解条件表达式)

介绍

  1. 场景
    你有一个复杂的条件(if-then-else)语句。
  2. 手法
    ifthenelse三个段落中分别提炼出独立函数。

范例

重构前

class Production{
  price() {
    if(date.before(this.SUMMER_START) || date.after(this.SUMMER_END)) {
      return quantity * this._winterRate + this._winterServiceCharge
    } else {
      return quantity * _summerRate
    }
  }
}

重构后

class Production{
  price(date) {
    if(this.notSummer(date)) {
      return this.winterCharge(quantity)
    } else {
      return this.summerCharge(quantity)
    }
  }

  notSummer(date) {
   return date.before(this.SUMMER_START) || date.after(this.SUMMER_END)
  }

  winterCharge(quantity) {
   return quantity * this._winterRate + this._winterServiceCharge
  }

  summerCharge(quantity) {
    return quantity * _summerRate
  }
 }

二. Consolidate Conditional Expression(合并条件表达式)

  1. 场景
    你有一系列条件测试,都得到相同结果。
  2. 手法
    将这些测试合并为一个条件表达式,并将这个条件表达式提炼成为一个独立函数。

范例

  1. 使用逻辑或
    重构前
  disabilityAmount() {
    if(this._seniority < 2) return 0
    if(this._monthsDisabled > 12) return 0
    if(this._isPartTime) return 0
    // compute the disability amount
    //...
  }

重构后

  disabilityAmount() {
    if(this.isNotEligibleForDisability()) return 0
    // compute the disability amount
    //...
  }

  isNotEligibleForDisability() {
    return ((this._seniority < 2) || (this._monthsDisabled > 12) || (this._isPartTime))
  }
  1. 使用逻辑与
    重构前
  if(this.onVacation()) {
    if(this.lengthOfService() > 10) {
      return 1
    }
  }
  return 0.5

重构后

  return (this.onVacation() && this.lengthOfService() > 10) ? 1 : 0.5

三. Consolidate Duplicate Conditional Fragments(合并重复的条件片段)

介绍

  1. 场景
    在条件表达式的每个分支上有着相同的一段代码。
  2. 手法
    将这段重复代码搬移到条件表达式之外。

范例

重构前

if(this.isSpecialDeal()) {
  total = price * 0.95
  this.send()
} else {
  total = price * 0.98
  this.send()
}

重构后

if(this.isSpecialDeal()) {
  total = price * 0.95
} else {
  total = price * 0.98
}
this.send()

四. Remove Control Flag(移除控制标记)

介绍

  1. 场景
    在一系列布尔表达式中,某个变量带有“控制标记”的作用。
  2. 手法
    break语句或者return语句取代控制标记。

动机

  1. 人们之所以使用控制标记,因为结构化编程原则告诉他们:每个子程序只能有一个入口和一个出口。
  2. 我赞同“单一入口”原则,但是“单一出口”原则会让你在代码中加入讨厌的控制标记,大大降低条件表达式的可读性。这就是编程语言提供break语句和continue语句的原因:用它们跳出复杂的条件语句。

范例

  1. break取代简单的控制标记

重构前

function checkSecurity(peoples) {
  let found = false
  for(let i = 0; i < peoples.length; i++) {
    if(!found) {
      if(peoples[i] === 'Don' || peoples[i] === 'John') {
        sendAlert()
        found = true
      }
    }
  }
}

重构后

function checkSecurity(peoples) {
  for(let i = 0; i < peoples.length; i++) {
    if(peoples[i] === 'Don' || peoples[i] === 'John') {
      sendAlert()
      break;
    }
  }
}
  1. return返回控制标记

重构前

function checkSecurity(peoples) {
  let found = ''
  for(let i = 0; i < peoples.length; i++) {
    if(!found) {
      if(peoples[i] === 'Don' || peoples[i] === 'John') {
        sendAlert()
        found = peoples[i]
      }
    }
  }
  someLaterCode(found)
}

重构后

function checkSecurity(peoples) {
  const found = foundMiscreant(peoples)
  someLaterCode(found)
}

function foundMiscreant(peoples) {
  for(let i = 0; i < peoples.length; i++) {
    if(peoples[i] === 'Don' || peoples[i] === 'John') {
      sendAlert()
      return peoples[i]
    }
  }
  return ''
}

五. Replace Nested Conditional with Guard Clauses(以卫语句取代嵌套条件表达式)

介绍

  1. 场景
    函数中的条件逻辑使人难以看清正常的执行路径。
  2. 手法
    使用卫语句表现所有特殊情况。

动机

  1. 条件表达式通常有两种表现形式。第一种形式是:所有分支都属于正常行为。第二种形式则是:条件表达式提供的答案中只有一种是正常行为,其他都是不常见的情况。
  2. 如果两条分支都是正常行为,就应该使用如if...else... 的条件表达式;如果某个条件极其罕见,就应该单独检查该条件,并在该条件为真时立即从函数中返回。这样的单独检查常常被称为“卫语句”。
  3. 如今的编程语言都会强制保证每个函数只有一个入口。至于“单一出口”规则,其实并不是那么有用。在我看来,保持代码清晰才是最关键的。

范例

  1. 使用卫语句

重构前

function getPayAmount() {
  let result;
  if(_isDead) {
    result = deadAmount()
  } else {
    if(_isSeparated) {
      result = separatedAmount()
    } else {
      if(_isRetired) {
        result = retiredAomunt()
      } else {
        result = normalPayAmount()
      }
    }
  }
  return result
}

重构后

function getPayAmount() {
  if(_isDead) return deadAmount()
  if(_isSeparated) return separatedAmount()
  if(_isRetired) return retiredAomunt()
  return normalPayAmount()
}
  1. 将条件反转

重构前

function getAdjustedCapital() {
  let result = 0
  if(_capital > 0) {
    if(_intRate > 0 && _duration > 0) {
      result = (_income / _duration) * ADJ_FACTOR
    }
  }
  return result
}

重构后

function getAdjustedCapital() {
  if(_capital <= 0) return 0
  if(_intRate <= 0 || _duration <= 0) return 0
  return (_income / _duration) * ADJ_FACTOR
}

六. Replace Conditional with Polymorphism(以多态取代条件表达式)

介绍

  1. 场景
    你手上有个条件表达式,它根据对象类型的不同而选择不同的行为。
  2. 手法
    将这个条件表达式的每个分支放进一个子类内的覆写函数中,然后将原始函数声明为抽象函数。

动机

  1. 多态最根本的好处就是:如果你需要根据对象的不同类型而采取不同的行为,多态使你不必编写明显的条件表达式。
  2. 正是因为有了多态,你会发现“类型码的switch语句”以及“基于类型名称的if-then-else语句”在面向对象程序中很少出现。

范例

重构前

class Employee {
  constructor(type) {
    this._type = type
  }

  getType() {
    return this._type.getTypeCode()
  }

  setType(arg) {
    this._type = EmployeeType.newType(arg)
  }

  payAmount() {
    switch(this.getType()) {
      case EmployeeType.ENGINEER:
        return this._monthlySalary
      case EmployeeType.SALESMAN:
        return this._monthlySalary + this._commission
      case EmployeeType.MANAGER:
        return this._monthlySalary + this._bonus
      default:
        throw new Error('Incorrect Employee') 
    }
  }
}

class EmployeeType{
  static ENGINEER = 0; //工程师
  static SALESMAN = 1; //销售员
  static MANAGER = 2; //管理者

  getTypeCode() {}

  newType() {
    switch(arg) {
      case EmployeeType.ENGINEER:
        return new Engineer()
      case EmployeeType.SALESMAN:
        return new Saleman()
      case EmployeeType.MANAGER:
        return new Manager()
      default:
        throw new Error('Incorrect Employee Code')
    }
  }
}

class Engineer extends EmployeeType {
  getTypeCode() {
    return Employee.ENGINEER
  }
}

class Manager extends EmployeeType {
  getTypeCode() {
    return Employee.MANAGER
  }
}

class Saleman extends EmployeeType{
  getTypeCode() {
    return Engineer.SALESMAN
  }
}

重构后

class Employee {
  constructor(type) {
    this._type = type
  }

  getType() {
    return this._type.getTypeCode()
  }

  setType(arg) {
    this._type = EmployeeType.newType(arg)
  }

  payAmount() {
    return this._type.payAmount()
  }
}

class EmployeeType{
  static ENGINEER = 0; //工程师
  static SALESMAN = 1; //销售员
  static MANAGER = 2; //管理者

  getTypeCode() {}

  newType() {
    switch(arg) {
      case EmployeeType.ENGINEER:
        return new Engineer()
      case EmployeeType.SALESMAN:
        return new Saleman()
      case EmployeeType.MANAGER:
        return new Manager()
      default:
        throw new Error('Incorrect Employee Code')
    }
  }

  payAmount(emp) {}
}

class Engineer extends EmployeeType {
  getTypeCode() {
    return Employee.ENGINEER
  }

  payAmount(emp) {
    return emp.getMonthlySalary()
  }
}

class Manager extends EmployeeType {
  getTypeCode() {
    return Employee.MANAGER
  }

  payAmount(emp) {
    return emp.getMonthlySalary() + emp.getCommission()
  }
}

class Saleman extends EmployeeType{
  getTypeCode() {
    return Engineer.SALESMAN
  }

  payAmount(emp) {
    return emp.getMonthlySalary() + emp.getBonus()
  }
}

七. Introduce Null Object(引入Null对象)

介绍

  1. 场景
    你需要再三检查某对象是否为null
  2. 手法
    null值替换为null对象。

动机

  1. 多态的最根本好处在于:你不必再向对象询问“你是什么类型”而后根据得到的答案调用对象的某个行为——你只管调用该行为就是了,其他的一切多态机制会为你安排妥当。
  2. 当某个字段内容是null时,多态可扮演另一个较不直观(亦较不为人所知)的用途。
  3. 空对象一定是常量,他们的任何成分都不会发生变化。因此可以使用单例模式来实现他们。

范例

重构前

class Site {
  _customer;

  getCustomer() {
    return this._customer
  }
}

class Customer {
  getName() {}

  getPlan() {}

  getHistory() {}
}

class PaymentHistory {
  getWeeksDelinquentInLastYear() {}
}

//示例代码
const customer = site.getCustomer()
const plan = customer ? customer.getPlan() : BillingPlan.basic() 
const customerName = customer ? customer.getName() : 'occupant'
const weeksDelinquent = customer ? customer.getHistory().getWeeksDelinquentInLastYear() : 0

重构后

class Site {
  _customer;

  getCustomer() {
    return this._customer ? this._customer : Customer.newNull()
  }
}

class Customer {
  static newNull() {
    return new NullCustomer();
  }

  isNull() {
    return false
  }

  getName() {}

  getPlan() {}

  getHistory() {}
}

class NullCustomer extends Customer {
  isNull() {
    return true
  }

  getName() {
    return 'occupant'
  }

  getPlan() {
    return BillingPlan.basic() 
  }

  getHistory() {
    return PaymentHistory.newNull()
  }
}

class PaymentHistory {
  static newNull() {
    return new NullPaymentHistory();
  }

  getWeeksDelinquentInLastYear() {}
}

class NullPaymentHistory extends PaymentHistory {
  getWeeksDelinquentInLastYear() {
    return 0
  }
}

//示例代码
const customer = site.getCustomer()
const plan = customer.getPlan()
const customerName = customer.getName()
const weeksDelinquent = customer.getHistory().getWeeksDelinquentInLastYear()

八. Introduce Assertion(引入断言)

介绍

  1. 场景
    某一段代码需要对程序状态做出某种假设。
  2. 手法
    以断言明确表现这种假设。

动机

  1. 常常有这样一段假设:只有当某个条件为真时,该段代码才能正常运行。例如平放根计算只对正值才能进行。
  2. 这样的假设通常并没有在代码中明确表现出来,你必须阅读整个算法才能看出。有时候程序员会以注释写出这样的假设。使用断言明确标明这些假设是一种更好的技术。
  3. 断言是一个条件表达式,应该一定总是真,如果它失败,表示程序员犯了错误。实际上,程序最后的成品往往将断言统统删除。

范例

重构前

getExpenseLimit() {
  return (_expenseLimit !== NULL_EXPENSE) ? 
          _expenseLimit :
          _primaryProject.getMemberExpenseLimit()
}

重构后

getExpenseLimit() {
  Assert.isTrue(_expenseLimit !== NULL_EXPENSE || _primaryProject !== null) 
  return (_expenseLimit !== NULL_EXPENSE) ? 
          _expenseLimit :
          _primaryProject.getMemberExpenseLimit()
}

简化函数调用

一. Rename Method(函数改名)

介绍

  1. 场景
    函数的名称未能揭示函数的用途。
  2. 手法
    修改函数名称。

动机

  1. 给函数命名有一个好方法:首先考虑应该给这个函数写上一句怎样的注释,然后想办法将注释编程函数名称。
  2. 如果你看到一个函数名称不能很好地传达它的用途,应该马上加以修改。
  3. 你的代码首先是为人写的,其次才是为计算机写的。而人需要良好名称的函数。

范例

重构前

getTelephoneNumber (){
  return `(${_officeAreaCode}-${_officeNumber})`
}

重构后

getOfficeTelephoneNumber (){
  return `(${_officeAreaCode}-${_officeNumber})`
}

二. Add Parameter(添加参数)

介绍

  1. 场景
    某个函数需要从调用端得到更多信息。
  2. 手法
    为此函数添加一个对象参数,让该对象带进函数所需信息。

动机

  1. 除了添加参数外,你常常还有其他选择。只要可能,其他选择都比添加参数要好,因为他们不会增加参数列的长度。
  2. 过长的参数列是不好的味道,因为程序员很难记住那么多参数。
  3. 并非禁止添加参数,但是在添加参数之前需要了解是否有其他选择。

三. Remove Parameter(移除参数)

介绍

  1. 场景
    函数本体不再需要某个参数。
  2. 手法
    将该参数去除。

动机

  1. 程序员可能经常添加参数,却往往不愿意去掉他们。
  2. 参数代表着函数所需的信息,不同的参数值有不同的意义,应及时去掉多余参数。

四. Separate Query form Modifier(将查询函数和修改函数分离)

介绍

  1. 场景
    某个函数既返回对象状态值,又修改对象状态。
  2. 手法
    建立两个不同的函数,其中一个负责查询,另一个负责修改。

动机

  1. 任何有返回值的函数,都不应该有看得到的副作用。
  2. 如果你遇到一个“既有返回值又有副作用”的函数,就应该试着将查询动作从修改动作中分割出来。

范例

重构前

foundMiscreant(people) {
  for(let i = 0; i < people.length; i++) {
    if(people[i] === 'Don' || peoplep[i] === 'John') {
      this.sendAlert()
      return 'Dom'
    }
  }
  return ''
}

checkSecurity(people) {
  const found = this.foundMiscreant(people)
  this.someLaterCode(found)
}

重构后

foundPerson(people) {
  for(let i = 0; i < people.length; i++) {
    if(people[i] === 'Don' || peoplep[i] === 'John') {
      return 'Dom'
    }
  }
  return ''
}

alertPerson(people) {
  if(this.foundPerson(people)) {
    this.sendAlert()
  }
}

checkSecurity(people) {
  this.alertPerson(people)
  const found = this.foundPerson(people)
  this.someLaterCode(found)
}

五. Parameterize Method(令函数携带参数)

介绍

  1. 场景
    若干函数做了类似的工作,但在函数本体中却包含了不同的值。
  2. 手法
    建立单一函数,以参数表达那些不同的值。

动机

  1. 你可能发现这样的两个函数:他们做着类似的工作,但因为少数几个值致使行为略有不同。
  2. 你可以将这些各自分离的函数统一起来,并通过参数来处理那些变化,用以简化问题。
  3. 本项重构的要点在于:以“可将少量数据视为参数”为依据,找出带有重复性的代码。

范例

重构前

baseCharge() {
  let result = Math.min(this.lastUsage(), 100) * 0.03
  if(this.lastUsage() > 100) {
    result += (Math.min(this.lastUsage(), 200) -100) * 0.05
  }
  if(this.lastUsage() > 200) {
    result += (this.lastUsage() - 200) * 0.07
  }
  return new Dollars(result)
}

重构后

baseCharge() {
  let result = this.usageInRange(0, 100) * 0.03
  result += this.usageInRange(100, 200) + 0.05
  result += this.usageInRange(200, Number.MAX_SAFE_INTEGER)
  return new Dollars(result)
}

usageInRange(start, end) {
  return this.lastUsage() > start ? Math.min(this.lastUsage(), end) - start : 0
}

六. Replace Parameter with Explicit Methods(以明确函数取代参数)

介绍

  1. 场景
    你有一个函数,其中完全取决于参数值而采取不同行为。
  2. 手法
    针对该参数的每一个可能值,建立一个独立函数。

动机

  1. 如果某个参数有多种可能的值,而函数内又以条件表达式检查这些参数值,并根据不同参数值做出不同的行为,那么就应该使用本项重构。
  2. 提供不同的函数给调用者使用,可以避免出现条件表达式。
  3. 本项重构可以获取一个更清晰的接口,哪怕只是给一个内部的布尔变量赋值,Switch.beOn()也比Switch.setState(true)要清晰的多。

范例

重构前

class Employee {
  static ENGINEER = 0;
  static SALESMAN = 1;
  static MANAGER = 2;

  static create(type) {
    switch(type) {
      case Employee.ENGINEER:
        return new Engineer()
      case Employee.SALESMAN:
        return new Salesman()
      case Employee.MANAGER:
        return new Manager()
      default:
        throw new Error('Incorrect type value')
    }
  }
}

const e = Employee.create(Employee.ENGINEER)

重构后

class Employee {
  static createEngineer() {
    return new Engineer()
  }

  static createSalesman() {
    return new Salesman()
  }

  static createManager() {
    return new Manager()
  }
}

const e = Employee.createEngineer()

七. Preserve Whole Object(保持对象完整)

介绍

  1. 场景
    你从某个对象中取出若干值,将他们作为某一次函数调用时的参数。
  2. 手法
    改为传递整个对象。

范例

重构前

class Room {
  withinPlan(plan) {
    const low = this.daysTempRange().getLow()
    const high = this.daysTempRange().getHigh()
    return plan.withinRange(low, high)
  }
}

class HeatingPlan {
  withinRange(low, high) {
    return low >= this._range.getLow() && high <= this._range.getHigh()
  }
}

重构后

class Room {
  withinPlan(plan) {
    return plan.withinRange(this.daysTempRange())
  }
}

class HeatingPlan {
  withinRange(arg) {
    return arg.getLow() >= this._range.getLow() && arg.getHigh() <= this._range.getHigh()
  }
}

八. Replace Parameter with Methods(以函数取代参数)

介绍

  1. 场景
    对象调用某个函数,并将所得结果作为参数,传递给另一个函数。而接受该参数的函数本身也能够调用前一个函数。
  2. 手法
    让参数接受者去除该项参数,并直接调用前一个函数。

动机

  1. 如果函数可以通过其他途径获得参数值,那么他就不应该通过参数获取该值。

范例

重构前

getPrice() {
  const basePrice = this._quantity * this._itemPrice
  const discountLevel = this._quantity > 100 ? 2 : 1
  return this.discountPrice(basePrice, discountLevel)
}

discountPrice(basePrice, discountLevel) {
  return discountLevel === 2 ? basePrice * 0.1 : basePrice * 0.05
}

重构后

getPrice() {
  return this.getDiscountLevel() === 2 ? this.getBasePrice() * 0.1 : this.getBasePrice() * 0.05
}

getBasePrice() {
  return this._quantity * this._itemPrice
}

getDiscountLevel() {
  return this._quantity > 100 ? 2 : 1
}

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

介绍

  1. 场景
    某些参数总是很自然地同时出现。
  2. 手法
    以一个对象取代这些参数。

动机

  1. 经常看到特定的一组参数总是一起被传递。可能有好几个函数都使用这一组参数,这些函数可能隶属与同一个类,也可能隶属于不同的类。
  2. 这样的一组参数就是所谓的数据泥团,我们可以运用一个对象包装所有这些数据,再以该对象取代他们。

范例

重构前

class Entry{
  constructor(value, chargeDate) {
    this._value = value
    this._chargeDate = chargeDate
  }

  getDate() {
    return this._chargeDate
  }

  getValue() {
    return this._value
  }
}

class Account{
  getFlowBetween(start, end) {
    let result = 0
    this._entries.forEach(entry => {
      if(entry.getDate().equals(start) || entry.getDate().equals(end) || (entry.getDate().after(start) && entry.getDate().before(end))) {
        result += entry.getValue()
      }
    })
    return result
  }
}

const flow = anAccount.getFlowBetween(startDate, endDate)

重构后

class Entry{
  constructor(value, chargeDate) {
    this._value = value
    this._chargeDate = chargeDate
  }

  getDate() {
    return this._chargeDate
  }

  getValue() {
    return this._value
  }
}

class DateRange {
  constructor(start, end) {
    this._start = start
    this._end = end
  }

  getStart() {
    return this._start
  }

  getEnd() {
    return this._end
  }

  includes(arg) {
    return arg.equals(this._start) || 
    arg.equals(this._end) || 
    (arg.after(this._start) && arg.before(this._end))
  }
}

class Account{
  getFlowBetween(range) {
    let result = 0
    this._entries.forEach(entry => {
      if(range.includes(entry.getDate())){
        result += entry.getValue()
      }
    })
    return result
  }
}

const flow = anAccount.getFlowBetween(new DateRange(startDate, endDate))

十. Remove Setting Method(移除设置函数)

介绍

  1. 场景
    类中的某个字段应该在对象创建时被设值,然后就不再改变。
  2. 手法
    去掉该字段的所有设值函数。

范例

重构前

class Account {
  constructor(id) {
    this.setId(id)
  }

  setId(arg) {
    this._id = arg
  }
}

重构后

class Account {
  constructor(id) {
    this._id = id
  }
}

十一. Hide Method(隐藏函数)

介绍

  1. 场景
    有一个函数,从来没有被其他任何类用到。
  2. 手法
    将这个函数修改为private

动机

  1. 重构往往促使你修改函数的可见度。
  2. 当你面对一个过于丰富、提供了过多行为的接口时,就值得将非必要的取值函数和设值函数隐藏起来。

十二. Replace Constructor with Factory Method(以工厂函数取代构造函数)

介绍

  1. 场景
    你希望在创建对象时不仅仅是做简单的建构工作。
  2. 手法
    将构造函数替换为工厂函数。

范例

重构前

class Employee {
  static ENGINEER = 0;
  static SALESMAN = 1;
  static MANAGER = 2;

  constructor(type) {
    this._type = type
  }
}

重构后

class Employee {
  static ENGINEER = 0;
  static SALESMAN = 1;
  static MANAGER = 2;

  static create(type) {
    return new Employee(type)
  }

  constructor(type) {
    this._type = type
  }
}

十三. Encapsulate Downcast(封装向下转型)

介绍

  1. 场景
    某个函数返回的对象,需要由函数调用者执行向下转型。
  2. 手法
    将向下转型动作移至函数中。

范例

JavaScript无需转型,无法演示该重构手法。

十四. Replace Error Code with Exception(以异常取代错误码)

介绍

  1. 场景
    某个函数返回一个特定的代码,用以表示某种错误情况。
  2. 手法
    改用异常。

动机

演示

上一篇下一篇

猜你喜欢

热点阅读