我正在编写一个C ++应用程序。大多数应用程序都需要读写数据引用,这一点也不例外。我为数据模型和序列化逻辑创建了一个高级设计。这个问题要求考虑以下特定目标对我的设计进行审查:
以一种简单灵活的方式来读取和写入任意格式的数据模型:原始二进制,XML,JSON等。等 数据格式应与数据本身以及请求序列化的代码分离。
为了确保序列化尽可能地没有错误。I / O具有多种固有的风险:我的设计是否引入了更多的失败方法?如果是这样,我如何重构设计以减轻这些风险?
该项目使用C ++。无论您是喜欢还是讨厌它,语言都有其自己的处理方式,并且设计旨在与该语言一起工作,而不是反对它。
最后,该项目基于wxWidgets构建。当我在寻找适用于更一般情况的解决方案时,此特定实现应与该工具箱配合良好。
接下来是用C ++编写的一组非常简单的类,它们说明了设计。这些不是我到目前为止已经部分编写的实际类,此代码仅说明了我正在使用的设计。
首先,一些示例DAO:
#include <iostream>
#include <map>
#include <memory>
#include <string>
#include <vector>
// One widget represents one record in the application.
class Widget {
public:
using id_type = int;
private:
id_type id;
};
// Container for widgets. Much more than a dumb container,
// it will also have indexes and other metadata. This represents
// one data file the user may open in the application.
class WidgetDatabase {
::std::map<Widget::id_type, ::std::shared_ptr<Widget>> widgets;
};
接下来,我定义用于读取和写入DAO的纯虚拟类(接口)。这个想法是从数据本身(SRP)中提取数据的序列化。
class WidgetReader {
public:
virtual Widget read(::std::istream &in) const abstract;
};
class WidgetWriter {
public:
virtual void write(::std::ostream &out, const Widget &widget) const abstract;
};
class WidgetDatabaseReader {
public:
virtual WidgetDatabase read(::std::istream &in) const abstract;
};
class WidgetDatabaseWriter {
public:
virtual void write(::std::ostream &out, const WidgetDatabase &widgetDb) const abstract;
};
最后,这是为所需的I / O类型获取正确的读取器/写入器的代码。也将定义读者/作家的子类,但这些子类不会对设计审查产生任何影响:
enum class WidgetIoType {
BINARY,
JSON,
XML
// Other types TBD.
};
WidgetIoType forFilename(::std::string &name) { return ...; }
class WidgetIoFactory {
public:
static ::std::unique_ptr<WidgetReader> getWidgetReader(const WidgetIoType &type) {
return ::std::unique_ptr<WidgetReader>(/* TODO */);
}
static ::std::unique_ptr<WidgetWriter> getWidgetWriter(const WidgetIoType &type) {
return ::std::unique_ptr<WidgetWriter>(/* TODO */);
}
static ::std::unique_ptr<WidgetDatabaseReader> getWidgetDatabaseReader(const WidgetIoType &type) {
return ::std::unique_ptr<WidgetDatabaseReader>(/* TODO */);
}
static ::std::unique_ptr<WidgetDatabaseWriter> getWidgetDatabaseWriter(const WidgetIoType &type) {
return ::std::unique_ptr<WidgetDatabaseWriter>(/* TODO */);
}
};
根据我设计的既定目标,我有一个特别需要关注的问题。可以以文本或二进制模式打开C ++流,但是无法检查已打开的流。通过程序员错误,有可能向XML或JSON读取器/写入器提供二进制流。这可能会导致细微(或不太细微)的错误。我希望代码能够快速失败,但是我不确定该设计是否能够做到这一点。
解决此问题的一种方法可能是将开放流的责任分担给读取器或写入器,但我认为这违反了SRP,会使代码更复杂。编写DAO时,编写者不必在乎流的方向:它可以是文件,标准输出,HTTP响应,套接字,任何内容。一旦这种担忧被封装在序列化逻辑中,它将变得更加复杂:它必须知道流的特定类型以及要调用的构造函数。
除了该选项之外,我不确定简单,灵活并且有助于防止使用它的代码中的逻辑错误是对这些对象建模的更好方法。
解决方案必须与之集成的用例是一个简单的文件选择对话框。用户从“文件”菜单中选择“打开...”或“另存为...”,程序将打开或保存WidgetDatabase。每个小部件还将有“导入...”和“导出...”选项。
当用户选择要打开或保存的文件时,wxWidgets将返回文件名。响应该事件的处理程序必须是通用代码,该通用代码采用文件名,获取序列化器并调用函数来执行繁重的工作。理想情况下,如果另一段代码正在执行非文件I / O,例如通过套接字将WidgetDatabase发送到移动设备,则此设计也将起作用。
小部件会保存为自己的格式吗?它可以与现有格式互操作吗?是! 上述所有的。回到文件对话框,考虑一下Microsoft Word。Microsoft可以自由开发DOCX格式,但是他们希望在一定的限制内。同时,Word还读取或写入旧版和第三方格式(例如PDF)。这个程序没有什么不同:我所说的“二进制”格式是一种尚未定义的用于速度的内部格式。同时,它必须能够在其域中读写开放的标准格式(与问题无关),以便能够与其他软件一起使用。
最后,只有一种类型的Widget。它将具有子对象,但这些对象将由此序列化逻辑处理。该程序将永远不会加载小部件和链轮。该设计只需要与Widgets和WidgetDatabases有关。