重构-改善既有代码的设计(七)


处理继承关系

函数上移(Pull Up Method)

  • 示例

    class Employee {...}
    
    class Saleman extends Employee {
        get name() {...}
    }
    
    class Engineer extends Employee {
        get name() {...}
    }

    重构为

    class Employee{
        get name() {...}
    }
    
    class Saleman extends Employee {...}
    class Engineer extends Employee {...}
  • 动机

    如果某个函数在各个子类中的函数体都相同(它们很可能是通过复制粘贴得到的),这就是最显而易见的函数上移适用场合。

  • 做法

    1. 检查待提升函数,确定它们是完全一致的。
    2. 检查函数体内引用的所有函数调用和字段都能从超类中调用到。
    3. 如果待提升函数的签名不同,使用改变函数声明将那些签名都修改为你想要在超类中使用的签名。
    4. 在超类中新建一个函数,将某一个待提升函数的代码复制到其中。
    5. 执行静态检查。
    6. 依橱依阁待提升的子类函数。
    7. 测试。
    8. 逐一移除待提升的子类函数,直到只剩下的超类中的函数为止。

字段上移(Pull Up Field)

  • 示例

    class Employee {...}
    
    class Salesman extends Employee {
        private Stirng name;
    }
    
    class Engineer extends Employee {
        private Stirng name;
    }

    重构为

    class Employee {
        private Stirng name;
    }
    
    class Salesman extends Employee {...}
    class Engineer extends Employee {...}
  • 动机

    如果各子类是分别开发的,或者是在重构过程中组合起来的,你常会发现它们拥有重复特性,特别是字段更容易重复。这样的字段有时拥有近似的名字,但也并非绝对如此。判断若干字段是否重复,唯一的方法就是观察函数如何使用它们。如果它们被使用的方式很相似,我就可以将它们提升到超类中去。

  • 做法

    1. 针对待提升之字段,检查它们的所有使用点,确认它们以同样的方式被使用。
    2. 如果这些字段的名称不同,先使用变量改名为它们取个相同的名字。
    3. 在超类中新建一个字段。
    4. 移除子类中的字段。
    5. 测试。

构造函数本体上移(Pull Up Constructor Body)

  • 示例

    class Party {...}
    
    class Employee extends Party {
        constructor(name, id, monthlyCost) {
            super();
            this._id = id;
            this._name = name;
            this._monthlyCost = monthlyCost;
        }
    }

    重构为

    class Party{
        constructor(name){
            this._name = name;
        }
    }
    
    class Employee extends Party{
        constructor(name, id, monthlyCost){
            super(name);
            this._id = id;
            this._monthlyCost = monthlyCost;
        }
    }
  • 动机

    如果我看见各个子类中的函数有共同行为,我的第一个念头就是使用提炼函数将它们提炼到一个独立函数中,然后使用函数上移将这个函数提升至超类。但是构造函数的出现打乱了我的算盘,因为它们附加了特殊的规则,对一些做法与函数的调用次序有所限制。要对付它们,我需要略微不同的做法。

  • 做法

    1. 如果超类还不存在构造函数,首先为其定义一个。确保让子类调用超类的构造函数。
    2. 使用移动语句将子类中构造函数中的公共语句移动到超类的构造函数调用语句之后。
    3. 逐一移除子类间的公共代码,将其提升至超类构造函数中。对于公共代码中引用到的变量,将其作为参数传递给超类的构造函数。
    4. 测试。
    5. 如果存在无法简单提升至超类的公共代码,先应用提炼函数,再利用函数上移提升之。

函数下移(Push Down Method)

  • 示例

    class Employee {
        get quota {...}
    }
    
    class Salesman extends Employee {...}
    class Engineer extends Employee {...}

    重构为

    class Employee {...}
    class Salesman extends Employee {...}
    class Engineer extends Employee {
        get quota {...}
    }
  • 动机

    如果超类中的某个函数只与一个(或少数几个)子类有关,那么最好将其从超类中挪走,放到真正关心它的子类中去。这项重构手法只有在超类明确知道哪些子类需要这个函数时适用。

  • 做法

    1. 将超类中的函数本体复制到每一个需要此函数的子类中。
    2. 删除超类中的函数。
    3. 测试。
    4. 将该函数从所有不需要它的那些子类中删除。
    5. 测试。

