被拒绝的遗赠(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; // 拒绝继承的方法
}
// 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:逐步重构

重构前(问题代码)

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;
}

// 需要重写 fly 方法,因为汽车不能飞
void fly() override {
std::cout << "Cars cannot fly!" << std::endl; // 拒绝继承的方法
}

// hasWings, canFly 字段对汽车没用,但继承了
};

问题分析

  • BadCar 继承了 BadVehicle,但不需要 fly() 方法
  • BadCar 继承了不需要的字段(如 hasWingscanFly
  • 继承关系不合理

重构后(清洁版本)

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;
}
};

关键变化点

  1. 用组合替代继承(Replace Inheritance with Composition)

    • 使用接口而不是深度继承
    • 每个类只实现需要的接口
  2. 提高灵活性

    • 类可以灵活组合不同的接口
    • 不需要继承不需要的功能
  3. 提高可维护性

    • 继承关系更清晰
    • 代码更易理解

步骤3:重构技巧总结

使用的重构手法

  • 用组合替代继承(Replace Inheritance with Composition):使用接口和组合而不是深度继承
  • 提取接口(Extract Interface):提取公共接口

注意事项

  • ⚠️ 确保接口设计合理
  • ⚠️ 如果继承关系确实合理,可以保留
  • ⚠️ 重构后要更新所有使用处,确保行为一致

6. 预防策略

🛡️ 编码时:

  • 即时检查

    • 子类是否真的”是一个”父类?
    • 子类是否需要父类的大部分功能?
    • 使用IDE的代码分析工具,检测被拒绝的遗赠
  • 小步提交

    • 发现被拒绝的遗赠时,立即重构继承关系
    • 使用”用组合替代继承”重构,保持继承关系合理

🔍 Code Review清单:

  • 重点检查

    • 子类是否重写父类方法只是为了拒绝它?
    • 子类是否继承了不需要的字段?
    • 继承关系是否合理?
  • 拒绝标准

    • 子类重写父类方法只是为了拒绝它
    • 子类继承了不需要的字段
    • 继承关系不清晰

⚙️ 自动化防护:

  • IDE配置

    • 使用代码分析工具检测被拒绝的遗赠
    • 启用继承关系警告
  • CI/CD集成

    • 在CI流水线中集成代码分析工具
    • 检测被拒绝的遗赠,生成警告报告

下一篇预告:注释(Comments)- 如何用清晰的代码替代过多的注释