C ++序列化设计评论


9

我正在编写一个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有关。


1
您是否考虑过为此使用Boost序列化库?它包含您拥有的所有设计目标。
Bart van Ingen Schenau,2015年

1
@BartvanIngenSchenau我没有,主要是因为我对Boost的爱恨交加。我认为在这种情况下,我需要支持的某些格式可能比Boost序列化所能处理的更为复杂,而又不会增加足够的复杂性,因此使用它不会给我带来多少好处。

啊! 因此,您不是要对小部件实例进行(反序列化)(那会很奇怪……),但是这些小部件仅需要读取和写入结构化数据?您必须实现现有文件格式,还是可以自由定义即席格式?不同的小部件是否使用可以实现为通用模型的通用或相似格式?然后,您可以进行用户界面–域逻辑–模型– DAL拆分,而不必将所有内容都作为WxWidget的上帝对象。实际上,我不明白为什么小部件在这里是相关的。
阿蒙(Amon)2015年

@amon我再次编辑了问题。wxWidgets仅与与用户的界面有关:我所讨论的Widget与wxWidgets框架无关(即没有上帝对象)。我只是将该术语用作DAO类型的通用名称。

1
@LarsViklund,您提出了令人信服的论点,并且改变了我对此事的看法。我更新了示例代码。

Answers:


7

我可能是错的,但是您的设计似乎设计过度。要序列只有一个Widget,要定义WidgetReaderWidgetWriterWidgetDatabaseReaderWidgetDatabaseWriter其中每对XML,JSON和二进制编码,以及工厂将所有这些类一起实现的接口。这是有问题的,原因如下:

  • 如果我要序列非Widget类,姑且称之为Foo,我不得不重新实现类这整个动物园,创造FooReaderFooWriterFooDatabaseReaderFooDatabaseWriter接口,每次三遍每个序列化格式,再加上工厂,使其甚至远程使用。别告诉我那里不会有任何复制粘贴!即使这些类中的每个类基本上仅包含一个方法,这种组合爆炸似乎也难以维持。

  • Widget不能被合理地封装。您要么使用getter方法打开应该序列化到开放世界的所有内容,要么必须实现friend每个WidgetWriter(可能还包括WidgetReader)所有实现。无论哪种情况,您都将在序列化实现和之间引入大量耦合Widget

  • 读者/作者动物园引起不一致。每当将成员添加到时Widget,都必须更新所有相关的序列化类以存储/检索该成员。这是无法静态检查正确性的内容,因此您还必须为每个读取器和写入器编写单独的测试。在您当前的设计中,每个要序列化的类的测试数量为4 * 3 = 12。

    另一方面,添加新的序列化格式(例如YAML)也是有问题的。对于每个要序列化的类,您都必须记住要添加一个YAML读取器和写入器,并将这种情况添加到枚举和工厂中。同样,这是无法静态测试的,除非您变​​得(太)聪明并且为工厂设计了一个模板化的接口,该接口独立于Widget并确保为每个输入/输出操作提供了每种序列化类型的实现。

  • 也许Widget现在满足了SRP,因为它不负责序列化。但是读取器和写入器实现显然没有,因为“ SRP =每个对象都有一个更改的原因”的解释:当序列化格式更改或更改时,实现必须Widget更改。

如果您能够事先花最少的时间,请尝试设计比该临时类更为通用的序列化框架。例如,您可以SerializationInfo使用类似于JavaScript的对象模型来定义一个通用的互换表示形式,让我们称之为:大多数对象都可以看作是std::map<std::string, SerializationInfo>,或a std::vector<SerializationInfo>或原语(例如)int

对于每种序列化格式,您将拥有一个用于管理从该流读取和写入序列化表示形式的类。对于要序列化的每个类,您都有某种机制可以将实例从序列化表示形式转换为序列化表示形式。

我已经使用cxxtools(主页GitHub序列化演示)进行了这样的设计,并且大多数情况下都非常直观,适用范围广,并且对我的用例很满意–唯一的问题是序列化表示形式的对象模型相当薄弱,需要您可以在反序列化过程中准确知道您期望的对象类型,并且该反序列化意味着可以默认构造的对象,可以稍后对其进行初始化。这是一个人为的用法示例:

class Point {
  int _x;
  int _y;
public:
  Point(x, y) : _x(x), _y(y) {}
  int x() const { return _x; }
  int y() const { return _y; }
};

void operator <<= (SerializationInfo& si, const Point& p) {
  si.addMember("x") <<= p.x();
  si.addMember("y") <<= p.y();
}

void operator >>= (const SerializationInfo& si, Point& p) {
  int x;
  si.getMember("x") >>= x;  // will throw if x entry not found
  int y;
  si.getMember("y") >>= y;
  p = Point(x, y);
}

int main() {
  // cxxtools::Json<T>(T&) wrapper sets up a SerializationInfo and manages Json I/O
  // wrappers for other formats also exist, e.g. cxxtools::Xml<T>(T&)

  Point a(42, -15);
  std::cout << cxxtools::Json(a);
  ...
  Point b(0, 0);
  std::cin >> cxxtools::Json(p);
}

我并不是说您应该使用cxxtools或完全复制该设计,但是根据我的经验,即使您对单个的小型类也不必添加序列化,但前提是您不必太在意序列化格式(例如,默认的XML输出将使用成员名称作为元素名称,并且永远不会使用属性作为数据。

流的二进制/文本模式问题似乎无法解决,但这还算不错。一方面,它只对二进制格式有意义,在我不倾向于为其编程的平台上;-)更严重的是,这是对序列化基础结构的限制,您只需要记录下来并希望每个人都正确使用即可。在读取器或写入器中打开流太不灵活,并且C ++没有内置的类型级别机制来区分文本和二进制数据。


鉴于这些DAO基本上已经是“序列化信息”类,您的建议将如何变化?这些是POJO的C ++等效项。我还将用一些有关如何使用这些对象的更多信息来编辑我的问题。
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.