14. 继承体系重构(下):提炼超类、折叠继承体系、以委托取代继承、以继承取代委托、替换算法
好,咱们接着聊继承体系重构。上一章我们讲了那些“拆”的操作,这一章正好反过来——讲讲怎么“合”、怎么“换”。说白了,继承体系不是越细越好,也不是越粗越好,关键是恰到好处。
我个人习惯把这一章的五种手法分成三类:向上抽象(提炼超类)、向下合并(折叠继承体系)、关系转换(继承↔委托互换 + 替换算法)。咱们一个一个来。
提炼超类(Extract Superclass)
这个手法其实很直观。你发现两个类有相似的字段和方法,那就把它们共性的部分抽出来,放到一个新的父类里。嗯,说白了就是“找共同点”。
我在项目中遇到过这样一个场景:Order 和 Invoice 两个类,都有客户信息、金额计算、状态管理。一开始各自为政,后来需求一多,改一个地方就得改两遍。我当时就果断提炼了一个 CustomerDocument 超类。
什么时候该用?
- 两个类有相同的字段(比如 customerName、totalAmount)
- 两个类有相似的方法逻辑(比如 calculateTotal())
- 你发现自己在复制粘贴代码
看个代码示例你就明白了。重构前:
// 重构前:两个类各自为政
public class Order {
private String customerName;
private List<Item> items;
public double calculateTotal() {
return items.stream()
.mapToDouble(Item::getPrice)
.sum();
}
}
public class Invoice {
private String customerName;
private List<LineItem> lineItems;
public double calculateTotal() {
return lineItems.stream()
.mapToDouble(LineItem::getAmount)
.sum();
}
}
重构后:
// 重构后:提炼超类
public abstract class CustomerDocument {
protected String customerName;
public abstract double calculateTotal();
}
public class Order extends CustomerDocument {
private List<Item> items;
@Override
public double calculateTotal() {
return items.stream()
.mapToDouble(Item::getPrice)
.sum();
}
}
public class Invoice extends CustomerDocument {
private List<LineItem> lineItems;
@Override
public double calculateTotal() {
return lineItems.stream()
.mapToDouble(LineItem::getAmount)
.sum();
}
}
我的小技巧:提炼超类时,别急着把所有公共方法都塞进去。先抽字段,再抽方法。如果两个方法只是“长得像”但逻辑不同,那就用模板方法模式,把公共流程放在超类,具体步骤留给子类实现。
折叠继承体系(Collapse Hierarchy)
这个手法正好和提炼超类相反。你想想看,如果子类啥也没干,或者干的事特别少,那这个子类还有存在的必要吗?
我曾经接手过一个项目,继承树有六层深。最底层的子类就加了一个字段,其他全是继承来的。我当时就纳闷了——这哥们儿图啥呢?
注意:折叠继承体系不是让你把所有类都合并成一个。它的适用场景是:
- 子类没有新增任何行为
- 子类只是继承了父类,没有覆盖任何方法
- 子类和父类的差异已经消失(比如需求变更后)
举个简单的例子:
// 重构前:这个子类啥也没干
public class BaseReport {
public void generate() {
// 生成报告的逻辑
}
}
public class SimpleReport extends BaseReport {
// 没有任何新增字段或方法
// 也没有覆盖任何方法
}
// 重构后:直接删除 SimpleReport,所有地方改用 BaseReport
你可能会问:“那如果以后又要加差异化怎么办?” 我的回答是:等需要的时候再加。别为了“未来可能”而保留一个空壳子类。YAGNI(You Ain't Gonna Need It)原则在这里很适用。
以委托取代继承(Replace Inheritance with Delegation)
这个手法可以说是继承体系重构里最常用的之一。为什么?因为继承是强耦合,委托是弱耦合。你想想看,子类一旦继承了父类,就绑死了父类的所有行为。如果父类变了,子类很可能跟着遭殃。
我记得有一次,一个团队用继承实现了一个 Stack 类继承自 Vector。结果 Vector 的所有方法都暴露给了 Stack 的使用者,包括那些不应该被调用的方法(比如 insertElementAt)。这就是典型的“继承滥用”。
什么时候该用委托取代继承?
- 子类只使用了父类的一部分方法
- 父类的接口污染了子类的公共接口
- 你想让子类有更灵活的扩展能力
看代码:
// 重构前:继承导致接口污染
public class MyStack<E> extends Vector<E> {
public void push(E e) {
addElement(e);
}
public E pop() {
E e = lastElement();
removeElementAt(size() - 1);
return e;
}
}
// 使用者可以调用 insertElementAt、removeAllElements 等不该有的方法
// 重构后:委托模式
public class MyStack<E> {
private Vector<E> vector = new Vector<>();
public void push(E e) {
vector.addElement(e);
}
public E pop() {
E e = vector.lastElement();
vector.removeElementAt(vector.size() - 1);
return e;
}
public boolean isEmpty() {
return vector.isEmpty();
}
}
我的经验:委托模式虽然好,但也不是银弹。如果子类和父类有大量的方法需要委托,那写起来也挺烦的。这时候你可以考虑用 IDE 的“委托生成”功能,或者用 Lombok 的 @Delegate 注解(Java 项目)。
以继承取代委托(Replace Delegation with Inheritance)
这个手法是上一个的反向操作。什么时候会用?说白了,就是当你发现委托太麻烦,而继承反而更简单的时候。
举个例子:你有一个 Person 类,里面委托了一个 Employee 对象。但后来发现,所有 Person 的方法都直接转发给了 Employee,而且 Person 本身没有任何额外行为。这时候,不如直接让 Person 继承 Employee。
注意:这个手法要慎用。只有在以下条件都满足时才考虑:
- 委托类的方法几乎全部被使用
- 没有接口污染的问题
- 你确定未来不会需要其他父类
替换算法(Replace Algorithm)
这个手法其实和继承关系不大,但它经常出现在继承体系重构的上下文中。为什么?因为当你重构继承体系时,经常会发现某些子类覆盖了父类的方法,但实现方式很糟糕。
替换算法的核心思想很简单:用一个更清晰的算法替换掉那个又臭又长的算法。
我曾经在一个支付系统中看到这样的代码:一个 calculateFee 方法,里面嵌套了七八层 if-else,每个分支还调用了不同的子类方法。我当时就把它替换成了一个策略模式 + 简单的计算公式。代码量从 200 行降到了 30 行。
// 重构前:又臭又长的算法
public double calculateFee(Order order) {
if (order.getType() == OrderType.NORMAL) {
if (order.getAmount() < 100) {
return order.getAmount() * 0.05;
} else if (order.getAmount() < 500) {
return order.getAmount() * 0.03;
} else {
return order.getAmount() * 0.01;
}
} else if (order.getType() == OrderType.VIP) {
// 又是一大堆条件
}
// 还有更多...
}
// 重构后:清晰的算法
public double calculateFee(Order order) {
FeeStrategy strategy = feeStrategyFactory.getStrategy(order.getType());
return strategy.calculate(order.getAmount());
}
替换算法的检查清单:
- 先把原算法的输入输出搞清楚
- 写单元测试,覆盖所有边界情况
- 用更简单的数据结构或设计模式替换
- 跑测试,确保结果一致
总结一下
这五种手法,其实都是在回答同一个问题:这个继承关系到底合不合理?
- 提炼超类:发现共性,向上抽象
- 折叠继承体系:发现冗余,向下合并
- 以委托取代继承:发现耦合过紧,解耦
- 以继承取代委托:发现委托太繁琐,简化
- 替换算法:发现实现太烂,重写
嗯,说到底,重构没有标准答案。每个项目、每个团队、每个场景都不一样。我给你的建议是:多问自己一句“这个继承关系真的有必要吗?” 很多时候,答案会自己浮现出来。