首页/开发/代码质量与评审速查/坏味道:被拒绝的馈赠(Refused Bequest)
重构手法62 条

坏味道:被拒绝的馈赠(Refused Bequest)

子类只用到父类很少的能力(或抛异常拒绝继承的方法):优先用组合替换继承(Replace Superclass with Delegate),或把不适用的部分下推(Push Down Method / Field)。

什么是被拒绝的馈赠

被拒绝的馈赠(Refused Bequest)指子类只用到父类很少的能力,或者对继承来的方法直接抛出不支持异常。它说明继承关系里的类型语义并不成立,子类只是看中了父类的实现。典型例子是用栈继承向量,结果栈暴露了按下标随机访问这类不属于栈语义的接口,调用方一旦误用,类型检查无法拦住。

识别与处理

识别信号子类覆写后直接抛不支持异常,或大部分继承来的能力闲置
主要手法Replace Superclass with Delegate,改为持有父类实例并定向转发
辅助手法Push Down Method 与 Push Down Field,把不适用的成员下推到需要的子类
方向组合优先于继承,只在类型语义确实成立时保留继承

典型使用场景

  • 栈继承向量这类只为复用实现的继承:改为内部持有一个双端队列,只暴露栈的操作
  • 子类拒绝实现父类方法:拆分接口,让子类只实现自己支持的能力
  • 父类方法对某个子类无意义:把该方法下推到真正需要它的子类
  • 超类同时承担两类职责:先抽出子类特有行为,再决定是否解除继承关系
  • 框架基类携带大量可选钩子:用接口加组合替代表层继承

从继承改为组合

从继承改为组合
// 改造前:Stack 继承了 Vector,随机访问等无关接口一并暴露
class Stack extends Vector { }

// 改造后:内部持有容器,只暴露栈语义需要的操作
class Stack {
    private final Deque<Object> elements = new ArrayDeque<>();
    void push(Object o) { elements.push(o); }
    Object pop() { return elements.pop(); }
}

处理方式对照

情形推荐处理注意点
只为复用实现而继承改为持有实例并定向转发只转发需要的能力,避免白名单失效
子类拒绝部分方法抽出更小的接口,让角色承担调用方需要按角色而非按具体类型依赖
父类方法只对部分子类有意义把方法下推到子类下推后父类调用点需要重新审视
继承关系语义成立但接口过宽收窄可见性,逐步替换调用方公共接口收窄属于破坏性变更,需评估外部使用者
改组合时不要一次性照搬父类的全部公开方法:只暴露实际需要的部分。照搬转发会让新类型的接口和父类一样宽,等于没有解决暴露面过大的问题。
替换继承为组合时留意父类的隐含契约:相等性判断、哈希值、字符串表示、克隆语义。子类原本继承到的这些行为在组合后会改变,若调用方曾经依赖,需要显式补上。

落地检查清单

  • 子类是否对所有继承来的方法都给出合理实现,而不是抛异常
  • 继承带来的接口是否全部符合子类的类型语义
  • 解除继承后,暴露的方法是否只包含真正需要的能力
  • 原本依赖父类契约的调用方是否有替代方案

易错点

  • 只为复用代码而继承:类型语义不成立,多态调用处出现意料之外的行为,问题在运行时才暴露
  • 继承改组合时全量转发父类方法:新类型接口依旧过宽,暴露面问题原样保留
  • 忽略父类隐含契约:相等性、哈希与克隆语义改变后,集合与缓存中出现重复或找不到的元素
  • 机械地把所有继承都改成组合:类型学上确实成立的继承关系被拆散,反而增加一层无意义的转发代码

数据来源与参考

  • 《重构》第 2 版 · 第 3 章(Refused Bequest)
​‌‌​​‌​​​‌‌​‌‌‌‌​‌‌​​​‌‌​‌‌‌​​‌‌​​‌​‌‌‌​​‌‌‌​​​‌​‌‌​‌​​‌​‌‌‌​​​‌​‌‌​‌​​‌​‌‌​​​‌‌​‌‌‌​‌​​​‌‌​‌‌‌‌​​‌​‌‌‌​​‌‌​​​‌‌​‌‌​‌‌‌‌​‌‌​‌‌​‌