可变数据(Mutable Data):坏味道识别与重构实战指南
可变数据(Mutable Data):坏味道识别与重构实战指南
24种代码坏味道系列 · 第6篇
1. 开篇场景
你是否遇到过这样的代码:类的公共字段 balance 可以被任何地方直接修改,导致账户余额可能变成负数,或者被意外设置为异常值?
1 | class BadExample { |
这就是可变数据的典型症状。数据没有受到保护,可以在任何地方被修改,就像银行金库没有锁,任何人都可以随意存取现金。
当你需要确保数据的一致性时(如余额不能为负数),公共可变数据会让你无法控制。更糟糕的是,当bug出现时,你很难追踪是哪里修改了数据,因为修改可能发生在代码的任何地方。
2. 坏味道定义
可变数据是指数据可以被意外修改,没有受到适当的保护,导致不可预测的行为。
就像一个没有锁的保险箱,虽然方便存取,但也容易被误操作或恶意修改。
核心问题:数据应该通过受控的方式访问和修改,而不是直接暴露。公共可变数据破坏了封装性,增加了bug的风险。
3. 识别特征
🔍 代码表现:
- 特征1:类的公共字段(非
const) - 特征2:数据修改没有验证逻辑
- 特征3:数据可以在类外部直接访问和修改
- 特征4:没有提供受控的访问方法(getter/setter)
- 特征5:数据的不变性无法保证
🎯 出现场景:
- 场景1:快速开发时,使用公共字段简化代码
- 场景2:从C语言迁移到C++时,保留了结构体的习惯
- 场景3:数据类(Data Class)没有封装
- 场景4:配置信息使用公共字段存储
💡 快速自检:
- 问自己:这个数据是否应该被外部直接修改?
- 问自己:修改这个数据是否需要验证?
- 工具提示:使用静态分析工具检测公共字段的使用
4. 危害分析
🚨 维护成本:追踪数据修改位置需要额外40%的时间
⚠️ 缺陷风险:数据被意外修改导致bug,风险增加70%
🧱 扩展障碍:添加验证逻辑时需要修改所有访问处
🤯 认知负担:需要理解所有可能修改数据的地方
5. 重构实战
步骤1:安全准备
- ✅ 确保有完整的单元测试覆盖
- ✅ 创建重构分支:
git checkout -b refactor/encapsulate-mutable-data - ✅ 使用版本控制,便于回滚
步骤2:逐步重构
重构前(问题代码)
1 | class BadExample { |
问题分析:
balance和accountNumber是公共字段,可以直接修改withdraw和deposit没有验证,可能导致数据不一致- 无法追踪数据修改的来源
重构后(清洁版本)
1 | class GoodExample { |
关键变化点:
封装字段(Encapsulate Field):
- 将公共字段改为私有字段
- 通过方法控制对数据的访问
添加验证:
withdraw和deposit方法添加了验证逻辑- 确保数据的一致性和有效性
提供受控访问:
- 提供
getBalance()和getAccountNumber()方法 - 数据只能通过受控的方式访问
- 提供
步骤3:重构技巧总结
使用的重构手法:
- 封装字段(Encapsulate Field):将公共字段封装为私有字段
- 提取方法(Extract Method):将验证逻辑提取为独立方法
注意事项:
- ⚠️ 如果字段是配置信息,考虑使用
const或constexpr - ⚠️ 如果字段是只读的,提供
const访问方法 - ⚠️ 重构后要更新所有直接访问字段的代码
6. 预防策略
🛡️ 编码时:
即时检查:
- 避免使用公共字段(非
const) - 使用私有字段和访问方法
- 在修改方法中添加验证逻辑
- 避免使用公共字段(非
小步提交:
- 发现公共字段时,立即封装
- 使用”封装字段”重构,保持数据受保护
🔍 Code Review清单:
重点检查:
- 是否有公共字段(非
const) - 数据修改是否有验证
- 是否提供了受控的访问方法
- 是否有公共字段(非
拒绝标准:
- 非
const的公共字段 - 没有验证的数据修改
- 直接访问私有字段的代码
- 非
⚙️ 自动化防护:
IDE配置:
- 启用公共字段警告
- 使用静态分析工具检测数据访问
CI/CD集成:
- 在CI流水线中集成静态分析工具
- 检测公共字段使用,生成警告报告
下一篇预告:发散式变化(Divergent Change)- 如何让类只因为一个原因而改变
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 青羽川!
评论
