user类中出现save()是危险信号,因其混淆业务逻辑与持久化职责,导致数据库更换、审计日志等需求需修改所有业务类,破坏单一职责原则,且引发测试困难、线程安全及析构风险。

不该写进业务对象,保存逻辑必须和业务逻辑解耦。
为什么 save() 方法出现在 User 类里是危险信号
当 User 类里出现 save()、load() 或直接调用 std::ofstream / sqlite3_exec() 时,说明职责已混淆。业务对象该只回答“它是什么”“它能做什么计算”,而不是“它存在哪”“怎么写进磁盘”。一旦数据库换 SQLite 为 PostgreSQL,或要加审计日志、缓存失效、事务包装,就得改所有业务类——这不是扩展,是污染。
常见错误现象:User::save() 里硬编码了 JSON 序列化路径;Order::load(int id) 直接 new 一个 DatabaseConnection;单元测试时不得不 mock 文件系统或数据库连接。
- 业务对象应保持无状态、无依赖、可自由拷贝(比如放进
std::vector<user></user>) - 保存行为天然带副作用(IO、异常、并发),和纯业务方法语义冲突
- C++ 没有 GC,若
save()在析构函数里自动触发,极易引发未定义行为(如静态对象析构顺序问题)
推荐结构:Repository 模式 + 显式序列化层
把持久化交给独立组件,比如 UserRepository,它持有数据库连接或文件句柄,只暴露 save(const User&)、find_by_id(int) 等接口。业务对象 User 保持 POD 或简单结构体,甚至用 struct 更合适。
序列化本身也该剥离:不建议 User 自己实现 to_json(),而用专用工具如 nlohmann::json::to_json(json&, const User&)(非成员函数),或 boost::serialization 的 serialize() 模板特化。
-
User不含任何#include <fstream></fstream>或数据库头文件 -
UserRepository可以用 RAII 管理连接,支持事务:TransactionScope构造时 begin,析构时 commit/rollback - 测试时可注入 mock repository,完全绕过 IO
什么情况下可以破例?
极小项目、POC 或嵌入式资源受限场景(比如单片机上只有 EEPROM,且 User 就 3 个字段),可接受 User::write_to_eeprom(uint8_t addr) 这种直白方法——但必须满足两个条件:没有其他持久化需求,且 User 不会被传给任何通用容器或算法(避免意外拷贝触发重复写入)。
即便如此,也要把地址偏移、校验逻辑抽成常量或配置项,别让 write_to_eeprom(0x20) 散落在多个地方。
- 破例不是设计选择,是权衡后的临时妥协
- 一旦出现第二个实体(如
DeviceConfig)也要存 EEPROM,就必须立刻提取公共基类或工具函数 - 注意 C++20
[[no_unique_address]]对空基类优化无效于含虚函数的“伪 repository”混入类
真正难的不是写 save(),而是判断哪一层该承担副作用。业务对象越干净,后续加日志、监控、分布式同步就越少踩坑——尤其在多线程环境下,save() 的隐式锁或内存屏障容易被忽略。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











