冗赘的元素(Lazy Element):坏味道识别与重构实战指南

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


1. 开篇场景

你是否遇到过这样的代码:一个 StringWrapper 类只是简单地包装了 std::string,没有添加任何额外功能;一个 getName 方法只是简单地返回 name.getValue(),没有添加任何逻辑?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class StringWrapper {
std::string value;
public:
std::string getValue() { return value; }
void setValue(const std::string& str) { value = str; }
};

class BadExample {
StringWrapper name;
public:
std::string getName() {
return name.getValue(); // 只是简单地转发
}
};

这就是冗赘的元素的典型症状。不必要的抽象层增加了复杂度,但没有带来任何价值,就像给一个简单的工具套上多层包装,不仅没有保护作用,反而增加了使用的复杂度。

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


2. 坏味道定义

冗赘的元素是指不必要的抽象层,增加了复杂度但没有带来价值。

就像给简单工具套上多层包装,不仅没有保护作用,反而增加了使用的复杂度。

核心问题:抽象应该带来价值。如果抽象只是简单地转发调用,没有添加任何逻辑,就应该去掉。简单的代码比复杂的代码更好。


3. 识别特征

🔍 代码表现:

  • 特征1:类只是简单地包装另一个类,没有添加功能
  • 特征2:方法只是简单地转发调用,没有添加逻辑
  • 特征3:抽象层没有提供额外的价值
  • 特征4:代码中有不必要的中间层
  • 特征5:删除抽象层后,功能不受影响

🎯 出现场景:

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

💡 快速自检:

  • 问自己:这个抽象是否带来了价值?
  • 问自己:如果删除这个抽象,功能是否受影响?
  • 工具提示:使用代码分析工具检测不必要的抽象层

4. 危害分析

🚨 维护成本:需要理解不必要的抽象层,时间成本增加30%

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

🧱 扩展障碍:添加新功能时需要穿过抽象层,增加了复杂度

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


5. 重构实战

步骤1:安全准备

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

步骤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
// 坏味道:不必要的包装类
class StringWrapper {
private:
std::string value;

public:
StringWrapper(const std::string& str) : value(str) {}
std::string getValue() { return value; }
void setValue(const std::string& str) { value = str; }
int length() { return value.length(); }
};

// 坏味道:不必要的中间方法
class BadExample {
private:
StringWrapper name;

public:
BadExample(const std::string& n) : name(n) {}

// 这个方法只是简单地转发,没有添加价值
std::string getName() {
return name.getValue();
}

// 这个方法也只是转发
void setName(const std::string& n) {
name.setValue(n);
}

// 这个方法也只是转发
int getNameLength() {
return name.length();
}
};

问题分析

  • StringWrapper 只是简单地包装 std::string,没有添加功能
  • getNamesetNamegetNameLength 只是简单地转发调用
  • 这些抽象层没有带来任何价值

重构后(清洁版本)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// ✅ 直接使用,去掉不必要的抽象
class GoodExample {
private:
std::string name; // ✅ 直接使用 std::string

public:
GoodExample(const std::string& n) : name(n) {}

// ✅ 直接访问,不需要包装
std::string getName() const {
return name;
}

void setName(const std::string& n) {
name = n;
}

int getNameLength() const {
return name.length();
}
};

关键变化点

  1. 内联类(Inline Class)

    • 删除不必要的 StringWrapper
    • 直接使用 std::string
  2. 简化方法

    • 方法直接操作 name,不需要通过包装类
    • 代码更简洁、更直接
  3. 提高可读性

    • 减少了抽象层,代码更易读
    • 减少了方法调用,性能更好

步骤3:重构技巧总结

使用的重构手法

  • 内联类(Inline Class):删除不必要的包装类
  • 内联方法(Inline Method):删除不必要的转发方法

注意事项

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

6. 预防策略

🛡️ 编码时:

  • 即时检查

    • 抽象是否带来了价值?
    • 如果删除抽象,功能是否受影响?
    • 使用IDE的代码分析工具,检测不必要的抽象层
  • 小步提交

    • 发现不必要的抽象层时,立即删除
    • 使用”内联类”或”内联方法”重构,保持代码简洁

🔍 Code Review清单:

  • 重点检查

    • 是否有不必要的抽象层?
    • 抽象是否带来了价值?
    • 是否可以简化代码?
  • 拒绝标准

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

⚙️ 自动化防护:

  • IDE配置

    • 使用代码分析工具检测不必要的抽象层
    • 启用代码复杂度警告
  • CI/CD集成

    • 在CI流水线中集成代码分析工具
    • 检测不必要的抽象层,生成警告报告

下一篇预告:夸夸其谈通用性(Speculative Generality)- 如何避免过度设计