在开发大型 C++ 桌面应用(如组态软件、编辑器、CAD 系统)时,我们经常面临一个核心挑战:如何高效、统一地管理内存中复杂的异构对象树?
我们需要一个能够容纳不同类型业务对象(如设备、工程、图元)的容器,同时还需要支持层级关系、自动化内存管理、事件通知以及序列化。
本文将深入剖析一个基于 C++11 实现的通用数据模型框架。该设计采用了组合模式 、访问者模式 和观察者模式,实现了一个高内聚、低耦合的"内存对象数据库"。
1. 核心架构设计
系统的核心思想是将"树形拓扑结构"与"具体业务数据"分离。通过继承和模板技术,实现类型擦除与统一管理。
类关系图谱
DataNode(基类): 抽象节点。定义了父子关系、ID 生成、树遍历等通用行为。DataDocument(管理器): 树的根节点。负责全局上下文管理、工厂创建方法以及事件分发。DataElement<T>(适配器) : 泛型子类。将具体的业务类T包装为树节点,使其能够挂载到DataNode树上。DataVisitor(访问者): 提供了一种安全访问内部原始数据的机制。
2. 关键技术实现
2.1 异构对象的统一容器 (Type Erasure)
在 C++ 中,不同类型的对象难以存放在同一个容器中。本项目通过 DataNode 指针实现了多态存储。
cpp
// 1. 定义抽象基类,只包含通用接口
class DataNode {
// 存储子节点,使用 shared_ptr 自动管理生命周期
std::vector<std::shared_ptr<DataNode>> _listOfChildren;
public:
virtual ~DataNode() {}
void AppendChild(const std::shared_ptr<DataNode> &newChild);
};
// 2. 定义泛型适配器,持有具体数据
template<class T>
class DataElement : public DataNode {
std::shared_ptr<T> _attachedInstance; // 真正的业务对象
public:
DataElement() : _attachedInstance(new T()) {}
// 转发调用到具体业务对象
virtual wstring ModuleID() { return _attachedInstance->ModuleID(); }
};
设计优势:
- 统一性 :无论业务对象是
Device还是Project,在树结构中统统被视为DataNode。 - 扩展性 :新增业务类型无需修改核心树代码,只需实例化
DataElement<NewType>即可。
2.2 智能指针与生命周期管理 (RAII)
传统的 C++ 树结构容易出现内存泄漏或野指针。本项目全面采用了 C++11 智能指针:
- 强引用 :父节点持有子节点的
std::shared_ptr,保证子节点生命周期跟随父节点。 - 内存优化技巧 :在析构或清空节点时,使用了 Swap Trick 彻底释放 vector 内存。
cpp
// DataNode.cpp
void DataNode::RemoveAll() {
// 创建临时空 vector 并交换,强制释放 capacity 内存
std::vector<std::shared_ptr<DataNode>>().swap(_listOfChildren);
}
2.3 基于回调的事件驱动机制 (Observer Pattern)
为了实现数据与视图(UI)的解耦,DataDocument 提供了基于 std::function 的事件回调。
cpp
// DataDocument.h
typedef std::function<void(DataDocument*, DataNode*)> DataNodeFunction;
class DataDocument : public DataNode {
DataNodeFunction _pNodeInserted; // 节点插入回调
public:
void SetNodeInserted(DataNodeFunction pFunc) { _pNodeInserted = pFunc; }
// 当有节点插入时触发
void DataNodeInserted(DataNode *insertedNode) {
if (_pNodeInserted) _pNodeInserted(this, insertedNode);
}
};
应用场景 :
UI 层只需在初始化时调用 doc->SetNodeInserted(UpdateTreeView)。当数据层发生 AppendChild 操作时,UI 会自动刷新,无需数据层感知 UI 的存在。
2.4 工厂模式与"上户口"
为了确保每个节点都能正确关联到文档(Document),节点不能直接 new,而必须通过工厂方法创建。
cpp
// DataDocument.h
template<typename T>
shared_ptr<DataElement<T>> CreateNode() {
// 1. 创建节点
shared_ptr<DataElement<T>> myNode(new DataElement<T>());
// 2. 转换为基类指针
shared_ptr<DataNode> pNode = dynamic_pointer_cast<DataNode>(myNode);
// 3. 关键:设置所属文档指针 (Dependency Injection)
pNode->Set_DocumentElement(this);
return myNode;
}
3. 序列化接口设计
系统预留了标准的序列化接口,用于支持 XML/JSON 持久化。
cpp
// DataElement.h
virtual shared_ptr<NERC::Foundation::Parameter> ParserToParameter(Database* ptrDatabase) {
// 委托给具体的业务对象 T 进行序列化
return _attachedInstance->ParserToParameter(ptrDatabase);
};
这种设计将序列化逻辑下沉到具体的业务类 T 中,DataElement 仅作为透明的转发层。
4. 总结
这套框架展示了成熟的 C++ 工程化思维:
- 利用多态和模板 解决了类型系统的刚性问题。
- 利用 RAII 和智能指针 解决了手动内存管理的风险。
- 利用回调函数 实现了模块间的彻底解耦。
它非常适合作为 组态软件、节点编辑器、文件系统管理 等复杂层次化应用的底层数据内核。