过长的消息链(Message Chains):坏味道识别与重构实战指南
24种代码坏味道系列 · 第17篇
1. 开篇场景
你是否遇到过这样的代码:连续调用多个方法,形成过长的调用链,就像一条长长的链条,任何一个环节断裂都会导致整个链条失效?
1 2 3 4 5 6 7 8
| void printEmployeeCity(Employee* employee) { if (employee != nullptr && employee->company != nullptr && employee->company->address != nullptr) { std::cout << "City: " << employee->company->address->city << std::endl; } }
|
这就是过长的消息链的典型症状。连续调用多个方法,形成过长的调用链,耦合度高,就像一条长长的链条,任何一个环节断裂都会导致整个链条失效。
当你需要修改调用链时,你必须在多个类中查找和修改。当你需要理解代码时,你必须理解整个调用链。这种设计使得代码变得脆弱,增加了维护的难度。
2. 坏味道定义
过长的消息链是指连续调用多个方法,形成过长的调用链,耦合度高。
就像一条长长的链条,任何一个环节断裂都会导致整个链条失效。
核心问题:调用链过长意味着类之间的耦合度高。应该使用委托方法隐藏调用链,减少耦合度,提高代码的可维护性。
3. 识别特征
🔍 代码表现:
- 特征1:连续调用多个方法(超过3个)
- 特征2:调用链中有多个
-> 或 . 操作符
- 特征3:调用链需要多个空指针检查
- 特征4:调用链在多个地方重复出现
- 特征5:调用链的中间对象只是用于传递调用
🎯 出现场景:
- 场景1:快速开发时,直接访问深层对象
- 场景2:缺乏设计,没有考虑类的职责
- 场景3:重构不彻底,只修改了部分代码
- 场景4:从过程式编程迁移到面向对象时,没有重构调用链
💡 快速自检:
- 问自己:这个调用链是否超过3个方法调用?
- 问自己:这个调用链是否在多个地方重复出现?
- 工具提示:使用代码分析工具检测过长的调用链
4. 危害分析
🚨 维护成本:修改调用链需要在多个类中修改,时间成本增加50%
⚠️ 缺陷风险:调用链中任何一个环节断裂都会导致失败,bug风险增加60%
🧱 扩展障碍:添加新功能时需要理解整个调用链
🤯 认知负担:需要理解整个调用链,增加了心理负担
5. 重构实战
步骤1:安全准备
- ✅ 确保有完整的单元测试覆盖
- ✅ 创建重构分支:
git checkout -b refactor/hide-delegate
- ✅ 使用版本控制,便于回滚
步骤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
| class Address { public: std::string street; std::string city; Address(const std::string& st, const std::string& ct) : street(st), city(ct) {} };
class Company { public: Address* address; Company(Address* addr) : address(addr) {} };
class Employee { public: Company* company; Employee(Company* comp) : company(comp) {} };
class BadExample { public: void printEmployeeCity(Employee* employee) { if (employee != nullptr && employee->company != nullptr && employee->company->address != nullptr) { std::cout << "City: " << employee->company->address->city << std::endl; } } void updateEmployeeStreet(Employee* employee, const std::string& newStreet) { if (employee != nullptr && employee->company != nullptr && employee->company->address != nullptr) { employee->company->address->street = newStreet; } } };
|
问题分析:
- 调用链过长:
employee->company->address->city
- 需要多个空指针检查
- 调用链在多个地方重复出现
重构后(清洁版本)
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
| class GoodAddress { private: std::string street; std::string city; public: GoodAddress(const std::string& st, const std::string& ct) : street(st), city(ct) {} std::string getCity() const { return city; } void setStreet(const std::string& st) { street = st; } };
class GoodCompany { private: GoodAddress* address; public: GoodCompany(GoodAddress* addr) : address(addr) {} std::string getCity() const { return address != nullptr ? address->getCity() : ""; } void setStreet(const std::string& st) { if (address != nullptr) { address->setStreet(st); } } };
class GoodEmployee { private: GoodCompany* company; public: GoodEmployee(GoodCompany* comp) : company(comp) {} std::string getCity() const { return company != nullptr ? company->getCity() : ""; } void setStreet(const std::string& st) { if (company != nullptr) { company->setStreet(st); } } };
class GoodExample { public: void printEmployeeCity(GoodEmployee* employee) { std::cout << "City: " << employee->getCity() << std::endl; } void updateEmployeeStreet(GoodEmployee* employee, const std::string& newStreet) { employee->setStreet(newStreet); } };
|
关键变化点:
隐藏委托(Hide Delegate):
简化调用:
- 调用链从
employee->company->address->city 简化为 employee->getCity()
- 减少了空指针检查的复杂度
提高可维护性:
- 调用链的变化被封装在类内部
- 修改调用链时只需修改类内部
步骤3:重构技巧总结
使用的重构手法:
- 隐藏委托(Hide Delegate):在类中添加委托方法,隐藏调用链
- 提取方法(Extract Method):将调用链提取为独立方法
注意事项:
- ⚠️ 确保委托方法有清晰的语义
- ⚠️ 如果调用链只在局部使用,考虑使用局部变量
- ⚠️ 重构后要更新所有使用处,确保行为一致
6. 预防策略
🛡️ 编码时:
即时检查:
- 调用链是否超过3个方法调用?
- 调用链是否在多个地方重复出现?
- 使用IDE的代码分析工具,检测过长的调用链
小步提交:
- 发现过长的调用链时,立即使用委托方法隐藏
- 使用”隐藏委托”重构,保持调用链简短
🔍 Code Review清单:
重点检查:
- 是否有过长的调用链?
- 调用链是否在多个地方重复出现?
- 是否可以使用委托方法隐藏调用链?
拒绝标准:
- 调用链超过3个方法调用
- 调用链需要多个空指针检查
- 调用链在多个地方重复出现
⚙️ 自动化防护:
IDE配置:
- 使用代码分析工具检测过长的调用链
- 启用调用链长度警告
CI/CD集成:
- 在CI流水线中集成代码分析工具
- 检测过长的调用链,生成警告报告
下一篇预告:中间人(Middle Man)- 如何消除只是简单转发的中间层