中间人(Middle Man):坏味道识别与重构实战指南

24种代码坏味道系列 · 第18篇


1. 开篇场景

你是否遇到过这样的类:它的大部分方法只是简单地转发调用给另一个类,没有添加任何逻辑,就像一个”传话筒”,只是把消息传递过去?

1
2
3
4
5
6
7
8
9
10
class PersonManager {
Person* person;
public:
std::string getName() {
return person->getName(); // 只是简单地转发
}
void setName(const std::string& n) {
person->setName(n); // 只是简单地转发
}
};

这就是中间人的典型症状。类的大部分方法只是简单地委托给另一个类,没有添加价值,就像一个”传话筒”,只是把消息传递过去,没有添加任何信息。

当你需要理解代码时,你必须穿过这些中间层。当你需要修改代码时,你必须在多个层次中查找和修改。这种设计使得代码变得复杂,增加了维护的难度。


2. 坏味道定义

中间人是指类的大部分方法只是简单地委托给另一个类,没有添加价值。

就像一个传话筒,只是把消息传递过去,没有添加任何信息。

核心问题:如果类只是简单地转发调用,没有添加任何逻辑,就应该直接使用原类。如果类需要添加功能,才应该使用包装。


3. 识别特征

🔍 代码表现:

  • 特征1:类的大部分方法只是简单地转发调用
  • 特征2:方法没有添加任何逻辑或验证
  • 特征3:类只是包装另一个类,没有添加功能
  • 特征4:删除类后,功能不受影响
  • 特征5:类的所有方法都是简单的getter/setter转发

🎯 出现场景:

  • 场景1:过度设计,为了”将来可能用到”而添加包装层
  • 场景2:重构不彻底,只添加了包装但没有实现功能
  • 场景3:从其他代码复制时,保留了包装层
  • 场景4:缺乏设计,没有考虑包装的价值

💡 快速自检:

  • 问自己:这个类是否只是简单地转发调用?
  • 问自己:如果删除这个类,功能是否受影响?
  • 工具提示:使用代码分析工具检测中间人模式

4. 危害分析

🚨 维护成本:需要理解中间层,时间成本增加30%

⚠️ 缺陷风险:中间层可能引入新的bug,风险增加20%

🧱 扩展障碍:添加新功能时需要穿过中间层

🤯 认知负担:需要理解中间层的存在原因,增加了心理负担


5. 重构实战

步骤1:安全准备

  • ✅ 确保有完整的单元测试覆盖
  • ✅ 创建重构分支:git checkout -b refactor/remove-middle-man
  • ✅ 使用版本控制,便于回滚

步骤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
class Person {
private:
std::string name;
int age;

public:
Person(const std::string& n, int a) : name(n), age(a) {}

std::string getName() const { return name; }
void setName(const std::string& n) { name = n; }
int getAge() const { return age; }
void setAge(int a) { age = a; }

void display() const {
std::cout << "Name: " << name << ", Age: " << age << std::endl;
}
};

// 坏味道:中间人,只是简单地转发调用
class PersonManager {
private:
Person* person;

public:
PersonManager(Person* p) : person(p) {}

// 这些方法只是简单地转发,没有添加价值
std::string getName() {
return person->getName();
}

void setName(const std::string& n) {
person->setName(n);
}

int getAge() {
return person->getAge();
}

void setAge(int a) {
person->setAge(a);
}

void display() {
person->display();
}
};

问题分析

  • PersonManager 只是简单地转发调用,没有添加任何逻辑
  • 所有方法都是简单的getter/setter转发
  • 增加了不必要的抽象层

重构后(清洁版本)

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
// 好的做法:如果只是转发,直接使用原类;如果需要添加功能,才使用包装
class PersonService {
private:
Person* person;

public:
PersonService(Person* p) : person(p) {}

// 如果只是转发,直接暴露原对象
Person* getPerson() { return person; }

// 或者添加真正的业务逻辑
bool isAdult() const {
return person->getAge() >= 18;
}

void celebrateBirthday() {
person->setAge(person->getAge() + 1);
std::cout << "Happy Birthday! Now " << person->getName()
<< " is " << person->getAge() << " years old." << std::endl;
}

void display() {
person->display();
if (isAdult()) {
std::cout << "This person is an adult." << std::endl;
}
}
};

关键变化点

  1. 删除中间人(Remove Middle Man)

    • 如果只是转发,直接使用原类
    • 如果需要添加功能,才使用包装
  2. 添加价值

    • PersonService 添加了真正的业务逻辑(如 isAdultcelebrateBirthday
    • 包装层带来了实际价值
  3. 提高可维护性

    • 减少了不必要的抽象层
    • 代码更直接、更易理解

步骤3:重构技巧总结

使用的重构手法

  • 删除中间人(Remove Middle Man):删除只是简单转发的中间层
  • 内联类(Inline Class):将包装类内联到使用处

注意事项

  • ⚠️ 确保中间层确实没有价值
  • ⚠️ 如果中间层将来可能有用,考虑保留但添加注释
  • ⚠️ 重构后要更新所有使用处,确保行为一致

6. 预防策略

🛡️ 编码时:

  • 即时检查

    • 类是否只是简单地转发调用?
    • 如果删除类,功能是否受影响?
    • 使用IDE的代码分析工具,检测中间人模式
  • 小步提交

    • 发现中间人时,立即删除或添加功能
    • 使用”删除中间人”重构,保持代码简洁

🔍 Code Review清单:

  • 重点检查

    • 是否有只是简单转发的中间层?
    • 中间层是否带来了价值?
    • 是否可以简化代码?
  • 拒绝标准

    • 只是简单地转发调用的中间层
    • 没有添加任何逻辑的包装类
    • 增加了复杂度但没有带来价值的抽象

⚙️ 自动化防护:

  • IDE配置

    • 使用代码分析工具检测中间人模式
    • 启用代码复杂度警告
  • CI/CD集成

    • 在CI流水线中集成代码分析工具
    • 检测中间人模式,生成警告报告

下一篇预告:内幕交易(Insider Trading)- 如何减少类之间的过度耦合