通用序列化接口的设计模式选型:面向DAG通用对象库的自定义序列化实现方案咨询
你的现有方案确实存在明显的设计瑕疵——空的Serialized基类本质是一个无意义的标记接口,强制用户做向下转型更是违反了面向对象的封装原则,而且用户需要编写的样板代码太多,使用门槛很高。接下来我会给你几个更优雅的替代方案,覆盖不同的使用场景:
这个方案的核心是把序列化逻辑完全从Node类中剥离,定义一个与具体序列化库无关的抽象策略接口,让用户实现这个接口来适配他们喜欢的序列化工具。
首先定义抽象的DagSerializer接口:
class Node; // 前置声明 class DagSerializer { public: virtual ~DagSerializer() = default; // 序列化单个Node的核心方法,用户需实现如何将Node信息写入序列化结构 virtual void serialize_node(const Node& node) = 0; // 处理Node之间的关联(父/子关系) virtual void serialize_edge(const Node& parent, const Node& child) = 0; };
然后修改Node类,提供遍历DAG并调用序列化策略的方法:
class Node { public: // 保留原有的深度优先遍历访问者方法 template <typename Visitor> void accept(Visitor& v) const { v.visit(*this); for(auto& child_weak : children) { if(auto locked_child = child_weak.lock()) { locked_child->accept(v); } } } // 新增:接受序列化策略的入口方法 void serialize(DagSerializer& serializer) const { struct SerializationVisitor { DagSerializer& serializer; std::unordered_set<const Node*> visited; // 防止DAG循环引用重复序列化 void visit(const Node& node) { if(visited.count(&node)) return; visited.insert(&node); serializer.serialize_node(node); // 序列化父节点关联 for(const auto& parent : node.parents) { serializer.serialize_edge(*parent, node); } // 序列化子节点关联 for(const auto& child_weak : node.children) { if(auto child = child_weak.lock()) { serializer.serialize_edge(node, *child); } } } }; SerializationVisitor visitor{serializer}; accept(visitor); } protected: virtual ~Node(){} private: std::vector<std::weak_ptr<Node>> children; std::vector<std::shared_ptr<Node>> parents; };
用户只需实现DagSerializer适配目标序列化库,比如yaml-cpp:
#include <yaml-cpp/yaml.h> class YamlDagSerializer : public DagSerializer { public: YamlDagSerializer(YAML::Node& root) : root_(root) { root_["nodes"] = YAML::Node(YAML::NodeType::Sequence); root_["edges"] = YAML::Node(YAML::NodeType::Sequence); } void serialize_node(const Node& node) override { // 用dynamic_cast访问派生类成员,或给Node加自定义类型标识优化 if(const auto* my_node = dynamic_cast<const MyNode<int>*>(&node)) { YAML::Node yaml_node; yaml_node["id"] = reinterpret_cast<uintptr_t>(&node); // 用地址做唯一标识 yaml_node["value"] = my_node->value; root_["nodes"].push_back(yaml_node); } } void serialize_edge(const Node& parent, const Node& child) override { YAML::Node edge; edge["parent_id"] = reinterpret_cast<uintptr_t>(&parent); edge["child_id"] = reinterpret_cast<uintptr_t>(&child); root_["edges"].push_back(edge); } private: YAML::Node& root_; };
使用方式非常简洁:
YAML::Node yaml_root; YamlDagSerializer serializer(yaml_root); my_dag_root->serialize(serializer); // yaml_root中已包含序列化后的DAG数据
优点:
- 完全解耦DAG库与序列化库,DAG库无需依赖任何序列化工具
- 用户实现逻辑清晰,仅需编写两个核心方法
- 避免了不安全的向下转型(dynamic_cast可控,或可通过自定义类型标识优化)
缺点:
- 需要手动处理DAG循环引用(通过visited集合),这是序列化DAG的固有问题
你的原方案已经用到访问者模式,可以通过双重分发扩展它,让访问者直接处理不同派生类的序列化,无需在Node中添加虚serialize方法。
首先修改Node类和访问者基类,支持双重分发:
// 抽象访问者基类 class NodeVisitor { public: virtual ~NodeVisitor() = default; // 为每个可能的Node派生类声明visit方法(或用模板优化) virtual void visit(class MyNode<int>& node) = 0; virtual void visit(class MyNode<std::string>& node) = 0; }; class Node { public: // 纯虚accept方法,触发双重分发 virtual void accept(NodeVisitor& visitor) = 0; protected: virtual ~Node(){} private: std::vector<std::weak_ptr<Node>> children; std::vector<std::shared_ptr<Node>> parents; };
派生类实现accept方法:
template <typename T> class MyNode : public Node { public: void accept(NodeVisitor& visitor) override { visitor.visit(*this); // 遍历子节点继续序列化 for(auto& child_weak : children) { if(auto child = child_weak.lock()) { child->accept(visitor); } } } T value; };
用户编写序列化访问者,比如YAML版本:
#include <yaml-cpp/yaml.h> class YamlSerializationVisitor : public NodeVisitor { public: YamlSerializationVisitor(YAML::Node& root) : root_(root) { root_["nodes"] = YAML::Node(YAML::NodeType::Sequence); root_["edges"] = YAML::Node(YAML::NodeType::Sequence); visited_.reserve(64); } void visit(MyNode<int>& node) override { if(visited_.count(&node)) return; visited_.insert(&node); // 序列化当前节点 YAML::Node yaml_node; yaml_node["id"] = reinterpret_cast<uintptr_t>(&node); yaml_node["type"] = "int_node"; yaml_node["value"] = node.value; root_["nodes"].push_back(yaml_node); serialize_edges(node); } void visit(MyNode<std::string>& node) override { if(visited_.count(&node)) return; visited_.insert(&node); YAML::Node yaml_node; yaml_node["id"] = reinterpret_cast<uintptr_t>(&node); yaml_node["type"] = "string_node"; yaml_node["value"] = node.value; root_["nodes"].push_back(yaml_node); serialize_edges(node); } private: void serialize_edges(Node& node) { // 序列化父节点关联 for(const auto& parent : node.parents) { YAML::Node edge; edge["parent_id"] = reinterpret_cast<uintptr_t>(parent.get()); edge["child_id"] = reinterpret_cast<uintptr_t>(&node); root_["edges"].push_back(edge); } // 序列化子节点关联 for(const auto& child_weak : node.children) { if(auto child = child_weak.lock()) { YAML::Node edge; edge["parent_id"] = reinterpret_cast<uintptr_t>(&node); edge["child_id"] = reinterpret_cast<uintptr_t>(child.get()); root_["edges"].push_back(edge); } } } YAML::Node& root_; std::unordered_set<Node*> visited_; };
使用方式:
YAML::Node yaml_root; YamlSerializationVisitor visitor(yaml_root); my_dag_root->accept(visitor);
优点:
- 序列化逻辑完全与Node类解耦,DAG库无需任何序列化相关代码
- 利用双重分发,类型安全,无需向下转型
- 用户可根据派生类灵活定制序列化逻辑
缺点:
- 抽象访问者基类需要提前知晓所有可能的派生类(可通过模板访问者缓解此问题)
如果用户不需要运行时切换序列化库,而是编译时就确定工具,CRTP(奇异递归模板模式)是最优选择——它避免了虚函数开销,同时让派生类轻松接入序列化。
首先定义CRTP基类:
template <typename Derived> class SerializableNode : public Node { public: // 静态多态的serialize方法,调用派生类的具体实现 template <typename Serializer> void serialize(Serializer& serializer) const { static_cast<const Derived*>(this)->serialize_impl(serializer); // 遍历子节点序列化 for(const auto& child_weak : children) { if(auto child = child_weak.lock()) { if(auto serializable_child = dynamic_cast<const SerializableNode*>(child.get())) { serializable_child->serialize(serializer); } } } } protected: // 派生类需实现的具体序列化逻辑 virtual void serialize_impl(auto& serializer) const = 0; };
用户派生类继承CRTP基类:
#include <yaml-cpp/yaml.h> class MyIntNode : public SerializableNode<MyIntNode> { public: int value; protected: void serialize_impl(YAML::Node& serializer) const override { YAML::Node yaml_node; yaml_node["id"] = reinterpret_cast<uintptr_t>(this); yaml_node["value"] = value; serializer["nodes"].push_back(yaml_node); // 序列化父/子关系 for(const auto& parent : parents) { YAML::Node edge; edge["parent_id"] = reinterpret_cast<uintptr_t>(parent.get()); edge["child_id"] = reinterpret_cast<uintptr_t>(this); serializer["edges"].push_back(edge); } } };
使用方式:
YAML::Node yaml_root; yaml_root["nodes"] = YAML::Node(YAML::NodeType::Sequence); yaml_root["edges"] = YAML::Node(YAML::NodeType::Sequence); my_int_node->serialize(yaml_root);
优点:
- 无虚函数开销(可将serialize_impl改为非虚方法进一步优化)
- 编译时类型检查,安全性更高
- 用户仅需实现一个具体序列化方法
缺点:
- 不支持运行时切换序列化库,必须编译时确定
你的原方案确实存在设计缺陷,核心问题是试图用空基类统一不同序列化库的返回类型,违反了面向对象设计原则。三个方案各有优劣:
- 若需完全解耦、支持运行时切换序列化库,选策略模式
- 若希望序列化逻辑完全外置、类型安全,选增强访问者模式
- 若追求性能、编译时确定序列化库,选CRTP静态多态
另外还可以考虑适配器模式:为不同序列化库编写适配器,统一它们的API,DAG库仅与通用适配器接口交互,用户只需选择对应适配器即可。
内容的提问来源于stack exchange,提问作者joergbrech

