纯数据类(Data Class):坏味道识别与重构实战指南
24种代码坏味道系列 · 第22篇
1. 开篇场景
你是否遇到过这样的类:它只有数据字段和简单的getter/setter,所有业务逻辑都在使用这个类的地方,就像一个”空壳”,没有自己的行为?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| class BadPerson { public: std::string firstName; std::string lastName; int age; std::string email; };
void processPerson(BadPerson& person) { std::string fullName = person.firstName + " " + person.lastName; if (person.age < 0 || person.age > 150) { } }
|
这就是纯数据类的典型症状。类只有数据字段和简单的getter/setter,没有行为,就像一个”空壳”,所有业务逻辑都在使用这个类的地方。
当你需要修改业务逻辑时,你必须在多个使用这个类的地方修改。当你需要添加新功能时,你不知道应该放在哪里。这种设计使得代码变得分散,增加了维护的难度。
2. 坏味道定义
纯数据类是指类只有数据字段和简单的getter/setter,没有行为。
就像一个空壳,没有自己的行为,所有逻辑都在外部。
核心问题:数据和行为应该在一起。如果类只有数据没有行为,应该将相关的行为移到类中,提高内聚性。
3. 识别特征
🔍 代码表现:
- 特征1:类只有数据字段和getter/setter
- 特征2:类的业务逻辑在使用类的地方
- 特征3:类没有验证逻辑
- 特征4:类没有计算方法
- 特征5:删除类后,逻辑不受影响
🎯 出现场景:
- 场景1:快速开发时,只创建数据类
- 场景2:从其他语言迁移时,保留了数据类的习惯
- 场景3:缺乏设计,没有考虑行为的位置
- 场景4:重构不彻底,只创建了数据类
💡 快速自检:
- 问自己:这个类是否有行为?
- 问自己:相关的业务逻辑是否应该在这个类中?
- 工具提示:使用代码分析工具检测纯数据类
4. 危害分析
🚨 维护成本:修改业务逻辑需要在多个地方修改,时间成本增加50%
⚠️ 缺陷风险:业务逻辑分散,bug风险增加40%
🧱 扩展障碍:添加新功能时不知道应该放在哪里
🤯 认知负担:需要理解数据和行为的关系,增加了心理负担
5. 重构实战
步骤1:安全准备
- ✅ 确保有完整的单元测试覆盖
- ✅ 创建重构分支:
git checkout -b refactor/move-behavior-to-data-class
- ✅ 使用版本控制,便于回滚
步骤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
| class BadPerson { public: std::string firstName; std::string lastName; int age; std::string email; BadPerson() : age(0) {} BadPerson(const std::string& first, const std::string& last, int a, const std::string& em) : firstName(first), lastName(last), age(a), email(em) {} };
class BadExample { public: void processPerson(BadPerson& person) { std::string fullName = person.firstName + " " + person.lastName; if (person.age < 0 || person.age > 150) { std::cout << "Invalid age for " << fullName << std::endl; return; } if (person.email.find('@') == std::string::npos) { std::cout << "Invalid email for " << fullName << std::endl; return; } std::cout << "Processing: " << fullName << ", Age: " << person.age << ", Email: " << person.email << std::endl; } bool isAdult(BadPerson& person) { return person.age >= 18; } std::string getFullName(BadPerson& person) { return person.firstName + " " + person.lastName; } };
|
问题分析:
BadPerson 只有数据字段,没有行为
- 所有业务逻辑都在
BadExample 中
- 数据和行为分离,内聚性低
重构后(清洁版本)
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
| class GoodPerson { private: std::string firstName; std::string lastName; int age; std::string email; bool isValidEmail(const std::string& em) const { return em.find('@') != std::string::npos; } bool isValidAge(int a) const { return a >= 0 && a <= 150; } public: GoodPerson() : age(0) {} GoodPerson(const std::string& first, const std::string& last, int a, const std::string& em) : firstName(first), lastName(last), age(a), email(em) { if (!isValidAge(a)) { throw std::invalid_argument("Invalid age"); } if (!isValidEmail(em)) { throw std::invalid_argument("Invalid email"); } } std::string getFullName() const { return firstName + " " + lastName; } bool isAdult() const { return age >= 18; } bool isValid() const { return isValidAge(age) && isValidEmail(email) && !firstName.empty() && !lastName.empty(); } void display() const { std::cout << "Name: " << getFullName() << ", Age: " << age << ", Email: " << email << std::endl; if (isAdult()) { std::cout << "This person is an adult." << std::endl; } } std::string getFirstName() const { return firstName; } std::string getLastName() const { return lastName; } int getAge() const { return age; } std::string getEmail() const { return email; } void setAge(int a) { if (!isValidAge(a)) { throw std::invalid_argument("Invalid age"); } age = a; } void setEmail(const std::string& em) { if (!isValidEmail(em)) { throw std::invalid_argument("Invalid email"); } email = em; } };
class GoodExample { public: void processPerson(GoodPerson& person) { if (!person.isValid()) { std::cout << "Invalid person data" << std::endl; return; } person.display(); } };
|
关键变化点:
移动方法(Move Method):
- 将业务逻辑移到
Person 类中
- 数据和行为在一起
添加验证:
- 在构造函数和setter中添加验证
- 确保数据的一致性
提高内聚性:
步骤3:重构技巧总结
使用的重构手法:
- 移动方法(Move Method):将业务逻辑移到数据类中
- 封装字段(Encapsulate Field):将字段封装为私有,通过方法访问
注意事项:
- ⚠️ 确保行为确实属于数据类
- ⚠️ 如果行为涉及多个类,考虑使用服务类
- ⚠️ 重构后要更新所有使用处,确保行为一致
6. 预防策略
🛡️ 编码时:
即时检查:
- 类是否有行为?
- 相关的业务逻辑是否应该在这个类中?
- 使用IDE的代码分析工具,检测纯数据类
小步提交:
- 发现纯数据类时,立即添加行为
- 使用”移动方法”重构,保持数据和行为在一起
🔍 Code Review清单:
重点检查:
- 是否有纯数据类?
- 相关的业务逻辑是否应该在这个类中?
- 是否可以添加行为方法?
拒绝标准:
- 只有数据字段和getter/setter的类
- 业务逻辑在使用类的地方
- 类没有验证逻辑
⚙️ 自动化防护:
IDE配置:
CI/CD集成:
- 在CI流水线中集成代码分析工具
- 检测纯数据类,生成警告报告
下一篇预告:被拒绝的遗赠(Refused Bequest)- 如何解决子类拒绝继承的问题