基本类型偏执(Primitive Obsession):坏味道识别与重构实战指南
24种代码坏味道系列 · 第11篇
1. 开篇场景
你是否遇到过这样的代码:使用 std::string 表示邮箱地址,但没有验证;使用 std::string 表示电话号码,但没有验证;使用 int 表示年龄,但没有范围检查?
cpp
void sendEmail(std::string email, std::string subject, std::string body) {
// 使用 string 表示邮箱,但没有验证
std::cout << "Sending email to: " << email << std::endl;
}
void setAge(int age) {
// 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:逐步重构
重构前(问题代码)
cpp
// 坏味道:使用基本类型表示有意义的领域概念
class BadExample {
public:
// 使用 string 表示邮箱,但没有验证
void sendEmail(std::string email, std::string subject, std::string body) {
std::cout << "Sending email to: " << email << std::endl;
}
// 使用 string 表示电话号码,但没有验证
void callPhone(std::string phoneNumber) {
std::cout << "Calling: " << phoneNumber << std::endl;
}
// 使用 string 表示货币,容易出错
void processPayment(std::string amount, std::string currency) {
std::cout << "Processing payment: " << amount << " " << currency << std::endl;
}
// 使用 int 表示年龄,但没有范围检查
void setAge(int age) {
// age 可能是负数或很大的数
std::cout << "Age set to: " << age << std::endl;
}
};
问题分析:
std::string用于表示邮箱、电话号码、货币,但没有验证int用于表示年龄,但没有范围检查- 验证逻辑分散在多个地方,难以维护
重构后(清洁版本)
cpp
// ✅ 创建有意义的类型
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语句