过大的类(Large Class):坏味道识别与重构实战指南

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


1. 开篇场景

你是否遇到过这样的类:它包含了用户管理、订单管理、支付管理、地址管理、购物车管理等多个功能,就像一个”万能工具箱”,虽然什么都能做,但找起工具来却要翻遍整个箱子?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class BadExample {
// 用户相关字段
std::string userName;
std::string userEmail;
// ... 更多字段

// 订单相关字段
std::vector<int> orderIds;
// ... 更多字段

// 支付相关字段
double balance;
// ... 更多字段

// 50+ 方法处理各种功能
};

这就是过大的类的典型症状。类承担了太多职责,包含太多字段和方法,就像一个”万能工具箱”,虽然功能齐全,但难以理解和维护。

当你需要修改某个功能时,你必须在庞大的类中查找和修改。当你需要添加新功能时,你不知道应该放在哪里。这种设计使得代码变得复杂,增加了维护的难度。


2. 坏味道定义

过大的类是指类承担了太多职责,包含太多字段和方法。

就像一个万能工具箱,虽然功能齐全,但找起工具来却要翻遍整个箱子。

核心问题:类应该只负责一个职责。如果类包含太多字段和方法,说明它承担了太多职责,应该拆分。


3. 识别特征

🔍 代码表现:

  • 特征1:类包含大量字段(超过10个)
  • 特征2:类包含大量方法(超过20个)
  • 特征3:类的字段可以分组(如用户相关、订单相关)
  • 特征4:类的方法可以分组(如用户管理、订单管理)
  • 特征5:类的注释中提到了多个职责

🎯 出现场景:

  • 场景1:快速开发时,将所有功能放在一个类中
  • 场景2:重构不彻底,只修改了部分代码
  • 场景3:缺乏设计,没有考虑单一职责原则
  • 场景4:需求变更时,不断在现有类中添加新功能

💡 快速自检:

  • 问自己:这个类是否只负责一个职责?
  • 问自己:如果删除这个类的某个功能,类名是否仍然准确?
  • 工具提示:使用代码度量工具检测类的复杂度

4. 危害分析

🚨 维护成本:修改某个功能需要理解整个类,时间成本增加60%

⚠️ 缺陷风险:类承担太多职责,bug风险增加70%

🧱 扩展障碍:添加新功能时不知道应该放在哪里

🤯 认知负担:需要理解所有职责才能修改,心理负担重


5. 重构实战

步骤1:安全准备

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

步骤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
// 坏味道:一个类做了太多事情
class BadExample {
private:
// 用户相关字段
std::string userName;
std::string userEmail;
std::string userPassword;
int userAge;

// 订单相关字段
std::vector<int> orderIds;
std::map<int, double> orderAmounts;

// 支付相关字段
double balance;
std::string paymentMethod;
std::vector<std::string> transactionHistory;

// 地址相关字段
std::string street;
std::string city;
std::string zipCode;

// 购物车相关字段
std::vector<int> cartItems;
double cartTotal;

public:
// 用户管理方法
void setUserName(const std::string& name) { userName = name; }
// ... 更多方法

// 订单管理方法
void createOrder(int orderId, double amount) { /* ... */ }
// ... 更多方法

// 支付管理方法
bool processPayment(double amount) { /* ... */ }
// ... 更多方法

// 地址管理方法
void setAddress(const std::string& st, const std::string& ct, const std::string& zip) { /* ... */ }
// ... 更多方法

// 购物车管理方法
void addToCart(int itemId) { /* ... */ }
// ... 更多方法
};

问题分析

  • 类承担了5个不同的职责:用户管理、订单管理、支付管理、地址管理、购物车管理
  • 包含大量字段和方法
  • 类的职责不清晰