字段下移(Push Down Field)

  • 示例

    class Employee {
        private String quota;
    }
    
    class Salesman extends Employee {...}
    class Engineer extends Employee {...}

    重构为

    class Employee {...}
    class Salesman extends Employee {...}
    class Engineer extends Employee {
        protected String quota;
    }
  • 动机

    如果某个字段只被一个子类(或者一小部分子类)用到,就将其搬移到需要该字段的子类中。

  • 做法

    1. 在所有需要该字段的子类中声明该字段。
    2. 将该字段从超类中移除。
    3. 测试。
    4. 将该字段从所有不需要它的那些子类中删除。
    5. 测试。

以子类取代类型码(Replace Type Code With Subclasses)

  • 示例

    function createEmployee(name, type){
        return new Employee(name, type);
    }

    重构为

    function createEmploee(name, type){
        switch(type){
            case "enginner": return new Enginner(name);
            case "salaesman": return new Salesman(name);
            case "manager": return new Manager(name);
        }
    }
  • 动机

    继承有两个诱人之处。首先,你可以用多态来处理条件逻辑。如果有几个函数在根据类型码的取值采取不同的行为,多态就显得特别有用。引入子类之后,我可以用以多态取代条件表达水来处理这些函数。另外,有些字段或函数只对特定的类型码取值才有意义,例如“销售目标”只对“销售”这类员工才有意义。此时我可以创建子类,然后用字段下移把这样的字段放到合适的子类中去。

  • 做法

    1. 自封装类型码字段。
    2. 任选一个类型码取值,为其创建一个子类。覆写类型码类的取值函数,令其返回该类型码的字面量值。
    3. 创建一个选择器逻辑,把类型码参数映射到新的子类。
    4. 测试。
    5. 针对每一个类型码取值,重复上述“创建子类、添加选择器逻辑”的过程。每次修改后执行测试。
    6. 去掉类型码字段。
    7. 测试。
    8. 使用函数下移和以多态取代条件表达式处理原本访问了类型码的函数。全部处理完后,就可以移除类型码的访问函数。

移除子类(Remove Subclass)

  • 示例

    class Person {
        get genderCode() {return "X";}
    }
    class Male extends Person {
        get genderCode() {return "M";}
    }
    class Female extends Person {
        get genderCode() {return "F";}
    }

    重构为

    class Person {
        get genderCode() {return this._genderCode;}
    }
  • 动机

    如果子类的用处太少,就不值得存在了。此时,最好的选择就是移除子类,将其替换为超类中的一个字段。

  • 做法

    1. 使用以工厂函数取代构造函数,把子类的构造函数包装到超类的工厂函数中。
    2. 如果有任何代码检查子类的类型,先用提炼函数把类型检查逻辑包装起来,然后用搬移函数将其搬移到超类。每次修改后执行测试。
    3. 新建一个字段,用于代表子类的类型。
    4. 将原本针对子类的类型做判断的函数改为使用新建的类型字段。
    5. 删除子类。
    6. 测试。

提炼超类(Extract Superclass)

  • 示例

    class Department {
        get totalAnnualCost() {...}
        get name() {...}
        get headCount() {...}
    }
    
    class Employee {
        get annualCost() {...}
        get name() {...}
        get id() {...}
    }

    重构为

    class Party {
        get name() {...}
        get annualCost() {...}
    }
    
    class Department extends Party {
        get annualCost() {...}
        get headCount() {...}
    }
    
    class Employee extends Party {
        get annualCost() {...}
        get id() {...}
    }
  • 动机

    合理的继承关系就是在程序演化的过程中才浮现出来的:我发现了一些共同元素,希望把它们抽取到移除,于是就有了继承关系。

  • 做法

    1. 为原本的类新建一个空白的超类。
    2. 测试。
    3. 使用构造函数本体上移、函数上移和字段上移手法,逐一将子类的共同元素上移到超类。
    4. 检查留在子类中的函数,看它们是否还有共同的成分。如果有,可以先用提炼函数将其提炼出来,再用函数上移搬到超类。
    5. 检查所有使用原本的类的客户端代码,烤炉将其调整为使用超类的接口。

