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

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,734评论 6 505
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,931评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,133评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,532评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,585评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,462评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,262评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,153评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,587评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,792评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,919评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,635评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,237评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,855评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,983评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,048评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,864评论 2 354

推荐阅读更多精彩内容