基本类型偏执(Primitive Obsession):坏味道识别与重构实战指南
24种代码坏味道系列 · 第11篇
1. 开篇场景
你是否遇到过这样的代码:使用 std::string 表示邮箱地址,但没有验证;使用 std::string 表示电话号码,但没有验证;使用 int 表示年龄,但没有范围检查?
1 2 3 4 5 6 7 8 9
| void sendEmail(std::string email, std::string subject, std::string body) { std::cout << "Sending email to: " << email << std::endl; }
void setAge(int age) { std::cout << "Age set to: " << age << std::endl; }
|
这就是基本类型偏执的典型症状。过度使用基本类型(如 int、string、double)表示有意义的领域概念,就像用数字”1”和”0”表示”是”和”否”,虽然可以工作,但语义不清晰,容易出错。
当你需要验证邮箱格式时,你必须在每个使用邮箱的地方添加验证逻辑。当你需要修改年龄范围时,你必须在所有使用年龄的地方修改。这种设计使得代码难以维护,容易产生bug。
2. 坏味道定义
基本类型偏执是指过度使用基本类型表示有意义的领域概念,应该使用更有意义的类型。
就像用数字编码表示所有信息,虽然可以工作,但语义不清晰,容易出错。
核心问题:基本类型没有语义,无法表达领域概念。使用有意义的类型可以提高代码的可读性和可维护性,同时可以在类型层面保证数据的有效性。
3. 识别特征
🔍 代码表现:
- 特征1:使用基本类型表示有意义的领域概念(如用
string 表示邮箱)
- 特征2:基本类型需要验证,但验证逻辑分散在多个地方
- 特征3:基本类型有特殊含义,但没有类型层面的保护
- 特征4:多个地方使用相同的基本类型,但含义不同
- 特征5:基本类型需要转换或格式化,但逻辑分散
🎯 出现场景:
- 场景1:快速开发时,使用基本类型简化代码
- 场景2:从其他语言迁移时,保留了基本类型的习惯
- 场景3:缺乏领域建模,没有考虑类型设计
- 场景4:重构不彻底,只修改了部分代码
💡 快速自检:
- 问自己:这个基本类型是否有特殊含义?
- 问自己:这个基本类型是否需要验证?
- 工具提示:使用代码分析工具检测基本类型的使用模式
4. 危害分析
🚨 维护成本:验证逻辑分散在多个地方,时间成本增加50%
⚠️ 缺陷风险:基本类型没有验证,容易产生无效数据,bug风险增加60%
🧱 扩展障碍:添加新验证规则需要在多个地方修改
🤯 认知负担:需要理解基本类型的特殊含义,增加了心理负担
5. 重构实战
步骤1:安全准备
- ✅ 确保有完整的单元测试覆盖
- ✅ 创建重构分支:
git checkout -b refactor/replace-primitive-with-object
- ✅ 使用版本控制,便于回滚
步骤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
| class BadExample { public: void sendEmail(std::string email, std::string subject, std::string body) { std::cout << "Sending email to: " << email << std::endl; } void callPhone(std::string phoneNumber) { std::cout << "Calling: " << phoneNumber << std::endl; } void processPayment(std::string amount, std::string currency) { std::cout << "Processing payment: " << amount << " " << currency << std::endl; } void setAge(int age) { std::cout << "Age set to: " << age << std::endl; } };
|
问题分析:
std::string 用于表示邮箱、电话号码、货币,但没有验证
int 用于表示年龄,但没有范围检查
- 验证逻辑分散在多个地方,难以维护
重构后(清洁版本)
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
| class Email { private: std::string value; bool isValid(const std::string& email) { return email.find('@') != std::string::npos; } public: Email(const std::string& email) { if (!isValid(email)) { throw std::invalid_argument("Invalid email address"); } value = email; } std::string getValue() const { return value; } };
class PhoneNumber { private: std::string value; bool isValid(const std::string& phone) { for (char c : phone) { if (!std::isdigit(c) && c != '-') { return false; } } return phone.length() >= 10; } public: PhoneNumber(const std::string& phone) { if (!isValid(phone)) { throw std::invalid_argument("Invalid phone number"); } value = phone; } std::string getValue() const { return value; } };
class Money { private: double amount; std::string currency; public: Money(double amt, const std::string& curr) : amount(amt), currency(curr) { if (amount < 0) { throw std::invalid_argument("Amount cannot be negative"); } } double getAmount() const { return amount; } std::string getCurrency() const { return currency; } std::string toString() const { return std::to_string(amount) + " " + currency; } };
class Age { private: int value; public: Age(int age) { if (age < 0 || age > 150) { throw std::invalid_argument("Invalid age"); } value = age; } int getValue() const { return value; } };
class GoodExample { public: void sendEmail(const Email& email, const std::string& subject, const std::string& body) { std::cout << "Sending email to: " << email.getValue() << std::endl; } void callPhone(const PhoneNumber& phone) { std::cout << "Calling: " << phone.getValue() << std::endl; } void processPayment(const Money& money) { std::cout << "Processing payment: " << money.toString() << std::endl; } void setAge(const Age& age) { std::cout << "Age set to: " << age.getValue() << std::endl; } };
|
关键变化点:
用对象替换数据值(Replace Data Value with Object):
- 将基本类型替换为有意义的类型
- 每个类型都有自己的验证逻辑
类型安全:
提高可维护性:
- 验证逻辑集中在类型定义中
- 修改验证规则只需修改类型定义
步骤3:重构技巧总结
使用的重构手法:
- 用对象替换数据值(Replace Data Value with Object):将基本类型替换为有意义的类型
- 封装字段(Encapsulate Field):将字段封装为私有,通过方法访问
注意事项:
- ⚠️ 确保类型有清晰的语义
- ⚠️ 如果类型只在局部使用,考虑使用局部类
- ⚠️ 重构后要更新所有使用处,确保行为一致
6. 预防策略
🛡️ 编码时:
即时检查:
- 基本类型是否有特殊含义?
- 基本类型是否需要验证?
- 是否可以创建有意义的类型?
小步提交:
- 发现基本类型偏执时,立即创建有意义的类型
- 使用”用对象替换数据值”重构,保持类型有意义
🔍 Code Review清单:
重点检查:
- 基本类型是否表示有意义的领域概念?
- 基本类型是否需要验证?
- 是否可以创建有意义的类型?
拒绝标准:
- 使用基本类型表示有意义的领域概念
- 基本类型需要验证但没有类型层面的保护
- 验证逻辑分散在多个地方
⚙️ 自动化防护:
IDE配置:
- 使用代码分析工具检测基本类型的使用模式
- 启用类型安全警告
CI/CD集成:
- 在CI流水线中集成代码分析工具
- 检测基本类型使用模式,生成警告报告
下一篇预告:重复的switch(Repeated Switches)- 如何用多态替代重复的switch语句