数据泥团(Data Clumps):坏味道识别与重构实战指南

数据泥团(Data Clumps):坏味道识别与重构实战指南

24种代码坏味道系列 · 第10篇


1. 开篇场景

你是否遇到过这样的代码:firstNamelastName 总是成对出现,streetcityzipCode 也总是同时传递,但它们却作为独立的参数在多个函数间传递?

cpp 复制代码
void createUser(std::string firstName, std::string lastName, 
               std::string street, std::string city, 
               std::string zipCode) {
    // firstName, lastName 总是成对出现
    // street, city, zipCode 总是成对出现
}

void updateUser(std::string firstName, std::string lastName, 
               std::string street, std::string city, 
               std::string zipCode) {
    // 同样的数据组合又出现了
}

这就是数据泥团的典型症状。总是同时出现的数据应该组合成一个对象,就像总是成对出现的袜子,应该放在一起,而不是散落各处。

当你需要添加新字段时(如添加"国家"字段),你必须在所有使用这些数据的地方修改。更糟糕的是,这些数据之间的关系不明确,容易传错参数顺序。


2. 坏味道定义

数据泥团是指总是同时出现的数据应该组合成一个对象,而不是作为独立的参数传递。

就像总是成对出现的物品,应该放在一起管理,而不是分开存放。

核心问题:如果多个数据总是同时出现,说明它们之间存在某种关联,应该组合成对象。这样可以减少参数数量,提高代码的可读性和可维护性。


3. 识别特征

