49. 代码坏味道:平行继承体系 — 实战消除重复与搬移函数

平行继承体系,这名字听着挺唬人。说白了就是:你为了给一个类加功能,得同时修改两个继承体系里的类。我早年接手过一个支付系统,就掉进过这个坑里。

当时系统里有 PaymentMethodPaymentValidator 两套继承树。每新增一种支付方式,比如“微信支付”,就得在 PaymentMethod 下加个子类,同时在 PaymentValidator 下也加个子类。两个体系平行发展,改一个就得改另一个。你想想看,这维护成本是不是翻倍了?

什么是平行继承体系

平行继承体系,指的是两个独立的类继承树,它们之间存在一一对应的关系。每当你往一个继承树里加一个新子类,另一个继承树里也必须加一个对应的子类。

举个例子:

// 支付方式继承体系
abstract class PaymentMethod {
    abstract void processPayment();
}

class CreditCardPayment extends PaymentMethod {
    void processPayment() { /* 信用卡支付逻辑 */ }
}

class AlipayPayment extends PaymentMethod {
    void processPayment() { /* 支付宝支付逻辑 */ }
}

// 支付验证继承体系(平行存在)
abstract class PaymentValidator {
    abstract boolean validate();
}

class CreditCardValidator extends PaymentValidator {
    boolean validate() { /* 信用卡验证逻辑 */ }
}

class AlipayValidator extends PaymentValidator {
    boolean validate() { /* 支付宝验证逻辑 */ }
}

看到没?CreditCardPaymentCreditCardValidator 是成对出现的。AlipayPaymentAlipayValidator 也是。这就是典型的平行继承体系。

为什么这是个坏味道

我个人习惯,看到这种成对出现的继承树,就会警觉。原因有三:

  • 重复的修改成本:每加一个新支付方式,你得改两个地方。改漏一个就出 bug。
  • 逻辑分散:一个支付方式的完整逻辑被拆到了两个类里。读代码的人得来回跳转。
  • 难以扩展:新来的同事想加个“银联支付”,得先搞清楚两个继承树的关系。我见过有人只改了 PaymentMethod,忘了改 PaymentValidator,结果线上验证逻辑全乱了。
⚠️ 注意:平行继承体系往往不是一开始就有的。它通常是随着需求迭代,慢慢“长”出来的。一开始可能只有一个继承树,后来为了“分离关注点”,又建了第二个。结果两个树就绑死了。

如何消除平行继承体系

消除的核心思路就一个:搬移函数,消除重复。把两个继承树里对应的逻辑,合并到一个类里。

具体怎么做?我分三步走:

第一步:识别对应关系

先找出两个继承树里,哪些子类是一一对应的。比如:

PaymentMethod 子类 PaymentValidator 子类
CreditCardPayment CreditCardValidator
AlipayPayment AlipayValidator
WechatPayment WechatValidator

嗯,这里要注意:不是所有平行继承体系都这么明显。有时候两个树的名字不一样,但逻辑上就是绑定的。我曾经在一个项目中,发现 OrderProcessorOrderRule 两个继承树是平行的,但类名完全看不出关联。最后是靠分析代码调用链才发现的。

第二步:搬移函数

PaymentValidator 里的 validate() 方法,搬移到 PaymentMethod 里。或者反过来,把 processPayment() 搬移到 PaymentValidator 里。具体往哪搬,看哪个类更“核心”。

我个人习惯,把验证逻辑搬移到支付方式类里。因为验证是支付流程的一部分,放在一起更内聚。

// 重构后:合并到一个继承体系
abstract class PaymentMethod {
    abstract void processPayment();
    abstract boolean validate();  // 从 Validator 搬移过来
}

class CreditCardPayment extends PaymentMethod {
    void processPayment() { /* 信用卡支付逻辑 */ }
    boolean validate() { /* 信用卡验证逻辑,从 CreditCardValidator 搬来 */ }
}

class AlipayPayment extends PaymentMethod {
    void processPayment() { /* 支付宝支付逻辑 */ }
    boolean validate() { /* 支付宝验证逻辑,从 AlipayValidator 搬来 */ }
}

搬移之后,原来的 PaymentValidator 继承体系就可以删掉了。每个支付方式类自己负责验证,不再需要平行维护。

💡 小技巧:搬移函数时,不要一次性搬太多。我建议一次只搬一个方法,搬完就跑一遍测试。确保没破坏现有逻辑,再搬下一个。这叫“小步重构”。

第三步:消除重复的实例化逻辑

搬移完函数后,原来创建 PaymentValidator 子类的地方,也得改。比如原来有段代码:

// 重构前:需要同时创建两个对象
PaymentMethod method = new CreditCardPayment();
PaymentValidator validator = new CreditCardValidator();
validator.validate();
method.processPayment();

重构后,只需要创建 PaymentMethod 一个对象:

// 重构后:一个对象搞定
PaymentMethod method = new CreditCardPayment();
method.validate();   // 验证逻辑在 PaymentMethod 内部
method.processPayment();

你看,调用方也简化了。原来要管两个对象,现在只管一个。这就是消除平行继承体系带来的好处。

什么时候不适合合并

不是所有平行继承体系都要合并。我遇到过一种情况:两个继承树虽然平行,但它们的职责确实应该分离。比如一个负责“数据存储”,一个负责“数据展示”。这时候强行合并,反而会让类变得臃肿。

那怎么办?可以用 策略模式桥接模式 来解耦。让两个继承树通过组合而非继承来协作。这样既保持了职责分离,又避免了平行修改。

📌 核心原则:平行继承体系消除的关键,是看两个树之间的对应关系是否“强耦合”。如果每加一个子类,两个树都得改,那就合并。如果只是偶尔对应,可以考虑用组合替代继承。

实战案例:支付系统的重构

我记得之前重构那个支付系统时,花了大概两天时间。第一天分析对应关系,第二天搬移函数和测试。最后删掉了 PaymentValidator 整个继承树,代码量减少了 30%。

重构后的类图大概是这样:

重构后:单一继承体系 PaymentMethod + processPayment() + validate() CreditCardPayment processPayment() { ... } validate() { ... } AlipayPayment processPayment() { ... } validate() { ... } WechatPayment processPayment() { ... } validate() { ... } 每个子类同时包含支付逻辑和验证逻辑 不再需要平行的 Validator 继承体系

重构之后,新来的同事想加“银联支付”,只需要创建一个 UnionPayPayment 类,实现 processPayment()validate() 两个方法就行。不用再管另一个继承树了。

总结

平行继承体系这个坏味道,说白了就是两个继承树“长歪了”。消除它的核心手段就是搬移函数,把分散的逻辑合并到一起。记住三个要点:

  • 识别对应关系:找出两个树里成对出现的子类
  • 搬移函数:把验证、计算等逻辑搬移到主继承树里
  • 消除重复实例化:调用方只创建一个对象,而不是两个

我曾经在一个项目里,因为没及时消除平行继承体系,导致每次需求变更都要改四五个类。后来痛下决心重构,花了两天时间合并,之后半年都没再为这事头疼过。嗯,有些债,早还早轻松。


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