被拒绝的遗赠(Refused Bequest):坏味道识别与重构实战指南
被拒绝的遗赠(Refused Bequest):坏味道识别与重构实战指南
24种代码坏味道系列 · 第23篇
1. 开篇场景
你是否遇到过这样的继承关系:子类继承了父类的方法和字段,但大部分都不需要或需要重写,就像继承了一笔”遗产”,但大部分都用不上,还要想办法处理?
class BadVehicle {
void drive() { /* ... */ }
void fly() { /* ... */ }
// ... 很多方法
};
class BadCar : public BadVehicle {
void fly() override {
std::cout << "Cars cannot fly!" << std::endl; // 拒绝继承的方法
}
// hasWings, canFly 字段对汽车没用,但继承了
};
这就是被拒绝的遗赠的典型症状。子类继承了父类的方法和字段,但大部分都不需要或需要重写,就像继承了一笔”遗产”,但大部分都用不上。
当你需要理解子类时,你必须理解它不需要的父类功能。当你需要修改子类时,你必须在不需要的功能中查找。这种设计使得代码变得复杂,增加了维护的难度。
2. 坏味道定义
被拒绝的遗赠是指子类继承了父类的方法和字段,但大部分都不需要或需要重写。
就像继承了一笔用不上的遗产,不仅没有价值,还要想办法处理。
核心问题:继承应该表示”是一个”关系。如果子类不需要父类的大部分功能,说明继承关系不合理,应该使用组合或接口。
3. 识别特征
🔍 代码表现:
- 特征1:子类重写父类方法只是为了拒绝它
- 特征2:子类继承了不需要的字段
- 特征3:子类只使用父类的部分功能
- 特征4:子类和父类的关系不清晰
- 特征5:删除继承关系后,功能不受影响
🎯 出现场景:
- 场景1:过度使用继承,没有考虑”是一个”关系
- 场景2:从其他代码复制时,保留了继承关系
- 场景3:缺乏设计,没有考虑继承的合理性
- 场景4:重构不彻底,只修改了部分代码
💡 快速自检:
- 问自己:子类是否真的”是一个”父类?
- 问自己:子类是否需要父类的大部分功能?
- 工具提示:使用代码分析工具检测被拒绝的遗赠
4. 危害分析
🚨 维护成本:需要理解不需要的父类功能,时间成本增加40%
⚠️ 缺陷风险:继承关系不合理,bug风险增加30%
🧱 扩展障碍:添加新功能时不知道应该放在哪里
🤯 认知负担:需要理解不需要的继承关系,增加了心理负担
5. 重构实战
步骤1:安全准备
- ✅ 确保有完整的单元测试覆盖
- ✅ 创建重构分支:
git checkout -b refactor/replace-inheritance-with-composition - ✅ 使用版本控制,便于回滚
步骤2:逐步重构
重构前(问题代码)
// 坏味道:父类包含了很多功能
class BadVehicle {
protected:
std::string brand;
int wheels;
double maxSpeed;
bool hasEngine;
int passengerCapacity;
double fuelCapacity;
bool hasWings;
bool canFly;
public:
BadVehicle(const std::string& b) : brand(b), wheels(4), maxSpeed(100.0),
hasEngine(true), passengerCapacity(5),
fuelCapacity(50.0), hasWings(false), canFly(false) {}
virtual void start() {
if (hasEngine) {
std::cout << brand << " engine started." << std::endl;
}
}
virtual void drive() {
std::cout << brand << " is driving at max " << maxSpeed << " km/h." << std::endl;
}
virtual void fly() {
if (canFly && hasWings) {
std::cout << brand << " is flying." << std::endl;
} else {
std::cout << brand << " cannot fly." << std::endl;
}
}
};
// 子类只需要部分功能,但继承了所有
class BadCar : public BadVehicle {
public:
BadCar(const std::string& b) : BadVehicle(b) {
wheels = 4;
maxSpeed = 200.0;
}
// 需要重写 fly 方法,因为汽车不能飞
void fly() override {
std::cout << "Cars cannot fly!" << std::endl; // 拒绝继承的方法
}
// hasWings, canFly 字段对汽车没用,但继承了
};
问题分析:
BadCar继承了BadVehicle,但不需要fly()方法BadCar继承了不需要的字段(如hasWings、canFly)- 继承关系不合理
重构后(清洁版本)
// ✅ 使用接口和组合,而不是深度继承
class Vehicle {
protected:
std::string brand;
public:
Vehicle(const std::string& b) : brand(b) {}
virtual ~Vehicle() = default;
virtual void start() = 0;
virtual void stop() = 0;
std::string getBrand() const { return brand; }
};
class Drivable {
public:
virtual void drive() = 0;
virtual ~Drivable() = default;
};
class Flyable {
public:
virtual void fly() = 0;
virtual ~Flyable() = default;
};
class Refuelable {
public:
virtual void refuel(double amount) = 0;
virtual ~Refuelable() = default;
};
// ✅ 只实现需要的接口
class GoodCar : public Vehicle, public Drivable, public Refuelable {
private:
int wheels;
double maxSpeed;
int passengerCapacity;
public:
GoodCar(const std::string& b) : Vehicle(b), wheels(4),
maxSpeed(200.0), passengerCapacity(5) {}
void start() override {
std::cout << brand << " engine started." << std::endl;
}
void stop() override {
std::cout << brand << " stopped." << std::endl;
}
void drive() override {
std::cout << brand << " is driving at max " << maxSpeed << " km/h." << std::endl;
}
void refuel(double amount) override {
std::cout << "Refueling " << brand << " with " << amount << " liters." << std::endl;
}
};
class GoodAirplane : public Vehicle, public Flyable, public Refuelable {
private:
double maxSpeed;
int passengerCapacity;
public:
GoodAirplane(const std::string& b) : Vehicle(b),
maxSpeed(900.0), passengerCapacity(200) {}
void start() override {
std::cout << brand << " engines started." << std::endl;
}
void stop() override {
std::cout << brand << " landed and stopped." << std::endl;
}
void fly() override {
std::cout << brand << " is flying at " << maxSpeed << " km/h." << std::endl;
}
void refuel(double amount) override {
std::cout << "Refueling " << brand << " with " << amount << " liters." << std::endl;
}
};
关键变化点:
用组合替代继承(Replace Inheritance with Composition):
- 使用接口而不是深度继承
- 每个类只实现需要的接口
提高灵活性:
- 类可以灵活组合不同的接口
- 不需要继承不需要的功能
提高可维护性:
- 继承关系更清晰
- 代码更易理解
步骤3:重构技巧总结
使用的重构手法:
- 用组合替代继承(Replace Inheritance with Composition):使用接口和组合而不是深度继承
- 提取接口(Extract Interface):提取公共接口
注意事项:
- ⚠️ 确保接口设计合理
- ⚠️ 如果继承关系确实合理,可以保留
- ⚠️ 重构后要更新所有使用处,确保行为一致
6. 预防策略
🛡️ 编码时:
即时检查:
- 子类是否真的”是一个”父类?
- 子类是否需要父类的大部分功能?
- 使用IDE的代码分析工具,检测被拒绝的遗赠
小步提交:
- 发现被拒绝的遗赠时,立即重构继承关系
- 使用”用组合替代继承”重构,保持继承关系合理
🔍 Code Review清单:
重点检查:
- 子类是否重写父类方法只是为了拒绝它?
- 子类是否继承了不需要的字段?
- 继承关系是否合理?
拒绝标准:
- 子类重写父类方法只是为了拒绝它
- 子类继承了不需要的字段
- 继承关系不清晰
⚙️ 自动化防护:
IDE配置:
- 使用代码分析工具检测被拒绝的遗赠
- 启用继承关系警告
CI/CD集成:
- 在CI流水线中集成代码分析工具
- 检测被拒绝的遗赠,生成警告报告
下一篇预告:注释(Comments)- 如何用清晰的代码替代过多的注释
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 青羽川!
评论
