重构:改善既有代码的设计-笔记(三)

重构:改善既有代码的设计-购书链接

重新组织数据

  1. 拆分变量

    一个变量只承担一种责任

    1
    2
    3
    4
    function distanceTravelled(){
    let acc = 0
    acc = ''
    }
    1
    2
    3
    4
    function distanceTravelled(){
    let acc = 0
    let str = ''
    }

    做法

    1. 在待分解变量的声明及其第一次被赋值处,修改其名称
    2. 如果可能的话,把新变量声明为不可修改
    3. 以该变量的第二次赋值动作为分解,修改此前对该变量的引用,引用新变量
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    function distanceTravelled(scenario, time){
    let result
    let acc = scenario.primaryForce / scenario.mass
    let primaryTime = Math.min(time, scenario.delay)
    result = 0.5 * acc * primaryTime * primaryTime
    let secondTime = time - scenario.delay
    if(secondTime > 0){
    let primaryVelocity = acc * scenario.delay
    acc = (scenario.primaryForce + scenario.secondForce) / scenario.mass
    result += primaryVelocity * secondTime + 0.5 * acc * secondTime * secondTime
    }
    return result
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    function distanceTravelled(scenario, time){
    let result
    let primaryAcceleration = scenario.primaryForce / scenario.mass
    let primaryTime = Math.min(time, scenario.delay)
    result = 0.5 * primaryAcceleration * primaryTime * primaryTime
    let secondTime = time - scenario.delay
    if(secondTime > 0){
    let primaryVelocity = primaryAcceleration * scenario.delay
    let secondaryAcceleration = (scenario.primaryForce + scenario.secondForce) / scenario.mass
    result += primaryVelocity * secondTime + 0.5 * secondaryAcceleration * secondTime * secondTime
    }
    return result
    }
  2. 字段改名

    将不符合语义的字段名改为更加贴切的名字

    1
    const organization = { name: 'Acme', country: 'GB' }
    1
    2
    3
    4
    5
    6
    7
    class Organization {
    constructor(data){
    this._title = data.title || data.name
    }
    get title() {return this._title }
    set title(title) {this._title = title}
    }

    做法

    1. 如果记录未封装,则先封装记录
    2. 对字段改名,并将引用处同步更新
    1
    const organization = { name: 'Acme', country: 'GB' }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    class Organization {
    constructor(data){
    this._title = data.title || data.name
    this._country = data.country
    }
    get title() {return this._title }
    set title(title) {this._title = title}
    get country() {return this._country}
    set country(country) {this._country = country}
    }
    const organization = new Organization({ name: 'Acme', country: 'GB' })
  3. 以查询取代派生变量

    可变对数据是软件中最大对错误源头,对数据对修改常常导致代码各个部分以丑陋的形式互相耦合

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    class ProductionPlan {
    constructor(adjustments){
    this._adjustments = adjustments
    this._production = this._adjustments.reduce((prev, item) => prev + item.amount, 0)
    }
    get production(){return this._production}
    applyAdjustment(anAdjustment){
    this._adjustments.push(anAdjustment)
    this._production += anAdjustment.amount
    }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    class ProductionPlan {
    constructor(adjustments){
    this._adjustments = adjustments
    }
    get production(){return this._adjustments.reduce((prev, item) => prev + item.amount, 0)}
    applyAdjustment(anAdjustment){
    this._adjustments.push(anAdjustment)
    }
    }

    做法

    1. 识别出所有对变量做更新的地方
    2. 新建一个函数用于计算该变量的值
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    class ProductionPlan {
    constructor(adjustments){
    this._adjustments = adjustments
    this._production = this._adjustments.reduce((prev, item) => prev + item.amount, 0)
    }
    get production(){return this._production}
    applyAdjustment(anAdjustment){
    this._adjustments.push(anAdjustment)
    this._production += anAdjustment.amount
    }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    class ProductionPlan {
    constructor(adjustments){
    this._adjustments = adjustments
    }
    get production(){return this._adjustments.reduce((prev, item) => prev + item.amount, 0)}
    applyAdjustment(anAdjustment){
    this._adjustments.push(anAdjustment)
    }
    }
  4. 将引用对象改为值对象(将值对象改成引用对象)

    更新一个引用对象的属性值,直接替换整个内部对象

    1
    2
    3
    4
    5
    6
    class Person {
    constructor(){
    this._tele = new TelephoneNumber()
    }
    }
    class Tele{...}
    1
    2
    3
    4
    5
    class Person {
    set officeAreaCode(arg){return this._tele = new Tele() }
    set officeNumber(arg){return this._tele = new Tele() }
    }
    class Tele{...}

    做法

    1. 检查重构目标是否为不可变的对象,或者是否修改为不可变对象
    2. 修改对象中的设值函数,创建一个新的对象
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    class Person {
    constructor(name){
    this._name = name
    this._telephoneNumber = new TelephoneNumber()
    }
    get name(){return this._name }
    get telephoneNumber(){return this._telephoneNumber.toString() }
    get officeAreaCode(){return this._telephoneNumber.areaCode }
    set officeAreaCode(arg){return this._telephoneNumber.areaCode = arg }
    get officeNumber(){return this._telephoneNumber.number }
    set officeNumber(arg){return this._telephoneNumber.number = arg }
    }
    class TelephoneNumber{
    get number(){return this._number}
    set number(arg){return this._number = arg}
    get areaCode(){return this._areaCode }
    set areaCode(arg){return this._areaCode = arg}
    toString(){return `(${this._areaCode}) ${this._number}`}
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    class Person {
    constructor(name){
    this._name = name
    }
    get name(){return this._name }
    get telephoneNumber(){return this._telephoneNumber.toString() }
    get officeAreaCode(){return this._telephoneNumber.areaCode }
    set officeAreaCode(arg){return this._telephoneNumber = new TelephoneNumber(arg, this.officeNumber) }
    get officeNumber(){return this._telephoneNumber.number }
    set officeNumber(arg){return this._telephoneNumber = new TelephoneNumber(this.areaCode, arg) }
    }
    class TelephoneNumber{
    constructor(areaCode, number){
    this._areaCode = areaCode
    this._number = number
    }
    get number(){return this._number}
    get areaCode(){return this._areaCode }
    toString(){return `(${this._areaCode}) ${this._number}`}
    }
  5. 将值对象改成引用对象(将引用对象改为值对象)

    过多的副本会难以维护,可以创建一个仓库存储

    1
    2
    3
    4
    5
    6
    class Order {
    constructor(data){
    this._customer = new Customer(data.customer)
    }
    }
    class Customer {}
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    let _repository = {}
    _repository.customer = new Map()
    function registerCustomer(id){
    if(!_repository.customer.has(id)){
    _repository.customer.set(id, new Customer(id))
    }
    return findCustomer(id)
    }
    function findCustomer(id){
    return _repository.customer.get(id)
    }
    class Order {
    constructor(data){
    this._customer = registerCustomer(data.customer)
    }
    }
    class Customer {}

    做法

    1. 为相关的对象创建一个仓库
    2. 确保构造函数有办法找到关联对象的正确实例
    3. 修改宿主对象的构造函数,令其从仓库中获取关联对象
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    class Order {
    constructor(data){
    this._number = data.number;
    this._customer = new Customer(data.customer)
    }
    get customer(){return this._customer}
    }
    class Customer {
    constructor(id){
    this._id = id
    }
    get id(){return this._id}
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    let _repositoryData
    function initialize(){
    _repositoryData = {}
    _repositoryData.customer = new Map()
    }
    function registerCustomer(id){
    if(!_repositoryData.customer.has(id)){
    _repositoryData.customer.set(id, new Customer(id))
    }
    return findCustomer(id)
    }
    function findCustomer(id){
    return _repositoryData.customer.get(id)
    }
    initialize()
    class Order {
    constructor(data){
    this._number = data.number;
    this._customer = registerCustomer(data.customer)
    }
    get customer(){return this._customer}
    }
    class Customer {
    constructor(id){
    this._id = id
    }
    get id(){return this._id}
    }

重新组织数据.png

简化条件逻辑

  1. 分解条件表达式

    把复杂的条件表达式分解成多个独立的函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    function price(){
    ...
    if(aDate.isSummer || aDate.isSpring){
    charge = quantity * aPlan.summerRate
    }else{
    charge = quantity * aPlan.rate
    }
    ...
    }
    1
    2
    3
    4
    5
    6
    7
    function price(aDate, aPlan){
    ...
    return summerOrSpring() ? summerCharge() : orderCharge()
    function summerOrSpring(){...}
    function summerCharge(){...}
    function orderCharge(){...}
    }

    做法

    1. 对条件判断和每个条件分支都使用提炼函数
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    function price(aDate, aPlan){
    let charge
    let quantity = 100
    if(aDate.isSummer || aDate.isSpring){
    charge = quantity * aPlan.summerRate
    }else{
    charge = quantity * aPlan.rate
    }
    return charge
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    function price(aDate, aPlan){
    let quantity = 100
    return summerOrSpring() ? summerCharge() : orderCharge()
    function summerOrSpring(){
    return aDate.isSummer || aDate.isSpring
    }
    function summerCharge(){
    return quantity * aPlan.summerRate
    }
    function orderCharge(){
    return quantity * aPlan.rate
    }
    }
  2. 合并条件表达式

    把相同返回的条件合并成一处

    1
    2
    3
    if(state == 1) return 0
    if(start == 2) return 0
    return 1
    1
    2
    if(state == 1 || start == 2) return 0
    return 1

    做法

    1. 确定条件表达式没有副作用
    2. 使用适当的运算符合并
    1
    2
    3
    4
    if(state == 1) return 0
    if(start == 2) return 0
    if(end == 3) return 0
    return 1
    1
    2
    if(state == 1 || start == 2 || end == 3) return 0
    return 1
  3. 以卫语句取代嵌套条件表达式

    多层的嵌套判断会减低可读性,可以使用卫语句进行提前返回

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    function payAmount(employee){
    if(employee.isSeparated){
    ...
    }else{
    if(employee.isRetired){
    ...
    }else{
    ...
    }
    }
    }
    1
    2
    3
    4
    5
    function payAmount(employee){
    if(employee.isSeparated) return
    if(employee.isRetired) return
    return
    }

    做法

    1. 选中外层需要被替换的条件逻辑,替换成卫语句
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    function payAmount(employee){
    let result
    if(employee.isSeparated){
    result = {amount: 0, reasonCode: 'SEP'}
    }else{
    if(employee.isRetired){
    result = {amount: 0, reasonCode: 'RET'}
    }else{
    result = {amount: 1000, reasonCode: ''}
    }
    }
    return result
    }
    1
    2
    3
    4
    5
    function payAmount(employee){
    if(employee.isSeparated) return {amount: 0, reasonCode: 'SEP'}
    if(employee.isRetired) return {amount: 0, reasonCode: 'RET'}
    return {amount: 1000, reasonCode: ''}
    }
  4. 以多态取代条件表达式

    通过类的多态去改善比较复杂的条件表达式

    1
    2
    3
    4
    5
    6
    7
    8
    9
    class Bird {
    constructor(name, type){
    switch (bird.type){
    case 'E': this.plumage = 'e'
    case 'N': this.plumage = 'n'
    default: this.plumage = 'unknown'
    }
    }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    class Bird {
    get plumage(){
    return 'unknown'
    }
    }
    class E extends Bird{
    get plumage(){
    return 'e'
    }
    }
    class N extends Bird{
    get plumage(){
    return 'n'
    }
    }
    function createBird(...arg){
    switch (arg[1]){
    case 'E': return new E(...arg);
    case 'N': return new N(...arg);
    default: return new Bird(...arg);
    }
    }

    做法

    1. 如果现有的类不具备多态行为,就用工厂模式创建
    2. 在调用方代码中使用工程函数获得对象实例
    3. 将带有条件逻辑的函数移动到超类中
    4. 任选一个子类,在其中创建一个函数,使其复写超类中容纳条件表达式的那个函数
    5. 将于该子类相关的条件表达式分支复制到新函数中
    6. 处理完后将超类的函数声明为抽象函数
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    function plumages(birds){
    return new Map(birds.map(b => [b.name, b.plumage]))
    }
    function speeds(birds){
    return new Map(birds.map(b => [b.name, airSpeedVelocity(b)]))
    }
    function plumage(bird){
    switch (bird.type){
    case 'E': return 'a'
    case 'A': return bird.counts > 2 ? 't' : 'a'
    case 'N': return bird.voltage > 100 ? 's' : 'b'
    default: return 'unknown'
    }
    }
    function airSpeedVelocity(bird){
    switch (bird.type){
    case 'E': return 35
    case 'A': return 40 - bird.counts
    case 'N': return bird.voltage / 10 + 10
    default: return null
    }
    }
    class Bird {
    constructor(name, type, counts, voltage){
    this.name = name
    this.type = type
    this.counts = counts
    this.voltage = voltage
    this.plumage = plumage(this)
    }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    function plumages(birds){
    return new Map(birds.map(b => [b.name, b.plumage]))
    }
    function speeds(birds){
    return new Map(birds.map(b => [b.name, b.airSpeedVelocity]))
    }
    class Bird {
    constructor(name, type, counts, voltage){
    this.name = name
    this.type = type
    this.counts = counts
    this.voltage = voltage
    }
    get plumage(){
    return 'unknown'
    }
    get airSpeedVelocity(){
    return null
    }
    }
    class E extends Bird{
    get plumage(){
    return 'a'
    }
    get airSpeedVelocity(){
    return 35
    }
    }
    class A extends Bird{
    get plumage(){
    return this.counts > 2 ? 't' : 'a'
    }
    get airSpeedVelocity(){
    return 40 - this.counts
    }
    }
    class N extends Bird{
    get plumage(){
    this.voltage > 100 ? 's' : 'b'
    }
    get airSpeedVelocity(){
    return this.voltage / 10 + 10
    }
    }
    function createBird(...arg){
    switch (arg[1]){
    case 'E': return new E(...arg);
    case 'A': return new A(...arg);
    case 'N': return new N(...arg);
    default: return new Bird(...arg);
    }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    function rating(voyage, history){
    const vpf = voyageProfitFactor(voyage, history)
    const vr = voyageRisk(voyage)
    const chr = captainHistoryRisk(voyage, history)
    if(vpf * 3 > (vr + chr * 2)) return 'A'
    return 'B'
    }
    function voyageRisk(voyage){
    let result = 1
    if(voyage.length > 4) result += 2
    if(voyage.length > 8) result += voyage.length - 8
    if(['china', 'east-indies'].includes(voyage.zone)) result += 4
    return Math.max(result, 0)
    }
    function captainHistoryRisk(voyage, history){
    let result = 1
    if(history.length < 5) result += 4
    result += history.filter(v => v.profit < 0).length
    if(voyage.zone === 'china' && hasChina(history)) result -= 2
    return Math.max(result, 0)
    }
    function hasChina(history) {
    return history.some(v => v.zone === 'china')
    }
    function voyageProfitFactor(voyage, history){
    let result = 2
    if(voyage.zone === 'china') result += 1
    if(voyage.zone === 'east-indies') result += 1
    if(voyage.zone === 'china' && hasChina(history)){
    result += 3
    if(history.length > 10) result += 1
    if(voyage.length > 12) result += 1
    if(voyage.length > 18) result -= 1
    }else{
    if(history.length > 8) result += 1
    if(voyage.length > 14) result -= 1
    }
    return result
    }
    const voyage = { zone: 'west-indies', length: 10 }
    const history = [
    { zone: 'east-indies', profit: 5 },
    { zone: 'west-indies', profit: 15 },
    { zone: 'china', profit: -2 },
    { zone: 'west-africa', profit: 7 }
    ]
    console.log(rating(voyage, history))
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    function rating(voyage, history){
    return createRating(voyage, history).value
    }
    function createRating(voyage, history){
    if(voyage.zone === 'china' && history.some(v => v.zone === 'china')) return new ExperienceChinaRating(voyage, history)
    return new Rating(voyage, history)
    }
    class Rating {
    constructor(voyage, history){
    this.voyage = voyage
    this.history = history
    }
    get value(){
    const vpf = this.voyageProfitFactor
    const vr = this.voyageRisk
    const chr = this.captainHistoryRisk
    if(vpf * 3 > (vr + chr * 2)) return 'A'
    return 'B'
    }
    get voyageProfitFactor(){
    let result = 2
    if(this.voyage.zone === 'china') result += 1
    if(this.voyage.zone === 'east-indies') result += 1
    result += this.historyLengthFactor
    result += this.voyageLengthFactor
    return result
    }
    get voyageLengthFactor(){
    return this.voyage.length > 14 ? 1 : 0
    }
    get historyLengthFactor(){
    return this.history.length > 8 ? 1 : 0
    }
    get voyageRisk(){
    let result = 1
    if(this.voyage.length > 4) result += 2
    if(this.voyage.length > 8) result += this.voyage.length - 8
    if(['china', 'east-indies'].includes(this.voyage.zone)) result += 4
    return Math.max(result, 0)
    }
    get captainHistoryRisk(){
    let result = 1
    if(this.history.length < 5) result += 4
    result += this.history.filter(v => v.profit < 0).length
    return Math.max(result, 0)
    }
    }
    class ExperienceChinaRating extends Rating{
    get captainHistoryRisk(){
    const result = super.captainHistoryRisk - 2
    return result
    }
    get voyageProfitFactor(){
    return super.voyageProfitFactor + 3
    }
    get voyageLengthFactor(){
    let result = 0
    if(this.voyage.length > 12) result += 1
    if(this.voyage.length > 18) result -= 1
    return result
    }
    get historyLengthFactor(){
    return this.history.length > 10 ? 1 : 0
    }
    }
    const voyage = { zone: 'west-indies', length: 10 }
    const history = [
    { zone: 'east-indies', profit: 5 },
    { zone: 'west-indies', profit: 15 },
    { zone: 'china', profit: -2 },
    { zone: 'west-africa', profit: 7 }
    ]
    console.log(rating(voyage, history))
  5. 引入特例

    将多个相同的特殊情况取值收拢于一处

    1
    2
    3
    4
    5
    6
    7
    8
    9
    function customerName(aCustomer){
    if(aCustomer.toString() === 'unknown') return 'occupant'
    return aCustomer.name
    }
    class Customer {
    toString() {
    return this.name || 'unknown'
    }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    function customerName(aCustomer){
    return aCustomer.name
    }
    class Customer {
    toString() {
    return this.name || 'unknown'
    }
    }
    const customer = enrichCustomer(new Customer())
    function enrichCustomer(aCustomer){
    const unknownCustomer = {
    name: 'occupant',
    }
    if(aCustomer.toString() === 'unknown') return unknownCustomer
    return aCustomer
    }

    做法

    1. 给重构目标添加检查特例的属性,令其返回false
    2. 创建一个特例对象,其中只有检查特例的属性,返回true
    3. 对“与特例值做对比”提炼函数,并确保客户端使用这个新函数
    4. 将新的特例对象引入代码中,可以从函数调用中返回,也可以在变换函数中生成
    5. 修改特例对比函数的主题,在其直接使用检查特例的属性
    6. 使用函数组合成类、函数组合成变换,把通用的特例处理逻辑搬移到新建的特例对象中
    7. 对特例对比函数使用内联函数,将其内联到仍然需要的地方
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    function customerName(aCustomer){
    if(aCustomer.toString() === 'unknown') return 'occupant'
    return aCustomer.name
    }
    function customerPlan(aCustomer){
    if(aCustomer.toString() === 'unknown') return 'basic plan'
    return aCustomer.plan
    }
    class Customer {
    constructor(name, plan) {
    this.name = name
    this.plan = plan
    }
    toString() {
    return this.name || 'unknown'
    }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    function customerName(aCustomer){
    return aCustomer.name
    }
    function customerPlan(aCustomer){
    return aCustomer.plan
    }
    class Customer {
    constructor(name, plan) {
    this.name = name
    this.plan = plan
    }
    toString() {
    return this.name || 'unknown'
    }
    }
    const customer = enrichCustomer(new Customer())
    function enrichCustomer(aCustomer){
    const unknownCustomer = {
    name: 'occupant',
    plan: 'basic plan'
    }
    if(aCustomer.toString() === 'unknown') return unknownCustomer
    return aCustomer
    }
  6. 引入断言

    通过断言告诉阅读者,程序执行到这一点是,对当前状态做了何种假设
    做法

    1. 如果你发现代码假设的某个条件始终为真,就可以加入一个断言说明情况

简化条件逻辑.png

重构API

  1. 将查询函数和修改函数分离

    区分处有副作用和无副作用的函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    function setOffAlarms(){//setTimeout}
    function alertForMiscreant(people) {
    for (const p of people) {
    if(p === 'oo'){
    setOffAlarms()
    return p
    }
    }
    return ''
    }
    1
    2
    3
    4
    function alertForMiscreant(people) {
    if(findMiscreant(people) !== '') setOffAlarms()
    }
    function findMiscreant(people) {// find }

    做法

    1. 复制整个函数,将其作为一个查询命名
    2. 从新建的查询中去掉所有存在副作用的语句
    3. 查找调用函数的地方,替换
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    const people = ['xx', 'yy', 'zz', 'oo', 'zzz', 'cc']
    function setOffAlarms(){
    setTimeout(() => {
    console.log('!!!')
    }, 100)
    }
    function alertForMiscreant(people) {
    for (const p of people) {
    if(p === 'oo'){
    setOffAlarms()
    return p
    }
    if(p === 'cc'){
    setOffAlarms()
    return p
    }
    }
    return ''
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    function alertForMiscreant(people) {
    if(findMiscreant(people) !== '') setOffAlarms()
    }
    function findMiscreant(people) {
    for (const p of people) {
    if(p === 'oo' || p === 'cc'){
    return p
    }
    }
    return ''
    }
    const found = findMiscreant(people)
    alertForMiscreant(people)
  2. 函数参数化

    如果两个函数逻辑相似,只是字面量值不同,则可以合成一个函数,以参数的形式传入不同的值消除重复

    1
    2
    3
    4
    5
    6
    function tenPercentRaise(salary){
    return salary * 1.1
    }
    function fivePercentRaise(salary){
    return salary * 1.05
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    function raise(salary, factor=0){
    return salary * (factor + 1)
    }
    function tenPercentRaise(salary){
    return raise(salary, 0.1)
    }
    function fivePercentRaise(salary){
    return raise(salary, 0.05)
    }

    做法

    1. 从一组相似的函数中选择一个
    2. 把需要作为参数传入的字面量添加到参数列表中
    3. 修改函数所有的调用处,更新参数传入
    4. 修改函数体,使其使用新的参数
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    function baseCharge(usage){
    if(usage < 0) return 0
    return topBand(usage) * 0.07 + bottomBand(usage) * 0.03 + middleBand(usage) * 0.05
    }
    function bottomBand(usage){
    return Math.min(usage, 100)
    }
    function topBand(usage){
    return usage > 200 ? usage - 200 : 0
    }
    function middleBand(usage){
    return usage > 100 ? Math.min(usage, 200) - 100 : 0
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    function baseCharge(usage){
    if(usage < 0) return 0
    return withinBand(usage, 0, 100) * 0.03
    + withinBand(usage, 100, 200) * 0.05
    + withinBand(usage, 200, Infinity) * 0.07
    }
    function withinBand(usage, bottom, top){
    return usage > bottom ? Math.min(usage, top) - bottom : 0
    }
  3. 移除标记参数

    标记参数有时候会让人难以理解那些函数可以调用

    1
    2
    3
    4
    5
    6
    7
    function deliveryDate(anOrder, isRush){
    if(isRush){
    ...
    }else{
    ...
    }
    }
    1
    2
    3
    4
    5
    6
    function rushDeliveryDate(anOrder){
    ...
    }
    function regularDeliveryDate(anOrder){
    ...
    }

    做法

    1. 针对参数的每一种可能值新建一个函数
    2. 对于字面量作为参数的函数调用者,改为调用新建的明确函数
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    function deliveryDate(anOrder, isRush){
    if(isRush){
    let deliveryTime
    if(['MA', 'CT'].includes(anOrder.deliveryState)) deliveryTime = 1
    else if(['NY', 'NH'].includes(anOrder.deliveryState)) deliveryTime = 2
    else deliveryTime = 3
    return deliveryTime
    }else{
    let deliveryTime
    if(['MA', 'CT', 'NY'].includes(anOrder.deliveryState)) deliveryTime = 2
    else if(['ME', 'NH'].includes(anOrder.deliveryState)) deliveryTime = 3
    else deliveryTime = 4
    return deliveryTime
    }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    function rushDeliveryDate(anOrder){
    let deliveryTime
    if(['MA', 'CT'].includes(anOrder.deliveryState)) deliveryTime = 1
    else if(['NY', 'NH'].includes(anOrder.deliveryState)) deliveryTime = 2
    else deliveryTime = 3
    return deliveryTime
    }
    function regularDeliveryDate(anOrder){
    let deliveryTime
    if(['MA', 'CT', 'NY'].includes(anOrder.deliveryState)) deliveryTime = 2
    else if(['ME', 'NH'].includes(anOrder.deliveryState)) deliveryTime = 3
    else deliveryTime = 4
    return deliveryTime
    }
  4. 保持对象完整

    把一个对象内的字段拆成几个变量,再传入函数中,不如保持完整传入函数中去解析

    1
    2
    3
    const low = dayTempRange.low
    const high = dayTempRange.high
    function withinRange(low, high){...}
    1
    function withinRange(aNumberRange){...}

    做法

    1. 新建一个空函数,给它期望的参数列表
    2. 再新函数中调用旧函数,并将新参数映射到旧的参数列表中
    3. 逐一修改旧函数的调用者,令其使用新函数
    4. 使用内联函数将旧函数内联到新函数中,给新函数改名后,同时修改所有的引用点
    1
    2
    3
    4
    5
    6
    7
    8
    9
    const dayTempRange = { low: 10, high: 40 }
    const low = dayTempRange.low
    const high = dayTempRange.high
    if(withinRange(low, high)){
    console.log('123')
    }
    function withinRange(low, high){
    return low > 9 && 41 > high
    }
    1
    2
    3
    4
    5
    6
    if(withinRange(dayTempRange)){
    console.log('123')
    }
    function withinRange(aNumberRange){
    return aNumberRange.low > 9 && 41 > aNumberRange.high
    }
  5. 以查询取代参数

    频繁的传递参数会让函数看起来变得复杂

    1
    2
    3
    4
    5
    6
    class Order {
    get finalPrice(){
    return this.discountedPrice(basePrice, discount)
    }
    discountedPrice(price, discount){...}
    }
    1
    2
    3
    4
    5
    6
    7
    8
    class Order {
    get finalPrice(){
    return this.discountedPrice()
    }
    get basePrice() {...}
    get discount() {...}
    discountedPrice(){...}
    }

    做法

    1. 使用提炼函数将参数的计算过程提炼到一个独立的函数中
    2. 将函数体内引用该参数的地方改为调用新建的函数
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    class Order {
    constructor(quantity, price){
    this.quantity = quantity;
    this.price = price;
    }
    get finalPrice(){
    const basePrice = this.price * this.quantity
    let discount
    if(this.quantity > 100) discount = 2
    else discount = 1
    return this.discountedPrice(basePrice, discount)
    }
    discountedPrice(price, discount){
    switch(discount){
    case 1: return price * 0.9
    case 2: return price * 0.8
    }
    }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    class Order {
    constructor(quantity, price){
    this.quantity = quantity;
    this.price = price;
    }
    get finalPrice(){
    return this.discountedPrice()
    }
    get basePrice() { return this.price * this.quantity }
    get discount() { return this.quantity > 100 ? 2 : 1 }
    discountedPrice(){
    switch(this.discount){
    case 1: return this.basePrice * 0.9
    case 2: return this.basePrice * 0.8
    }
    }
    }
  6. 以参数取代查询

    当遇到引用复杂,需要调用者弄清参数意义时,需要用参数取代查询

    1
    2
    3
    4
    5
    6
    7
    8
    const thermostat = {}
    class HeatingPlan {
    get targetTemperature(){
    if(thermostat.t > this.max) return this.max
    else if(thermostat.t < this.min) return this.min
    return thermostat.t
    }
    }
    1
    2
    3
    4
    5
    6
    7
    class HeatingPlan {
    targetTemperature(t){
    if(t > this.max) return this.max
    else if(t < this.min) return this.min
    return t
    }
    }

    做法

    1. 对执行查询操作对代码提炼变量,将其从函数体中剥离出来
    2. 现有的函数体不再执行查询操作
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    const thermostat = {
    selectTemperature: 20
    }
    function setToHeat(){
    thermostat.selectTemperature += 10
    }
    function setToCool(){
    thermostat.selectTemperature -= 10
    }
    class HeatingPlan {
    constructor(max, min){
    this.max = max;
    this.min = min;
    }
    get targetTemperature(){
    if(thermostat.selectTemperature > this.max) return this.max
    else if(thermostat.selectTemperature < this.min) return this.min
    return thermostat.selectTemperature
    }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    function setToHeat(){
    thermostat.selectTemperature += 10
    }
    function setToCool(){
    thermostat.selectTemperature -= 10
    }
    class HeatingPlan {
    constructor(max, min){
    this.max = max;
    this.min = min;
    }
    targetTemperature(selectTemperature){
    if(selectTemperature > this.max) return this.max
    else if(selectTemperature < this.min) return this.min
    return selectTemperature
    }
    }
  7. 移除设值函数

    如果不可变的数据,不暴露修改的方法

    1
    2
    3
    4
    class Person {
    get name(){return this._name }
    set name(arg){ return this._name = arg }
    }
    1
    2
    3
    class Person {
    get name(){return this._name }
    }

    做法

    1. 使用私有字段的实现方式
    2. 移除设值函数
    1
    2
    3
    4
    5
    6
    7
    class Person {
    constructor(name){
    this._name = name;
    }
    get name(){return this._name }
    set name(arg){ return this._name = arg }
    }
    1
    2
    3
    4
    5
    6
    class Person {
    constructor(name){
    this._name = name;
    }
    get name(){return this._name }
    }
  8. 以工厂函数取代构造函数

    构造函数在部分普通场合难以适用时,可以将其改为工厂函数

    1
    2
    3
    4
    5
    6
    class Employee{
    constructor(name, typeCode){
    this. _name = name
    this._typeCode = typeCode
    }
    }
    1
    2
    3
    function createEngineer(name){
    return new Employee(name, 'E')
    }

    做法

    1. 新建一个工厂函数,让它地道用现有的构造函数
    2. 把调用构造函数的方法改为调用工厂函数
    3. 尽量修改构造函数的可见范围
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    class Employee{
    constructor(name, typeCode){
    this. _name = name
    this._typeCode = typeCode
    }
    get name(){return this._name }
    get type(){return Employee.legalTypeCode[this._typeCode] }
    static get legalTypeCode(){
    return { 'E': 'Engineer', 'M': 'Manager', 'S': 'Salesman' }
    }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    class Employee{
    constructor(name, typeCode){
    this. _name = name
    this._typeCode = typeCode
    }
    get name(){return this._name }
    get type(){return Employee.legalTypeCode[this._typeCode] }
    static get legalTypeCode(){
    return { 'E': 'Engineer', 'M': 'Manager', 'S': 'Salesman' }
    }
    }
    function createEngineer(name){
    return new Employee(name, 'E')
    }
  9. 以命令取代函数

    当函数里拥有许多复杂操作时,可以改成命令对象模式去处理

    1
    2
    3
    function score() {
    ...
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    function score() {
    return new Score().execute()
    }
    class Scorer{
    execute(){
    this.scoreSmoking()
    this.stateWithLowCertification()
    }
    scoreSmoking(){}
    stateWithLowCertification(){}
    }

    做法

    1. 创建一个空类,包含其目标函数
    2. 给每个参数都创建一个字段
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    function score(candidate, medicalExam, scoringGuide) {
    let result = 0
    let healthLevel = 0
    let highMedicalRiskFlag = false
    if(medicalExam.isSmoker){
    healthLevel += 10
    highMedicalRiskFlag = true
    }
    let certificationGrade = 'regular'
    if(scoringGuide.stateWithLowCertification(candidate.originState)){
    certificateGrade = 'low'
    result -= 5
    }
    result -= Math.max(healthLevel - 5, 0)
    return result
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    function score(candidate, medicalExam, scoringGuide) {
    return new Score(candidate, medicalExam, scoringGuide).execute()
    }
    class Scorer{
    constructor(candidate, medicalExam, scoringGuide){
    this._candidate = candidate
    this._medicalExam = medicalExam
    this._scoringGuide = scoringGuide
    }
    execute(){
    this._result = 0
    this._healthLevel = 0
    this._highMedicalRiskFlag = false
    this.scoreSmoking()
    this.stateWithLowCertification()
    this._result -= Math.max(this._healthLevel - 5, 0)
    return this._result
    }
    scoreSmoking(){
    if(this._medicalExam.isSmoker){
    this._healthLevel += 10
    this._highMedicalRiskFlag = true
    }
    }
    stateWithLowCertification(){
    this._certificationGrade = 'regular'
    if(this._scoringGuide.stateWithLowCertification(this._candidate.originState)){
    this._certificationGrade = 'low'
    this._result -= 5
    }
    }
    }
  10. 以函数取代命令

    使用函数去完成比较简单的任务

    1
    2
    3
    4
    5
    6
    7
    class ChargeCalculator{
    get basePrice(){}
    get charge(){}
    }
    function charge(){
    return new ChargeCalculator().charge
    }
    1
    2
    3
    function charge(){
    ...
    }

    做法

    1. 对命令对象在执行阶段用到的函数使用内联函数
    2. 将构造函数中的参数转移到执行函数
    3. 对所有的字段在执行函数中找到引用的地方,并改为参数
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    class ChargeCalculator{
    constructor(customer, usage, provider){
    this.customer = customer
    this.usage = usage
    this.provider = provider
    }
    get basePrice(){
    return this.customer.baseRate * this.usage
    }
    get charge(){
    return this.basePrice + this.provider.connectionCharge
    }
    }
    function charge(customer, usage, provider){
    return new ChargeCalculator(customer, usage, provider).charge
    }
    1
    2
    3
    4
    function charge(customer, usage, provider){
    const basePrice = customer.baseRate * usage
    return basePrice + provider.connectionCharge
    }

重构API.png

处理继承关系

  1. 函数上移

    如果某个函数在各个子类的函数体中相同,则将函数上移

    1
    2
    3
    4
    5
    6
    7
    8
    class Party {
    }
    class Employee extends Party {
    annualCost(){ ... }
    }
    class Department extends Party {
    annualCost(){ ... }
    }
    1
    2
    3
    4
    5
    6
    7
    class Party {
    annualCost(){ ... }
    }
    class Employee extends Party {
    }
    class Department extends Party {
    }

    做法

    1. 检查待提升函数,确定完全一致
    2. 检查函数体内引用的所有函数调用和字段都能从超类调用
    3. 如果待提升签名不同,则都需要修改成超类的字段名
    4. 在超类中新建一个函数,将某个待提升函数的代码复制到超类中
    5. 移除待提升的函数
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    class Party {
    constructor(monthlyCost){
    this.monthlyCost = monthlyCost
    }
    }
    class Employee extends Party {
    get annualCost(){ return this.monthlyCost * 12 }
    }
    class Department extends Party {
    get totalAnnualCost(){ return this.monthlyCost * 12 }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    class Party {
    constructor(monthlyCost){
    this.monthlyCost = monthlyCost
    }
    get annualCost(){ return this.monthlyCost * 12 }
    }
    class Employee extends Party {
    }
    class Department extends Party {
    }
  2. 字段上移

    如果字段在各个子类中,字段可提升到超类

    1
    2
    3
    4
    5
    6
    7
    8
    class Party {
    }
    class Employee extends Party {
    get annualCost(){ ... }
    }
    class Department extends Party {
    get annualCost(){ ... }
    }
    1
    2
    3
    4
    5
    6
    7
    class Party {
    get annualCost(){ ... }
    }
    class Employee extends Party {
    }
    class Department extends Party {
    }

    做法

    1. 针对待提升的字段,检查他们的所有使用点,确定以同样的方式使用
    2. 如果字段名称不同,则先用变量改名
    3. 在超类中新增字段
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      class Party {
      constructor(monthlyCost){
      this.monthlyCost = monthlyCost
      }
      }
      class Employee extends Party {
      get annualCost(){ return this.monthlyCost * 12 }
      }
      class Department extends Party {
      get totalAnnualCost(){ return this.monthlyCost * 12 }
      }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    class Party {
    constructor(monthlyCost){
    this.monthlyCost = monthlyCost
    }
    get annualCost(){ return this.monthlyCost * 12 }
    }
    class Employee extends Party {
    }
    class Department extends Party {
    }
  3. 构造函数本体上移

    子类中存在共同实例属性,则可以提升到超类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    class Party {
    }
    class Employee extends Party {
    constructor(){
    super()
    this.name = name
    }
    }
    class Department extends Party {
    constructor(){
    super()
    this.name = name
    }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    class Party {
    constructor(name){
    this.name = name
    }
    }
    class Employee extends Party {
    }
    class Department extends Party {
    }

    做法

    1. 如果超类不存在构造函数,直接定义一个。确保子类调用超类到构造函数
    2. 使用移动语句将子类中构造函数中的公共语句,移动到超类到构造函数调用的语句
    3. 逐一移除子类间的公共代码,将其提升到超类的构造函数中
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      class Party {
      }
      class Employee extends Party {
      constructor(name, id, monthlyCost){
      super()
      this.name = name
      this.id = id
      this.monthlyCost = monthlyCost
      }
      }
      class Department extends Party {
      constructor(name, staff){
      super()
      this.name = name
      this.staff = staff
      }
      }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    class Party {
    constructor(name){
    this.name = name
    }
    }
    class Employee extends Party {
    constructor(name, id, monthlyCost){
    super(name)
    this.id = id
    this.monthlyCost = monthlyCost
    }
    }
    class Department extends Party {
    constructor(name, staff){
    super(name)
    this.staff = staff
    }
    }
  4. 函数下移

    如果某个函数只一个或者几个子类调用,则将函数下移到子类中

    1
    2
    3
    4
    5
    class Party {
    annualCost(){...}
    }
    class Employee extends Party {}
    class Department extends Party {}
    1
    2
    3
    4
    5
    class Party {}
    class Employee extends Party {
    annualCost(){...}
    }
    class Department extends Party {}

    做法

    1. 将超类的函数移动到目标子类中
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    class Party {
    constructor(monthlyCost){
    this.monthlyCost = monthlyCost
    }
    annualCost(){ return this.monthlyCost * 12 }
    }
    class Employee extends Party {
    get cost(){
    return this.annualCost() * 10
    }
    }
    class Department extends Party {
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    class Party {
    constructor(monthlyCost){
    this.monthlyCost = monthlyCost
    }
    }
    class Employee extends Party {
    annualCost(){ return this.monthlyCost * 12 }
    get cost(){
    return this.annualCost() * 10
    }
    }
    class Department extends Party {
    }
  5. 字段下移

    如果某个字段只被一个子类用到,就将其搬移到需要该字段到子类中

    1
    2
    3
    4
    5
    class Party {
    get annualCost(){...}
    }
    class Employee extends Party {}
    class Department extends Party {}
    1
    2
    3
    4
    5
    class Party {}
    class Employee extends Party {
    get annualCost(){...}
    }
    class Department extends Party {}

    做法

    1. 将超类中到字段移动到目标子类
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    class Party {
    constructor(monthlyCost){
    this.monthlyCost = monthlyCost
    }
    get annualCost(){ return this.monthlyCost * 12 }
    }
    class Employee extends Party {
    get cost(){
    return this.annualCost() * 10
    }
    }
    class Department extends Party {
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    class Party {
    constructor(monthlyCost){
    this.monthlyCost = monthlyCost
    }
    get annualCost(){ return this.monthlyCost * 12 }
    }
    class Employee extends Party {
    get cost(){
    return this.annualCost * 10
    }
    }
    class Department extends Party {
    }
  6. 以子类取代类型码

    用多种子类替代类型码判断

    1
    2
    3
    class Employee {
    validateType(type){...}
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    class Employee {}
    class Engineer extends Employee{
    get type(){}
    }
    class Salesman extends Employee{
    get type(){}
    }
    function createEmployee(name, type){
    switch(type){
    case 'engineer': return new Engineer(name)
    case 'salesman': return new Salesman(name)
    }
    }

    做法

    1. 封装类型码字段
    2. 任选一个类型码取值,创建一个子类,复写类型码类的取值函数,令其返回类型码的字面量
    3. 创建一个选择器的逻辑,把类型码参数映射到新的子类
    4. 正对每个类型码都重复创建子类
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    class Employee {
    constructor(name, type){
    this.validateType(type)
    this._name = name
    this._type = type
    }
    validateType(type){
    if(!['engineer', 'salesman', 'manager'].includes(type)){
    throw new Error('Invalid type')
    }
    }
    toString() { return `${this._name} (${this._type})`}
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    class Employee {
    constructor(name){
    this._name = name
    }
    toString() { return `${this._name} (${this.type})`}
    }
    class Engineer extends Employee{
    get type(){ return 'engineer' }
    }
    class Salesman extends Employee{
    get type(){ return 'salesman' }
    }
    class Manager extends Employee{
    get type(){ return 'manager' }
    }
    function createEmployee(name, type){
    switch(type){
    case 'engineer': return new Engineer(name)
    case 'salesman': return new Salesman(name)
    case 'manager': return new Manager(name)
    default: throw new Error('Invalid type')
    }
    }
  7. 移除子类

    如果子类的用处太小,则可以移除子类,替换成超类的一个字段

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    class Person{
    get genderCode(){}
    }
    class Male extends Person{
    get genderCode(){}
    }
    class Female extends Person{
    get genderCode(){}
    }
    function isMale(aPerson){ return aPerson instanceof Male}
    1
    2
    3
    4
    5
    6
    class Person{
    constructor(){
    this.genderCode = genderCode || 'X'
    }
    get isMale(){ ... }
    }

    做法

    1. 把子类的构造函数包装到超类的工厂中
    2. 新建一个字段用于代表子类的类型
    3. 将原来针对子类的判断函数修改未使用新建类字段
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    class Person{
    constructor(name){
    this.name = name
    }
    get genderCode(){ return 'X' }
    }
    class Male extends Person{
    get genderCode(){ return 'X' }
    }
    class Female extends Person{
    get genderCode(){ return 'F' }
    }
    function isMale(aPerson){ return aPerson instanceof Male}
    1
    2
    3
    4
    5
    6
    7
    class Person{
    constructor(name, genderCode){
    this.name = name
    this.genderCode = genderCode || 'X'
    }
    get isMale(){ return this.genderCode === 'X' }
    }
  8. 提炼超类

    如果两个类在做相似的事情,可以继承机制把他们的相似之处提炼到超类中

    1
    2
    class Employee {}
    class Department {}
    1
    2
    3
    4
    5
    6
    7
    class Party{
    constructor(name){
    this.name = name
    }
    }
    class Employee extends Party{}
    class Department extends Party{}

    做法

    1. 为原本的类新建一个超类
    2. 把共同元素梳理出来移动到超类中
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    class Employee {
    constructor(name, id, monthlyCost){
    this.name = name
    this.id = id
    this.monthlyCost = monthlyCost
    }
    get annualCost(){ return this.monthlyCost * 12 }
    }
    class Department {
    constructor(name, staff){
    this.name = name
    this.staff = staff
    }
    get totalMonthlyCost(){
    return this.staff.map(e => e.monthlyCost).reduce((prev, cur) => prev + cur, 0)
    }
    get totalAnnualCost(){
    return this.totalMonthlyCost * 12
    }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    class Party{
    constructor(name){
    this.name = name
    }
    get annualCost(){ return this.monthlyCost * 12 }
    }
    class Employee extends Party{
    constructor(name, id, monthlyCost){
    super(name)
    this.id = id
    this.monthlyCost = monthlyCost
    }
    }
    class Department extends Party{
    constructor(name, staff){
    super(name)
    this.staff = staff
    }
    get monthlyCost(){
    return this.staff.map(e => e.monthlyCost).reduce((prev, cur) => prev + cur, 0)
    }
    }
  9. 折叠继承体系

    当子类与父类没多大差别时,直接折叠子类与超类

    做法

    1. 选择要移除的类
    2. 将其移动到目标类中
  10. 以委托取代子类

    继承存在局限性,应该用组合与继承相结合的方式去改造

    1
    2
    class Booking{}
    class PremiumBooking extends Booking{}
    1
    2
    3
    4
    5
    6
    7
    8
    9
    class Booking{
    _bePremium(){
    this._premium = new PremiumBookingDelegate(this, extra)
    }
    }
    class PremiumBookingDelegate{}
    function createPremiumBooking(extra){
    booking._bePremium(extra)
    }

    做法

    1. 如果构造函数由多个调用者,先用工厂函数包裹起来
    2. 创建一个空的委托累,这个类的构造函数应该接受所有子类特有的数据项,并以参数的形式接受一个指回超类的引用
    3. 在超类中添加一个字段,用于安放委托对象
    4. 修改子类的创建逻辑,使其初始化上述的委托字段,放入一个委托对象实例
    5. 选择一个子类的函数,使用搬移函数移动函数放入委托类
    6. 如果被搬移函数还在子类之外被调用,就把留在源类中的委托代码从子类移到超类,并委托代码前加上卫语句,检查委托对象存在。
    7. 如果没有,则直接移除代码,重复上述过程,直到所有函数都移动到委托类
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      class Booking{
      constructor(show, date){
      this.show = show
      this.date = date
      }
      get hasTalkBack(){
      return this.show.talkBack && !this.isPeakDay
      }
      get basePrice(){
      let result = this.show.price
      if(this.isPeakDay) result *= 1.15
      return result
      }
      }
      class PremiumBooking extends Booking{
      constructor(show, date, extra){
      super(show, date)
      this.extra = extra
      }
      get hasTalkBack(){
      return this.show.talkBack
      }
      get basePrice(){
      return super.basePrice * this.extra.fee
      }
      get hasDinner(){
      return this.extra.dinner && !this.isPeakDay
      }
      }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    class Booking{
    constructor(show, date){
    this.show = show
    this.date = date
    }
    get hasTalkBack(){
    return this._premiumDelegate ? this._premiumDelegate.hasTalkBack : this.show.talkBack && !this.isPeakDay
    }
    get basePrice(){
    let result = this.show.price
    if(this.isPeakDay) result *= 1.15
    return this._premiumDelegate ? this._premiumDelegate.extendBasePrice(result) : result
    }
    get hasDinner(){
    return this._premiumDelegate ? this._premiumDelegate.hasDinner : false
    }
    _bePremium(extra){
    this._premiumDelegate = new PremiumBookingDelegate(this, extra)
    }
    }
    class PremiumBookingDelegate{
    constructor(hostBooking, extra){
    this.host = hostBooking
    this.extra = extra
    }
    get hasTalkBack(){
    return this.host.show.talkBack
    }
    get hasDinner(){
    return this.extra.dinner && !this.host.isPeakDay
    }
    extendBasePrice(base){
    return base * this.extra.fee
    }
    }
    function createBooking(show, date){
    return new Booking(show, date)
    }
    function createPremiumBooking(show, date, extra){
    let result = new Booking(show, date)
    result._bePremium(extra)
    return result
    }
  11. 以委托取代超类

    优先使用继承,当继承有问题,使用委托取代超类

    1
    2
    class CatalogItem{...}
    class Scroll extends CatalogItem{...}
    1
    2
    3
    4
    5
    6
    class CatalogItem{...}
    class Scroll{
    constructor(catalogId, catalog){
    this.catalogItem = catalog.get(catalogId) // new CatalogItem()
    }
    }

    做法

    1. 在子类中新建一个字段,使其引用超类的一个对象,并将这个委托引用初始化为超类的实例
    2. 针对超类每个函数,在子类中创建一个转发函数,将请求转发给委托引用
    3. 当所有的超类函数都被转发函数覆盖后,去除继承关系
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    class CatalogItem{
    constructor(id, title, tags){
    this.id = id
    this.title = title
    this.tags = tags
    }
    hasTag(arg){ return this.tags.includes(arg) }
    }
    class Scroll extends CatalogItem{
    constructor(id, title, tags, dateLastCleaned){
    super(id, title, tags)
    this.lastCleaned = dateLastCleaned
    }
    needsCleaning(targetDate){
    const threshold = this.hasTag('revered') ? 700 : 1500
    return this.daysSinceLastCleaning(targetDate) > threshold
    }
    daysSinceLastCleaning(targetDate){
    return this.lastCleaned.until(targetDate)
    }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    class CatalogItem{
    constructor(id, title, tags){
    this.id = id
    this.title = title
    this.tags = tags
    }
    hasTag(arg){ return this.tags.includes(arg) }
    }
    class Scroll{
    constructor(id, dateLastCleaned, catalogId, catalog){
    this.id = id
    this.catalogItem = catalog.get(catalogId)
    this.lastCleaned = dateLastCleaned
    }
    get id(){return this.catalogItem.id}
    get title(){return this.catalogItem.title}
    get tags(){return this.catalogItem.tags}
    hasTag(arg){ return this.catalogItem.hasTag(arg) }
    needsCleaning(targetDate){
    const threshold = this.hasTag('revered') ? 700 : 1500
    return this.daysSinceLastCleaning(targetDate) > threshold
    }
    daysSinceLastCleaning(targetDate){
    return this.lastCleaned.until(targetDate)
    }
    }

处理继承关系.png