24种代码坏味道系列 · 第24篇
1. 开篇场景
你是否遇到过这样的代码:代码中充满了注释,每个步骤都有注释说明,但代码本身却难以理解,就像在一本难懂的书上写满了注释,但书本身就应该写得清楚?
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
|
double calc(std::vector<std::string> users) { double total = 0.0; for (int i = 0; i < users.size(); i++) { std::string u = users[i]; if (u.empty()) { continue; } double fee = 100.0; if (u.length() > 5) { fee += 50.0; } total += fee; } return total; }
|
这就是注释的典型症状。过多的注释通常说明代码不够清晰,应该用清晰的代码表达意图,而不是依赖注释。
当你需要修改代码时,你必须同时更新代码和注释。当你需要理解代码时,你必须阅读代码和注释。这种设计使得代码变得复杂,增加了维护的难度。
2. 坏味道定义
注释是指过多的注释通常说明代码不够清晰,应该用清晰的代码表达意图。
就像在一本难懂的书上写满了注释,但书本身就应该写得清楚。
核心问题:代码应该自解释。如果代码需要大量注释才能理解,说明代码不够清晰,应该重构代码而不是添加注释。
3. 识别特征
🔍 代码表现:
- 特征1:代码中有大量注释
- 特征2:注释解释的是”做什么”而不是”为什么”
- 特征3:注释和代码重复
- 特征4:代码难以理解,需要注释才能理解
- 特征5:注释过时,与代码不一致
🎯 出现场景:
- 场景1:快速开发时,使用注释解释复杂逻辑
- 场景2:缺乏设计,代码不够清晰
- 场景3:重构不彻底,只添加了注释
- 场景4:从其他代码复制时,保留了注释
💡 快速自检:
- 问自己:这段代码是否可以用更清晰的代码表达?
- 问自己:如果删除注释,代码是否仍然清晰?
- 工具提示:使用代码分析工具检测注释密度
4. 危害分析
🚨 维护成本:需要同时维护代码和注释,时间成本增加30%
⚠️ 缺陷风险:注释过时导致理解错误,bug风险增加20%
🧱 扩展障碍:添加新功能时需要更新注释
🤯 认知负担:需要阅读代码和注释,增加了心理负担
5. 重构实战
步骤1:安全准备
- ✅ 确保有完整的单元测试覆盖
- ✅ 创建重构分支:
git checkout -b refactor/remove-comments
- ✅ 使用版本控制,便于回滚
步骤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 49 50 51 52 53 54 55
| class BadExample { public: double calc(std::vector<std::string> users) { double total = 0.0; for (int i = 0; i < users.size(); i++) { std::string u = users[i]; if (u.empty()) { continue; } double fee = 100.0; if (u.length() > 5) { fee += 50.0; } total += fee; } return total; } void proc(int orderId, double amount) { if (orderId <= 0) { std::cout << "Invalid order ID" << std::endl; return; } if (amount <= 0) { std::cout << "Invalid amount" << std::endl; return; } std::cout << "Processing order " << orderId << " with amount " << amount << std::endl; } };
|
问题分析:
- 代码中有大量注释
- 注释解释的是”做什么”而不是”为什么”
- 代码本身不够清晰
重构后(清洁版本)
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
| class GoodExample { private: static constexpr double BASE_FEE = 100.0; static constexpr double PREMIUM_FEE = 50.0; static constexpr int PREMIUM_NAME_LENGTH = 5; bool isValidUser(const std::string& userName) const { return !userName.empty(); } double calculateUserFee(const std::string& userName) const { double fee = BASE_FEE; if (isPremiumUser(userName)) { fee += PREMIUM_FEE; } return fee; } bool isPremiumUser(const std::string& userName) const { return userName.length() > PREMIUM_NAME_LENGTH; } bool isValidOrderId(int orderId) const { return orderId > 0; } bool isValidAmount(double amount) const { return amount > 0; } public: double calculateTotalUserFees(const std::vector<std::string>& users) const { double total = 0.0; for (const auto& userName : users) { if (!isValidUser(userName)) { continue; } total += calculateUserFee(userName); } return total; } void processOrder(int orderId, double amount) { if (!isValidOrderId(orderId)) { std::cout << "Invalid order ID" << std::endl; return; } if (!isValidAmount(amount)) { std::cout << "Invalid amount" << std::endl; return; } std::cout << "Processing order " << orderId << " with amount " << amount << std::endl; } };
|
关键变化点:
提取方法(Extract Method):
使用常量:
提高可读性:
- 代码自解释,不需要注释
- 方法名和变量名清晰表达意图
步骤3:重构技巧总结
使用的重构手法:
- 提取方法(Extract Method):将复杂逻辑提取为独立方法
- 引入常量(Introduce Constant):使用常量替代魔法数字
- 重命名(Rename):使用清晰的命名
注意事项:
- ⚠️ 注释应该解释”为什么”而不是”做什么”
- ⚠️ 如果代码确实复杂,可以保留注释但说明原因
- ⚠️ 重构后要删除过时的注释
6. 预防策略
🛡️ 编码时:
即时检查:
- 代码是否可以用更清晰的代码表达?
- 如果删除注释,代码是否仍然清晰?
- 使用IDE的代码分析工具,检测注释密度
小步提交:
- 发现需要大量注释的代码时,立即重构
- 使用”提取方法”重构,保持代码清晰
🔍 Code Review清单:
重点检查:
- 是否有过多的注释?
- 注释是否解释”为什么”而不是”做什么”?
- 代码是否可以用更清晰的代码表达?
拒绝标准:
- 注释解释的是”做什么”而不是”为什么”
- 注释和代码重复
- 代码难以理解,需要注释才能理解
⚙️ 自动化防护:
IDE配置:
CI/CD集成:
- 在CI流水线中集成代码分析工具
- 检测注释密度,生成警告报告
系列总结:恭喜你完成了24种代码坏味道的学习!这些坏味道是代码质量的重要指标,识别和重构它们可以显著提高代码的可维护性和可读性。记住:好的代码应该自解释,清晰简洁,易于理解和修改。