过大的类(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; };
|
这就是过大的类的典型症状。类承担了太多职责,包含太多字段和方法,就像一个”万能工具箱”,虽然功能齐全,但难以理解和维护。
当你需要修改某个功能时,你必须在庞大的类中查找和修改。当你需要添加新功能时,你不知道应该放在哪里。这种设计使得代码变得复杂,增加了维护的难度。
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); } };
|
关键变化点:
提取类(Extract Class):
使用组合:
GoodExample 使用组合而不是继承
- 通过组合使用各个功能类
提高可维护性:
步骤3:重构技巧总结
使用的重构手法:
- 提取类(Extract Class):将不同职责提取到不同的类中
- 移动方法(Move Method):将方法移到合适的类中
- 移动字段(Move Field):将字段移到合适的类中
注意事项:
- ⚠️ 确保每个类有清晰的职责边界
- ⚠️ 如果类之间有依赖,使用组合而不是继承
- ⚠️ 重构后要更新所有使用处,确保行为一致
6. 预防策略
🛡️ 编码时:
即时检查:
- 类是否只负责一个职责?
- 类的字段和方法是否可以分组?
- 使用IDE的代码度量工具,检测类的复杂度
小步提交:
- 发现类承担多个职责时,立即拆分
- 使用”提取类”重构,保持类的职责单一
🔍 Code Review清单:
重点检查:
- 类是否承担了多个职责?
- 类的字段和方法是否可以分组?
- 是否可以拆分为更小的类?
拒绝标准:
- 类包含大量字段和方法(超过20个方法)
- 类的字段可以明显分组
- 类的方法可以明显分组
⚙️ 自动化防护:
IDE配置:
CI/CD集成:
- 在CI流水线中集成代码度量工具
- 设置类的复杂度阈值,超过阈值时生成警告
下一篇预告:异曲同工的类(Alternative Classes with Different Interfaces)- 如何统一功能相似但接口不同的类