被拒绝的遗赠(Refused Bequest):坏味道识别与重构实战指南
24种代码坏味道系列 · 第23篇
1. 开篇场景
你是否遇到过这样的继承关系:子类继承了父类的方法和字段,但大部分都不需要或需要重写,就像继承了一笔”遗产”,但大部分都用不上,还要想办法处理?
1 2 3 4 5 6 7 8 9 10 11 12
| class BadVehicle { void drive() { } void fly() { } };
class BadCar : public BadVehicle { void fly() override { std::cout << "Cars cannot fly!" << std::endl; } };
|
这就是被拒绝的遗赠的典型症状。子类继承了父类的方法和字段,但大部分都不需要或需要重写,就像继承了一笔”遗产”,但大部分都用不上。
当你需要理解子类时,你必须理解它不需要的父类功能。当你需要修改子类时,你必须在不需要的功能中查找。这种设计使得代码变得复杂,增加了维护的难度。
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:逐步重构
重构前(问题代码)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51
| 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; } void fly() override { std::cout << "Cars cannot fly!" << std::endl; } };
|
问题分析:
BadCar 继承了 BadVehicle,但不需要 fly() 方法
BadCar 继承了不需要的字段(如 hasWings、canFly)
- 继承关系不合理
重构后(清洁版本)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85
| 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)- 如何用清晰的代码替代过多的注释