基本类型偏执(Primitive Obsession):坏味道识别与重构实战指南

基本类型偏执(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;
}

这就是基本类型偏执 的典型症状。过度使用基本类型(如 intstringdouble)表示有意义的领域概念,就像用数字"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;
    }
};

关键变化点

  1. 用对象替换数据值(Replace Data Value with Object)

    • 将基本类型替换为有意义的类型
    • 每个类型都有自己的验证逻辑
  2. 类型安全

    • 类型层面保证数据的有效性
    • 无效数据无法创建对象
  3. 提高可维护性

    • 验证逻辑集中在类型定义中
    • 修改验证规则只需修改类型定义

步骤3:重构技巧总结

使用的重构手法

  • 用对象替换数据值(Replace Data Value with Object):将基本类型替换为有意义的类型
  • 封装字段(Encapsulate Field):将字段封装为私有,通过方法访问

注意事项

  • ⚠️ 确保类型有清晰的语义
  • ⚠️ 如果类型只在局部使用,考虑使用局部类
  • ⚠️ 重构后要更新所有使用处,确保行为一致

6. 预防策略

🛡️ 编码时:

  • 即时检查

    • 基本类型是否有特殊含义?
    • 基本类型是否需要验证?
    • 是否可以创建有意义的类型?
  • 小步提交

    • 发现基本类型偏执时,立即创建有意义的类型
    • 使用"用对象替换数据值"重构,保持类型有意义

🔍 Code Review清单:

  • 重点检查

    • 基本类型是否表示有意义的领域概念?
    • 基本类型是否需要验证?
    • 是否可以创建有意义的类型?
  • 拒绝标准

    • 使用基本类型表示有意义的领域概念
    • 基本类型需要验证但没有类型层面的保护
    • 验证逻辑分散在多个地方

⚙️ 自动化防护:

  • IDE配置

    • 使用代码分析工具检测基本类型的使用模式
    • 启用类型安全警告
  • CI/CD集成

    • 在CI流水线中集成代码分析工具
    • 检测基本类型使用模式,生成警告报告

下一篇预告:重复的switch(Repeated Switches)- 如何用多态替代重复的switch语句

相关推荐
九河云16 小时前
从“被动适配”到“主动重构”:企业数字化转型的底层逻辑
大数据·人工智能·安全·重构·数字化转型
Jacen.L1 天前
数据泥团(Data Clumps):坏味道识别与重构实战指南
重构
疯狂的挖掘机1 天前
CT图像重构调研快速预览
重构
Justice Young1 天前
软件工程第九章、第十章:软件维护、软件重构、软件复用
重构·软件工程
MARS_AI_1 天前
融资加持下的云蝠智能:大模型语音Agent重构企业通信新生态
人工智能·自然语言处理·重构·交互·信息与通信·agi
Sahadev_1 天前
从逻辑表达式到原子化构建:复杂 UI 组件的重构之道
ui·重构
cute_ming2 天前
关于基于nodeMap重构DOM的最佳实践
java·javascript·重构
Jouham2 天前
瞬维智能丨行业深度:AI智能重构B2B高客单价领域获客全链路实践
人工智能·重构
x新观点2 天前
2026亚马逊广告AI工具推荐:破解流量博弈困局,重构投放效率
人工智能·重构