🔍 代码表现:

  • 特征1 :多个参数总是同时出现(如 firstNamelastName
  • 特征2:相同的参数组合在多个函数中重复出现
  • 特征3:参数之间存在逻辑关联(如地址相关的多个字段)
  • 特征4:函数参数列表很长,包含多个相关参数
  • 特征5:删除某个参数时,其他参数也变得无意义

🎯 出现场景:

  • 场景1:快速开发时,将相关数据作为独立参数传递
  • 场景2:从过程式编程迁移到面向对象时,没有重构参数
  • 场景3:缺乏设计,没有考虑数据的关联性
  • 场景4:重构不彻底,只修改了部分代码

💡 快速自检:

  • 问自己:这些参数是否总是同时出现?
  • 问自己:这些参数是否可以组合成有意义的对象?
  • 工具提示:使用代码分析工具检测参数组合模式

4. 危害分析

🚨 维护成本:添加新字段需要在多个函数中修改,时间成本增加50%

⚠️ 缺陷风险:参数顺序错误、类型混淆等bug增加40%

🧱 扩展障碍:添加新功能时需要修改多个函数的参数列表

🤯 认知负担:需要记住参数之间的关系,增加了心理负担


5. 重构实战

步骤1:安全准备

  • ✅ 确保有完整的单元测试覆盖
  • ✅ 创建重构分支:git checkout -b refactor/introduce-data-object
  • ✅ 使用版本控制,便于回滚

步骤2:逐步重构

重构前(问题代码)
cpp 复制代码
// 坏味道:这些数据总是同时出现,但没有组合在一起
class BadExample {
public:
    void createUser(std::string firstName, std::string lastName, 
                   std::string street, std::string city, 
                   std::string zipCode) {
        // firstName, lastName 总是成对出现
        // street, city, zipCode 总是成对出现
        std::cout << "Creating user: " << firstName << " " << lastName 
                  << " at " << street << ", " << city << " " << zipCode << std::endl;
    }
    
    void updateUser(std::string firstName, std::string lastName, 
                   std::string street, std::string city, 
                   std::string zipCode) {
        // 同样的数据组合又出现了
        std::cout << "Updating user: " << firstName << " " << lastName 
                  << " at " << street << ", " << city << " " << zipCode << std::endl;
    }
    
    void sendMail(std::string firstName, std::string lastName, 
                 std::string street, std::string city, 
                 std::string zipCode) {
        // 同样的数据组合再次出现
        std::cout << "Sending mail to: " << firstName << " " << lastName 
                  << " at " << street << ", " << city << " " << zipCode << std::endl;
    }
};

问题分析

  • firstNamelastName 总是成对出现,应该组合成 Name 对象
  • streetcityzipCode 总是成对出现,应该组合成 Address 对象
  • 相同的参数组合在多个函数中重复出现
重构后(清洁版本)
cpp 复制代码
// ✅ 将相关数据组合成对象
struct Name {
    std::string firstName;
    std::string lastName;
    
    Name(const std::string& first, const std::string& last) 
        : firstName(first), lastName(last) {}
    
    std::string getFullName() const {
        return firstName + " " + lastName;
    }
};

struct Address {
    std::string street;
    std::string city;
    std::string zipCode;
    
    Address(const std::string& st, const std::string& ct, const std::string& zip) 
        : street(st), city(ct), zipCode(zip) {}
    
    std::string getFullAddress() const {
        return street + ", " + city + " " + zipCode;
    }
};

class GoodExample {
public:
    // ✅ 使用对象而不是多个参数
    void createUser(const Name& name, const Address& address) {
        std::cout << "Creating user: " << name.getFullName() 
                  << " at " << address.getFullAddress() << std::endl;
    }
    
    void updateUser(const Name& name, const Address& address) {
        std::cout << "Updating user: " << name.getFullName() 
                  << " at " << address.getFullAddress() << std::endl;
    }
    
    void sendMail(const Name& name, const Address& address) {
        std::cout << "Sending mail to: " << name.getFullName() 
                  << " at " << address.getFullAddress() << std::endl;
    }
};

关键变化点

  1. 引入数据对象(Introduce Data Object)

    • firstNamelastName 组合成 Name 对象
    • streetcityzipCode 组合成 Address 对象
  2. 简化函数签名

    • 函数参数从5个减少到2个
    • 参数语义更清晰
  3. 提高可维护性

    • 添加新字段只需修改对象定义
    • 不需要修改所有函数的参数列表

步骤3:重构技巧总结

使用的重构手法

  • 引入数据对象(Introduce Data Object):将相关数据组合成对象
  • 引入参数对象(Introduce Parameter Object):将参数组合成对象

注意事项

  • ⚠️ 确保数据对象有清晰的语义
  • ⚠️ 如果数据对象只在函数内部使用,考虑使用局部结构体
  • ⚠️ 重构后要更新所有调用处,确保行为一致

6. 预防策略

🛡️ 编码时:

  • 即时检查

    • 多个参数是否总是同时出现?
    • 这些参数是否可以组合成对象?
    • 使用IDE的代码提示,如果参数提示难以阅读,考虑重构
  • 小步提交

    • 发现数据泥团时,立即组合成对象
    • 使用"引入数据对象"重构,保持参数简洁

🔍 Code Review清单:

  • 重点检查

    • 函数参数是否包含总是同时出现的数据?
    • 这些数据是否可以组合成对象?
    • 参数列表是否过长?
  • 拒绝标准

    • 多个相关参数作为独立参数传递
    • 相同的参数组合在多个函数中重复
    • 参数列表超过5个

⚙️ 自动化防护:

  • IDE配置

    • 使用代码分析工具检测参数组合模式
    • 启用参数数量警告
  • CI/CD集成

    • 在CI流水线中集成代码分析工具
    • 检测参数组合模式,生成警告报告

下一篇预告:基本类型偏执(Primitive Obsession)- 如何用有意义的类型替代基本类型

相关推荐
疯狂的挖掘机17 小时前
CT图像重构调研快速预览
重构
Justice Young21 小时前
软件工程第九章、第十章:软件维护、软件重构、软件复用
重构·软件工程
MARS_AI_21 小时前
融资加持下的云蝠智能:大模型语音Agent重构企业通信新生态
人工智能·自然语言处理·重构·交互·信息与通信·agi
Sahadev_21 小时前
从逻辑表达式到原子化构建:复杂 UI 组件的重构之道
ui·重构
cute_ming1 天前
关于基于nodeMap重构DOM的最佳实践
java·javascript·重构
Jouham2 天前
瞬维智能丨行业深度:AI智能重构B2B高客单价领域获客全链路实践
人工智能·重构
x新观点2 天前
2026亚马逊广告AI工具推荐:破解流量博弈困局,重构投放效率
人工智能·重构
趣味科技v2 天前
全维服务重构汽车消费体验:比亚迪方程豹4S店探店实录
重构·汽车
笙枫2 天前
2023-2025年时间序列预测前沿全景报告:从线性反思到十亿级基础模型的范式重构
重构