夸夸其谈通用性(Speculative Generality):坏味道识别与重构实战指南

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


1. 开篇场景

你是否遇到过这样的代码:为了”将来可能支持多种数据库”而创建了抽象接口,但实际上只使用了一种数据库;为了”将来可能支持多种格式”而创建了格式化器接口,但实际上只使用了一种格式?

1
2
3
4
5
6
7
class DatabaseConnection {
virtual void connect() = 0;
virtual void disconnect() = 0;
};

class MySQLConnection : public DatabaseConnection { /* ... */ };
class PostgreSQLConnection : public DatabaseConnection { /* ... */ }; // 从未使用

这就是夸夸其谈通用性的典型症状。为了”将来可能用到”而添加的过度抽象,实际上从未使用,就像买了一堆”可能用到的工具”,但从未真正使用过。

当你需要理解代码时,你必须理解这些从未使用的抽象。当你需要修改代码时,你必须在这些抽象中查找。这种设计使得代码变得复杂,增加了维护的难度。


2. 坏味道定义

夸夸其谈通用性是指为了”将来可能用到”而添加的过度抽象,实际上从未使用。

就像买了一堆可能用到的工具但从未使用,不仅占地方,还增加了维护成本。

核心问题:抽象应该基于实际需求。如果抽象从未被使用,就应该删除。YAGNI原则(You Aren’t Gonna Need It)告诉我们,不要添加”可能用到”的功能。


3. 识别特征

🔍 代码表现:

  • 特征1:定义了但从未使用的类或接口
  • 特征2:为了”将来可能用到”而添加的抽象
  • 特征3:抽象层没有实际的使用场景
  • 特征4:代码中有”预留”的功能
  • 特征5:删除抽象后,功能不受影响

🎯 出现场景:

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

💡 快速自检:

  • 问自己:这个抽象是否真的被使用?
  • 问自己:如果删除这个抽象,功能是否受影响?
  • 工具提示:使用代码分析工具检测未使用的类或接口

4. 危害分析

🚨 维护成本:需要理解未使用的抽象,时间成本增加40%

⚠️ 缺陷风险:未使用的抽象可能引入新的bug,风险增加30%

🧱 扩展障碍:添加新功能时不知道应该使用哪个抽象

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


5. 重构实战

步骤1:安全准备

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

步骤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
// 坏味道:为了"将来可能支持多种数据库"而抽象,但实际只用一种
class DatabaseConnection {
public:
virtual void connect() = 0;
virtual void disconnect() = 0;
virtual void executeQuery(const std::string& query) = 0;
virtual ~DatabaseConnection() = default;
};

class MySQLConnection : public DatabaseConnection {
// 实际使用的实现
};

// 坏味道:定义了但从未使用的类
class PostgreSQLConnection : public DatabaseConnection {
// 从未使用
};

// 坏味道:为了"将来可能支持多种格式"而抽象,但实际只用一种
class DataFormatter {
public:
virtual std::string format(const std::string& data) = 0;
virtual ~DataFormatter() = default;
};

class JSONFormatter : public DataFormatter {
// 实际使用的实现
};

// 坏味道:定义了但从未使用的类
class XMLFormatter : public DataFormatter {
// 从未使用
};

问题分析

  • PostgreSQLConnectionXMLFormatter 从未使用
  • 抽象接口增加了复杂度,但没有带来价值
  • 如果将来真的需要支持多种数据库或格式,再添加也不迟

重构后(清洁版本)

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
// ✅ 只在真正需要时才抽象
class GoodExample {
private:
// 直接使用具体类型,如果将来真的需要支持多种,再重构
void connectToMySQL() {
std::cout << "Connecting to MySQL" << std::endl;
}

void disconnectFromMySQL() {
std::cout << "Disconnecting from MySQL" << std::endl;
}

void executeMySQLQuery(const std::string& query) {
std::cout << "Executing MySQL query: " << query << std::endl;
}

std::string formatAsJSON(const std::string& data) {
return "{\"data\":\"" + data + "\"}";
}

public:
void processData(const std::string& data) {
connectToMySQL();
std::string formatted = formatAsJSON(data);
executeMySQLQuery("INSERT INTO table VALUES (" + formatted + ")");
disconnectFromMySQL();
}
};

关键变化点

  1. 删除未使用的抽象

    • 删除 PostgreSQLConnectionXMLFormatter
    • 删除不必要的抽象接口
  2. 简化代码

    • 直接使用具体类型
    • 如果将来真的需要支持多种,再重构
  3. 提高可读性

    • 减少了抽象层,代码更易读
    • 代码更直接,更易理解

步骤3:重构技巧总结

使用的重构手法

  • 删除未使用的代码(Remove Dead Code):删除从未使用的类或接口
  • 内联类(Inline Class):删除不必要的抽象层

注意事项

  • ⚠️ 确保代码确实从未使用
  • ⚠️ 如果代码将来可能用到,考虑添加注释说明
  • ⚠️ 重构后要运行所有测试,确保功能正常

6. 预防策略

🛡️ 编码时:

  • 即时检查

    • 抽象是否真的被使用?
    • 如果删除抽象,功能是否受影响?
    • 遵循YAGNI原则,不要添加”可能用到”的功能
  • 小步提交

    • 发现未使用的抽象时,立即删除
    • 使用”删除未使用的代码”重构,保持代码简洁

🔍 Code Review清单:

  • 重点检查

    • 是否有未使用的抽象?
    • 抽象是否基于实际需求?
    • 是否可以简化代码?
  • 拒绝标准

    • 定义了但从未使用的类或接口
    • 为了”将来可能用到”而添加的抽象
    • 没有实际使用场景的抽象层

⚙️ 自动化防护:

  • IDE配置

    • 使用代码分析工具检测未使用的类或接口
    • 启用未使用代码警告
  • CI/CD集成

    • 在CI流水线中集成代码分析工具
    • 检测未使用的代码,生成警告报告

下一篇预告:临时字段(Temporary Field)- 如何消除只在特定情况下使用的字段