19、处理继承(下):字段下移、以子类取代类型码、以策略类取代类型码
继承这东西,用好了是神器,用砸了就是灾难。上一章我们聊了字段上移、函数上移这些往上走的操作。这一章反过来,聊聊怎么把东西往下放,以及什么时候该彻底放弃继承,改用组合。
我个人习惯是:先看代码里有没有“假继承”。什么叫假继承?就是子类之间除了一个类型码不同,其他逻辑几乎一样。这种代码,说白了就是披着继承外衣的 switch-case。遇到这种情况,我建议你果断动手重构。
字段下移:把不该共享的字段赶回子类
先看一个我踩过的坑。几年前我接手一个支付系统,基类 Payment 里有个字段 bankAccount,所有子类都继承它。但后来发现,只有 BankTransferPayment 才需要这个字段,CreditCardPayment 和 WeChatPayment 根本用不上。
这就是典型的“字段放错位置”。基类里躺着只有部分子类才用的字段,不仅浪费内存,还让代码理解成本飙升。你想想看,新同事看到基类里有个 bankAccount,第一反应肯定是“所有支付方式都有银行账户?”——这显然不对。
怎么改?把字段下移到真正需要它的子类里。
// 重构前:基类里躺着不该有的字段
class Payment {
protected String bankAccount; // 只有银行转账才需要
protected BigDecimal amount;
}
class BankTransferPayment extends Payment {
// 用到了 bankAccount
}
class CreditCardPayment extends Payment {
// bankAccount 根本没用!
}
// 重构后:字段下移
class Payment {
protected BigDecimal amount;
}
class BankTransferPayment extends Payment {
private String bankAccount; // 下移到这里
}
class CreditCardPayment extends Payment {
// 干净了
}
以子类取代类型码:告别 switch-case 的第一步
类型码这东西,我见过太多项目被它拖垮。一个 int type 或者 String type 字段,然后满屏的 if (type == 1) 或者 switch(type)。每次新增一种类型,就要改好几个地方,漏改一个就出 bug。
我曾经在一个电商项目中看到这样的代码:订单状态用整数表示,0 代表待支付,1 代表已支付,2 代表已发货……然后业务逻辑里到处都是 if (order.status == 1)。后来需求变了,要新增一个“部分退款”状态,结果改了十几个文件,还漏了两个,线上出了 P0 事故。
重构手法很简单:把类型码变成子类。每个子类负责自己的行为。
// 重构前:类型码 + switch
class Employee {
private int type; // 0: 工程师, 1: 经理, 2: 销售
double calculateBonus() {
switch (type) {
case 0: return salary * 0.1;
case 1: return salary * 0.2;
case 2: return salary * 0.15;
default: throw new IllegalArgumentException();
}
}
}
// 重构后:以子类取代类型码
abstract class Employee {
abstract double calculateBonus();
}
class Engineer extends Employee {
double calculateBonus() {
return salary * 0.1;
}
}
class Manager extends Employee {
double calculateBonus() {
return salary * 0.2;
}
}
class Salesperson extends Employee {
double calculateBonus() {
return salary * 0.15;
}
}
以策略类取代类型码:当继承不合适的时候
等等,是不是所有类型码都要用子类取代?当然不是。我遇到过一种情况:某个对象的类型会频繁变化。比如订单状态,从“待支付”变成“已支付”,再变成“已发货”。如果用子类,难道每次状态变化都要把对象替换成另一个子类的实例?太麻烦了。
这时候,策略模式就派上用场了。把变化的行为抽到独立的策略类里,通过组合来解决问题。
// 策略接口
interface ShippingStrategy {
double calculateCost(double weight);
}
// 具体策略
class StandardShipping implements ShippingStrategy {
public double calculateCost(double weight) {
return weight * 5;
}
}
class ExpressShipping implements ShippingStrategy {
public double calculateCost(double weight) {
return weight * 10 + 20;
}
}
// 上下文类
class Order {
private ShippingStrategy strategy; // 组合策略
void setShippingStrategy(ShippingStrategy strategy) {
this.strategy = strategy;
}
double calculateShippingCost(double weight) {
return strategy.calculateCost(weight);
}
}
知识体系总览
下面这张图把三种重构手法的适用场景和关系梳理清楚了。我建议你把它存下来,每次重构前看一眼。
避坑指南
最后分享几个我踩过的坑,希望能帮你少走弯路。
- 字段下移别过头:我曾经把基类里一个很常用的字段下移了,结果导致所有子类都得重复声明。下移前先看看这个字段在多少子类里被用到,超过 70% 就别动了。
- 子类数量别太多:如果类型码有几十种,全变成子类反而更糟。这时候可以考虑用策略模式,或者用枚举 + 函数式接口来替代。
- 别忘了工厂方法:用子类取代类型码后,创建对象的地方需要改。我习惯在基类里加一个静态工厂方法,根据参数返回对应的子类实例。
嗯,这一章的内容就到这里。记住:继承不是银弹,组合也不是。关键是看你的业务场景,选最合适的方案。代码整洁之道,说白了就是不断做减法——去掉不必要的继承,去掉冗余的字段,去掉恶心的 switch-case。