过长的消息链(Message Chains):坏味道识别与重构实战指南
过长的消息链(Message Chains):坏味道识别与重构实战指南
24种代码坏味道系列 · 第17篇
1. 开篇场景
你是否遇到过这样的代码:连续调用多个方法,形成过长的调用链,就像一条长长的链条,任何一个环节断裂都会导致整个链条失效?
void printEmployeeCity(Employee* employee) {
// 过长的调用链:employee -> company -> address -> city
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:逐步重构
重构前(问题代码)
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) {
// 过长的调用链:employee -> company -> address -> city
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 - 需要多个空指针检查
- 调用链在多个地方重复出现
重构后(清洁版本)
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)- 如何消除只是简单转发的中间层
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 青羽川!
评论
