49. 代码坏味道:平行继承体系 — 实战消除重复与搬移函数
平行继承体系,这名字听着挺唬人。说白了就是:你为了给一个类加功能,得同时修改两个继承体系里的类。我早年接手过一个支付系统,就掉进过这个坑里。
当时系统里有 PaymentMethod 和 PaymentValidator 两套继承树。每新增一种支付方式,比如“微信支付”,就得在 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() { /* 支付宝验证逻辑 */ }
}
看到没?CreditCardPayment 和 CreditCardValidator 是成对出现的。AlipayPayment 和 AlipayValidator 也是。这就是典型的平行继承体系。
为什么这是个坏味道
我个人习惯,看到这种成对出现的继承树,就会警觉。原因有三:
- 重复的修改成本:每加一个新支付方式,你得改两个地方。改漏一个就出 bug。
- 逻辑分散:一个支付方式的完整逻辑被拆到了两个类里。读代码的人得来回跳转。
- 难以扩展:新来的同事想加个“银联支付”,得先搞清楚两个继承树的关系。我见过有人只改了
PaymentMethod,忘了改PaymentValidator,结果线上验证逻辑全乱了。
如何消除平行继承体系
消除的核心思路就一个:搬移函数,消除重复。把两个继承树里对应的逻辑,合并到一个类里。
具体怎么做?我分三步走:
第一步:识别对应关系
先找出两个继承树里,哪些子类是一一对应的。比如:
| PaymentMethod 子类 | PaymentValidator 子类 |
|---|---|
| CreditCardPayment | CreditCardValidator |
| AlipayPayment | AlipayValidator |
| WechatPayment | WechatValidator |
嗯,这里要注意:不是所有平行继承体系都这么明显。有时候两个树的名字不一样,但逻辑上就是绑定的。我曾经在一个项目中,发现 OrderProcessor 和 OrderRule 两个继承树是平行的,但类名完全看不出关联。最后是靠分析代码调用链才发现的。
第二步:搬移函数
把 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%。
重构后的类图大概是这样:
重构之后,新来的同事想加“银联支付”,只需要创建一个 UnionPayPayment 类,实现 processPayment() 和 validate() 两个方法就行。不用再管另一个继承树了。
总结
平行继承体系这个坏味道,说白了就是两个继承树“长歪了”。消除它的核心手段就是搬移函数,把分散的逻辑合并到一起。记住三个要点:
- 识别对应关系:找出两个树里成对出现的子类
- 搬移函数:把验证、计算等逻辑搬移到主继承树里
- 消除重复实例化:调用方只创建一个对象,而不是两个
我曾经在一个项目里,因为没及时消除平行继承体系,导致每次需求变更都要改四五个类。后来痛下决心重构,花了两天时间合并,之后半年都没再为这事头疼过。嗯,有些债,早还早轻松。
公众号:蓝海资料掘金营,微信deep3321