如何扩展数据库中存储的Person类且最小化系统改动?
我正在开发一个简易数据库,从CSV文件读取Person类字段并写入vector中。但未来如果要给Person类新增字段,就得重写类构造函数,同时CSV读写函数也得对应修改。想找一种能以最小代码改动为Person类添加字段的方法。
我自己想到的方案是用map替代固定字段,存储<字段名, 值>键值对,读取CSV时调用setter而非构造函数。请问还有其他可行方案吗?
Person类代码
person::person(unsigned int _id, std::string &_fullname, std::string &_occupation) : id(_id), fullname(_fullname), occupation(_occupation) { } unsigned int person::getId() const { return id; } const std::string &person::getFullname() const { return fullname; } const std::string &person::getOccupation() const { return occupation; } void person::setOccupation(std::string &_occupation) { person::occupation = _occupation; }
person_csv_converter类代码
person_csv_converter::person_csv_converter(const std::string &_csvFilename) { in.open(_csvFilename); if (in.is_open()) { std::cout << "CSV file was found!"; } else { throw file_not_found_exception("File was not found or file type is incorrect!"); } } person_csv_converter::~person_csv_converter() { in.close(); } std::vector<std::string> person_csv_converter::readLines() { std::string line; std::vector<std::string> lines; while (std::getline(in, line)) { lines.push_back(line); } return lines; } std::vector<person> person_csv_converter::convert(const std::vector<std::string> &_csvLines) { std::vector<person> persons; //pseudo code: /* * for i in range(_csvLines.size()) { * vector.append(new person(_csvLines.split(","))) * } */ }
1. 你提到的map键值对方案
这种方案能直接减少字段新增时的改动:Person类改用std::map<std::string, std::string>存储字段,CSV读取时只需按表头键名存入map,不需要修改构造函数。但缺点是失去强类型检查,访问字段依赖字符串键名容易出错,且性能比直接访问成员变量差。
2. 手动模拟反射机制
C++原生无反射,但可以手动给Person类添加字段元数据,让CSV转换器通过遍历元数据完成字段读写。新增字段时仅需更新元数据,无需修改构造函数和转换器核心逻辑。
示例实现思路
// 字段类型枚举 enum class FieldType { UINT, STRING }; // 字段描述结构体,包含名称、类型、赋值逻辑 struct FieldDesc { std::string name; FieldType type; std::function<void(person&, const std::string&)> setter; }; class person { private: unsigned int id; std::string fullname; std::string occupation; public: // 使用默认构造函数,新增字段无需修改 person() = default; // 静态字段元数据列表 static const std::vector<FieldDesc> fields; // 各字段的setter void setId(unsigned int val) { id = val; } void setFullname(const std::string& val) { fullname = val; } void setOccupation(const std::string& val) { occupation = val; } // getter保持不变 unsigned int getId() const { return id; } const std::string& getFullname() const { return fullname; } const std::string& getOccupation() const { return occupation; } }; // 初始化字段元数据 const std::vector<FieldDesc> person::fields = { {"id", FieldType::UINT, [](person& p, const std::string& val) { p.setId(std::stoul(val)); }}, {"fullname", FieldType::STRING, [](person& p, const std::string& val) { p.setFullname(val); }}, {"occupation", FieldType::STRING, [](person& p, const std::string& val) { p.setOccupation(val); }} };
转换器修改后的convert函数
// 先实现一个字符串分割函数 std::vector<std::string> split(const std::string& s, char delim) { std::vector<std::string> tokens; std::string token; std::istringstream tokenStream(s); while (std::getline(tokenStream, token, delim)) { tokens.push_back(token); } return tokens; } std::vector<person> person_csv_converter::convert(const std::vector<std::string> &_csvLines) { std::vector<person> persons; if (_csvLines.empty()) return persons; // 解析表头 std::vector<std::string> headers = split(_csvLines[0], ","); for (size_t i = 1; i < _csvLines.size(); ++i) { std::vector<std::string> values = split(_csvLines[i], ","); person p; // 根据表头匹配字段并赋值 for (size_t j = 0; j < headers.size(); ++j) { auto it = std::find_if(person::fields.begin(), person::fields.end(), [&headers, j](const FieldDesc& fd) { return fd.name == headers[j]; }); if (it != person::fields.end() && j < values.size()) { it->setter(p, values[j]); } } persons.push_back(p); } return persons; }
新增字段时,仅需在Person类中添加成员变量、setter,并在person::fields中新增一条字段描述即可,转换器代码完全无需修改。
3. 使用第三方序列化/CSV库
比如Boost.Serialization、nlohmann/json(可配合CSV解析)、FastCSV等,这类库通过宏或注解标记类字段,自动处理序列化/反序列化逻辑。新增字段时仅需添加对应标记,无需手动修改读写代码。
以nlohmann/json配合CSV为例:
// 给Person类添加JSON序列化支持 namespace nlohmann { template<> struct adl_serializer<person> { static void to_json(json& j, const person& p) { j = json{{"id", p.getId()}, {"fullname", p.getFullname()}, {"occupation", p.getOccupation()}}; } static void from_json(const json& j, person& p) { p.setId(j.at("id").get<unsigned int>()); p.setFullname(j.at("fullname").get<std::string>()); p.setOccupation(j.at("occupation").get<std::string>()); } }; }
CSV读取时,先将每行数据按表头映射为JSON对象,再通过json.get<person>()完成对象转换。新增字段仅需修改to_json和from_json函数。
4. C++元编程字段映射
利用C++11+的元组、可变参数模板特性,将Person的字段打包为元组,通过编译期遍历元组完成CSV读写。这种方案无运行时开销,新增字段仅需修改元组定义。
示例思路
class person { public: unsigned int id; std::string fullname; std::string occupation; // 将字段打包为元组,供元编程遍历 auto as_tuple() { return std::tie(id, fullname, occupation); } auto as_tuple() const { return std::tie(id, fullname, occupation); } }; // 表头与字段的映射 const std::vector<std::string> person_field_names = {"id", "fullname", "occupation"};
编写通用模板函数遍历元组完成赋值,新增字段时仅需修改as_tuple和person_field_names,转换器模板函数无需改动。
- 无第三方库且追求最小改动:手动模拟反射方案最优
- 允许引入第三方库:序列化/CSV库能大幅减少重复代码
- 性能敏感场景:元编程字段映射方案更适合
- 追求极致灵活:你提到的
map方案可选,但需接受类型安全和性能损失
内容的提问来源于stack exchange,提问作者nokkov