折叠继承体系(Collapse Hierarchy)

  • 示例

    class Employee {...}
    class Salesman extends Employee {...}

    重构为

    class Employee {...}
  • 动机

    在重构类继承体系时,我经常把函数和字段上下移动。随着继承体系额的演化,我有时会发现一个类与其超类已经没有多大差别,不值得再作为独立的类存在。此时我就会把超类和子类结合起来。

  • 做法

    1. 选择想移除的类:是超类还是子类?
    2. 使用字段上移、字段下移、函数上移和函数下移,把所有元素都移到同一个类中。
    3. 调整即将被移除的那个类的所有引用点,令它们改而引用合并后留下的类。
    4. 移除我们的目标:此时它应该已经成为一个空类。
    5. 测试。

以委托取代子类(Replace Subclass with Delegate)

  • 示例

    class Order {
        get daysToShip() {
            return this._warehouse.daysToShip;
        }
    }
    
    class PriorityOrder extends Order {
        get daysToShip() {
            return this._priorityPlan.daysToShip;
        }
    }

    重构为

    class Order {
        get daysToShip() {
            return (this._priorityDelegate) ? this._priorityDelegate.daysToShip : this._warehouse.daysToShip;
        }
    }
    
    class PriorityOrderDelegate {
        get daysToShip() {
            return this._priorityPlan.daysToShip;
        }
    }
  • 动机

    与继承关系相比,使用委托关系时接口更清晰、耦合更少。因此,继承关系遇到问题时运用以委托取代子类是常见的情况。

  • 做法

    1. 如果构造函数有多个调用者,首先用以工厂函数取代构造函数把构造函数包装起来。
    2. 创建一个空的委托类,这个类的构造函数应该接受所有子类特有的数据项,并且经常以参数的形式接受一个指回超类的引用。
    3. 在超类中添加一个字段,用于安放委托对象。
    4. 修改子类的创建逻辑,使其初始化上述委托字段,放入一个委托对象的实例。
    5. 选择一个子类中的函数,将其移入委托类。
    6. 使用搬移函数手法搬移上述函数,不要删除源类中的委托代码。
    7. 如果被搬移的源函数还在子类之外被调用了,就把留在源类中的委托代码从子类移到超类,并在委托代码之前加上卫语句,检查委托对象存在。如果子类之外已经没有其他调用者,就用移除死代码去掉已经没人使用的委托代码。
    8. 测试。
    9. 重复上述过程,直到子类中所有函数都搬到委托类。
    10. 找到所有调用子类构造函数的地方,逐一将其改为使用超类的构造函数。
    11. 测试。
    12. 运用移除死代码去掉子类。

以委托取代超类(Replace Superclass with Delegate)

  • 示例

    class List {...}
    class Stack extends List {...}

    重构为

    class Stack {
        constructor() {
            this._storage = new List();
        }
    }
    class List {...}
  • 动机

    使用委托关系能更清晰地表达“这是另一个东西,我只是需要用到其中携带的一些功能”。

  • 做法

    1. 在子类新建一个字段,使其引用超类的一个对象,并将这个委托引用初始化为超类的新实例。
    2. 针对超类的1每个函数,在子类中创建一个转发函数,将调用请求转发给委托引用。每转发一块完整逻辑,都要执行测试。
    3. 当所有超类函数都被转发函数覆写后,就可以去掉继承关系。

文章作者: caty
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 caty !
  目录