《重构》- 在对象之间搬移特性

一. 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)的所有函数提供委托函数,这里只展示了三个函数,其他函数的处理依此类推。

你可能感兴趣的:(《重构》- 在对象之间搬移特性)