13、继承体系重构(上):函数上移、字段上移、构造函数本体上移、函数下移、字段下移、提炼子类

继承体系,说白了就是代码世界里最经典的“父子关系”。

我刚入行那会儿,特别喜欢用继承。看到两个类有相似逻辑,二话不说就拉出一个父类。结果呢?半年后回来一看,继承树长得跟榕树似的,根深叶茂,但想动一根枝桠都怕整棵树倒了。

嗯,这就是技术债务的典型表现。今天我们就来聊聊,怎么用一套系统的手法,把这棵“榕树”修剪得清爽、健康。

核心思想:继承体系重构的核心,就是让每个类只做它该做的事。不该它管的,往上提;该它管的,往下放。

13.1 函数上移(Pull Up Method)

先问个问题:两个子类里有完全相同的函数,你会怎么做?

我见过不少项目,直接复制粘贴。结果呢?改一个地方,另一个忘了改,bug就来了。

正确的做法是:把这个函数往上提到父类里。

// 重构前
class Employee {
  // ...
}

class Salesman extends Employee {
  get name() {
    return this._name;
  }
}

class Engineer extends Employee {
  get name() {
    return this._name;
  }
}

// 重构后
class Employee {
  get name() {
    return this._name;
  }
}

class Salesman extends Employee {
  // name 方法已上移
}

class Engineer extends Employee {
  // name 方法已上移
}

你看,代码瞬间清爽了。但要注意一点:上移之前,先确认两个函数的行为是否真的完全一致。我曾经踩过一个坑,两个函数看起来一模一样,但一个用了this._name,另一个用了this.getName()。上移之后,子类的行为就变了。

我的习惯:上移之前,先跑一遍所有测试用例。别偷懒,这一步能救你命。

13.2 字段上移(Pull Up Field)

字段上移和函数上移的逻辑类似。如果多个子类都有同一个字段,那就把它提到父类里。

// 重构前
class Salesman extends Employee {
  private String name;
}

class Engineer extends Employee {
  private String name;
}

// 重构后
class Employee {
  protected String name;
}

class Salesman extends Employee {
  // name 字段已上移
}

class Engineer extends Employee {
  // name 字段已上移
}

这里有个小细节:字段的访问权限。上移之后,子类还能不能访问这个字段?我建议用protected,除非你确定子类不需要直接访问它。

注意:字段上移后,如果子类里有对这个字段的初始化逻辑,别忘了检查构造函数。否则,字段可能没被正确赋值。

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

构造函数的重构,比普通函数要复杂一些。因为子类的构造函数里,往往有对父类字段的初始化逻辑。

// 重构前
class Employee {
  protected String name;
  protected String id;
}

class Salesman extends Employee {
  public Salesman(String name, String id, String region) {
    this.name = name;
    this.id = id;
    this.region = region;
  }
}

// 重构后
class Employee {
  public Employee(String name, String id) {
    this.name = name;
    this.id = id;
  }
}

class Salesman extends Employee {
  public Salesman(String name, String id, String region) {
    super(name, id);
    this.region = region;
  }
}

你看,我们把nameid的初始化逻辑上移到了父类的构造函数里。子类只需要调用super()就行了。

我个人习惯,在做这一步之前,先看看子类构造函数里有没有“副作用”。比如,有没有在构造函数里调用其他方法?如果有,上移之后可能会改变调用顺序,导致行为不一致。

13.4 函数下移(Push Down Method)

有上就有下。函数下移,就是把父类里的某个函数,只放到需要它的子类里。

什么时候需要下移?当父类里的函数,只有少数子类用到时。

// 重构前
class Employee {
  get quota() {
    return 100;
  }
}

class Salesman extends Employee {
  // 使用 quota
}

class Engineer extends Employee {
  // 不使用 quota
}

// 重构后
class Employee {
  // quota 已下移
}

class Salesman extends Employee {
  get quota() {
    return 100;
  }
}

class Engineer extends Employee {
  // 没有 quota
}

你想想看,如果Engineer根本不需要quota,却还要继承它,是不是很奇怪?这就是“接口污染”。

核心原则:父类只放所有子类都需要的。如果某个方法只有部分子类需要,那就下移。

13.5 字段下移(Push Down Field)

字段下移和函数下移的逻辑一样。如果父类里的字段,只有部分子类用到,那就把它下移到对应的子类里。

// 重构前
class Employee {
  protected String region;
}

class Salesman extends Employee {
  // 使用 region
}

class Engineer extends Employee {
  // 不使用 region
}

// 重构后
class Employee {
  // region 已下移
}

class Salesman extends Employee {
  protected String region;
}

class Engineer extends Employee {
  // 没有 region
}

这里要注意:字段下移后,父类里对这个字段的引用也要一并处理。否则,编译都过不了。

13.6 提炼子类(Extract Subclass)

提炼子类,和前面的“下移”正好相反。它是从父类里,把一部分行为提炼出来,形成一个独立的子类。

什么时候需要提炼?当父类里的某些方法,只有特定场景下才用到时。

// 重构前
class Employee {
  get quota() {
    return 100;
  }
  get commission() {
    return 0.1 * this.sales;
  }
}

// 重构后
class Employee {
  get quota() {
    return 100;
  }
}

class Salesman extends Employee {
  get commission() {
    return 0.1 * this.sales;
  }
}

你看,我们把commission方法提炼到了Salesman子类里。这样,Employee类就变得更纯粹了。

我的经验:提炼子类之前,先问问自己:“这个子类真的有必要存在吗?”有时候,一个简单的条件判断就能解决问题,没必要搞出一个新类来。

13.7 知识体系总览

为了让你更直观地理解这些重构手法之间的关系,我画了一张图:

继承体系重构手法总览 父类(基类) 子类 A 子类 B 函数/字段上移 函数/字段下移 提炼子类 从父类提炼 上移(Pull Up) 下移(Push Down) 提炼子类(Extract Subclass)

13.8 避坑指南

说了这么多,最后分享几个我踩过的坑:

  • 不要一次性做太多:我曾经在一个大项目里,一口气做了十几个上移和下移。结果呢?测试全挂了。后来我学乖了,一次只做一个重构,跑一遍测试,再继续。
  • 注意多态:如果父类里的方法被子类重写了,上移之前一定要确认:重写后的逻辑是否还适用?
  • 小心循环依赖:字段上移后,如果父类和子类之间出现了循环引用,那就麻烦了。我建议在重构之前,先用工具分析一下依赖关系。
  • 测试,测试,再测试:这句话我说多少遍都不为过。没有测试的重构,就是耍流氓。

我曾经犯过的错:有一次,我把一个字段上移到了父类,结果忘了改子类里的构造函数。那个字段在子类里被初始化了两次,一次在父类,一次在子类。最后查了半天才发现。嗯,从那以后,我每次做完字段上移,都会检查所有子类的构造函数。

好了,这一章的内容就到这里。继承体系重构,说白了就是“该上就上,该下就下”。别怕改代码,怕的是不改。


公众号:蓝海资料掘金营,微信 deep3321