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