重构后(清洁版本)

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
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
// ✅ 按职责拆分为多个类
class User {
private:
std::string name;
std::string email;
std::string password;
int age;

public:
void setName(const std::string& n) { name = n; }
std::string getName() const { return name; }
void setEmail(const std::string& em) { email = em; }
void setPassword(const std::string& pwd) { password = pwd; }
bool validate() const { return !name.empty() && !email.empty(); }
};

class Order {
private:
int orderId;
double amount;
bool isCancelled;

public:
Order(int id, double amt) : orderId(id), amount(amt), isCancelled(false) {}
void cancel() { isCancelled = true; }
double getTotal() const { return amount; }
int getId() const { return orderId; }
};

class Payment {
private:
double balance;
std::string paymentMethod;
std::vector<std::string> transactionHistory;

public:
Payment(double bal) : balance(bal) {}
void setBalance(double bal) { balance = bal; }
double getBalance() const { return balance; }
bool processPayment(double amount) {
if (balance >= amount) {
balance -= amount;
transactionHistory.push_back("Payment: " + std::to_string(amount));
return true;
}
return false;
}
};

class Address {
private:
std::string street;
std::string city;
std::string zipCode;

public:
void setAddress(const std::string& st, const std::string& ct, const std::string& zip) {
street = st;
city = ct;
zipCode = zip;
}
std::string getFullAddress() const {
return street + ", " + city + " " + zipCode;
}
};

class ShoppingCart {
private:
std::vector<int> items;
double total;

public:
void addItem(int itemId) { items.push_back(itemId); }
void removeItem(int itemId) { /* ... */ }
void calculateTotal() {
total = items.size() * 10.0;
}
double getTotal() const { return total; }
};

// ✅ 组合类,使用组合而不是把所有功能放在一个类中
class GoodExample {
private:
User user;
Payment payment;
Address address;
ShoppingCart cart;
std::vector<Order> orders;

public:
GoodExample() : payment(1000.0) {}

User& getUser() { return user; }
Payment& getPayment() { return payment; }
Address& getAddress() { return address; }
ShoppingCart& getCart() { return cart; }

void createOrder(int orderId, double amount) {
orders.emplace_back(orderId, amount);
}
};

关键变化点

  1. 提取类(Extract Class)

    • 将不同职责提取到不同的类中
    • 每个类只负责一个职责
  2. 使用组合

    • GoodExample 使用组合而不是继承
    • 通过组合使用各个功能类
  3. 提高可维护性

    • 每个类职责清晰
    • 修改某个功能只需修改对应类

步骤3:重构技巧总结

使用的重构手法

  • 提取类(Extract Class):将不同职责提取到不同的类中
  • 移动方法(Move Method):将方法移到合适的类中
  • 移动字段(Move Field):将字段移到合适的类中

注意事项

  • ⚠️ 确保每个类有清晰的职责边界
  • ⚠️ 如果类之间有依赖,使用组合而不是继承
  • ⚠️ 重构后要更新所有使用处,确保行为一致

6. 预防策略

🛡️ 编码时:

  • 即时检查

    • 类是否只负责一个职责?
    • 类的字段和方法是否可以分组?
    • 使用IDE的代码度量工具,检测类的复杂度
  • 小步提交

    • 发现类承担多个职责时,立即拆分
    • 使用”提取类”重构,保持类的职责单一

🔍 Code Review清单:

  • 重点检查

    • 类是否承担了多个职责?
    • 类的字段和方法是否可以分组?
    • 是否可以拆分为更小的类?
  • 拒绝标准

    • 类包含大量字段和方法(超过20个方法)
    • 类的字段可以明显分组
    • 类的方法可以明显分组

⚙️ 自动化防护:

  • IDE配置

    • 使用代码度量工具检测类的复杂度
    • 启用类大小警告
  • CI/CD集成

    • 在CI流水线中集成代码度量工具
    • 设置类的复杂度阈值,超过阈值时生成警告

下一篇预告:异曲同工的类(Alternative Classes with Different Interfaces)- 如何统一功能相似但接口不同的类