C ++シリアライゼーションデザインレビュー


9

C ++アプリケーションを書いています。ほとんどのアプリケーションはデータ引用を読み書きする必要があり、これも例外ではありません。データモデルとシリアル化ロジックの高レベルデザインを作成しました。この質問は、これらの特定の目標を念頭に置いて、私のデザインのレビューを要求しています

  • 任意の形式(rawバイナリ、XML、JSONなど)でデータモデルを読み書きする簡単で柔軟な方法を提供する。al。データの形式は、シリアル化を要求しているコードと同様に、データ自体から分離する必要があります。

  • シリアル化が合理的に可能な限りエラーが発生しないようにするため。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 */);
  }
};

私の設計の指定された目標に従って、私は1つの特定の懸念があります。C ++ストリームはテキストモードまたはバイナリモードで開くことができますが、すでに開いているストリームを確認する方法はありません。プログラマーのエラーにより、たとえばXMLまたはJSONリーダー/ライターにバイナリストリームを提供することは可能です。これにより、微妙な(またはそれほど微妙ではない)エラーが発生する可能性があります。私はコードが速く失敗することを望みますが、このデザインがそれを実行するかどうかはわかりません。

これを回避する1つの方法は、ストリームを開く責任をリーダーまたはライターに任せることですが、これはSRPに違反し、コードがより複雑になると考えています。DAOを作成するとき、ライターはストリームの行き先を気にする必要はありません。ファイル、標準出力、HTTP応答、ソケットなど、何でもかまいません。いったんその懸念がシリアライゼーションロジックにカプセル化されると、それははるかに複雑になります。特定のタイプのストリームと呼び出すコンストラクターを知る必要があります。

そのオプションを除けば、これらのオブジェクトをモデル化するための、シンプルで柔軟性があり、それを使用するコードの論理エラーを防ぐのに役立つ方法は何なのかわかりません。


ソリューションを統合する必要がある使用例は、単純なファイル選択ダイアログボックスです。ユーザーが[ファイル]メニューから[開く...]または[名前を付けて保存...]を選択すると、プログラムはWidgetDatabaseを開くか保存します。個々のウィジェットには「インポート...」と「エクスポート...」オプションもあります。

ユーザーが開くまたは保存するファイルを選択すると、wxWidgetsはファイル名を返します。そのイベントに応答するハンドラーは、ファイル名を取り、シリアライザーを取得し、重い処理を行う関数を呼び出す汎用コードである必要があります。理想的には、ソケットを介してWidgetDatabaseをモバイルデバイスに送信するなど、別のコードがファイル以外のI / Oを実行している場合にも、この設計は機能します。


ウィジェットは独自の形式で保存しますか?既存のフォーマットと相互運用できますか?はい!上記のすべて。ファイルダイアログに戻って、Microsoft Wordについて考えてみましょう。マイクロソフトはDOCX形式を自由に開発できましたが、特定の制約の範囲内で必要でした。同時に、Wordはレガシーおよびサードパーティのフォーマット(PDFなど)の読み取りまたは書き込みも行います。このプログラムも例外ではありません。私が話す「バイナリ」形式は、速度を上げるために設計された、まだ定義されていない内部形式です。同時に、他のソフトウェアと連携できるように、ドメイン内のオープンスタンダードフォーマットを読み書きできるようにする必要があります(質問とは関係ありません)。

最後に、ウィジェットのタイプは1つだけです。子オブジェクトがありますが、これらはこのシリアル化ロジックによって処理されます。プログラムはウィジェットスプロケットの両方を決してロードしません。この設計はWidgetsとWidgetDatabases のみを考慮する必要があります。


1
このためにBoost Serializationライブラリの使用を検討しましたか?これには、すべての設計目標が組み込まれています。
Bart van Ingen Schenau 2015

1
@BartvanIngenSchenau私は持っていませんでした。主に私がBoostと持っている愛/憎しみの関係のためです。この場合、サポートする必要があるいくつかのフォーマットは、Boost Serializationが処理するよりも複雑になる可能性があると思います。

ああ!したがって、ウィジェットインスタンスを(逆)シリアル化するわけではありませんが(奇妙なことに...)、これらのウィジェットは構造化データを読み書きする必要があるだけですか?既存のファイル形式を実装する必要がありますか、それともアドホック形式を自由に定義できますか?異なるウィジェットは、共通のモデルとして実装できる共通または類似の形式を使用していますか?その後、すべてをWxWidgetのgodオブジェクトとして変更するのではなく、ユーザーインターフェイス(ドメインロジック)-モデル-DAL分割を実行できます。実際、ここでウィジェットが関連している理由はわかりません。
2015

@amon質問をもう一度編集しました。wxWidgetsは、ユーザーとのインターフェースにのみ関連します。私が話すウィジェットは、wxWidgetsフレームワークとは何の関係もありません(つまり、godオブジェクトはありません)。私はその用語をDAOのタイプの総称として使用します。

1
@LarsViklundあなたは説得力のある議論をし、あなたはその問題についての私の意見を変えました。サンプルコードを更新しました。

回答:


7

私は間違っているかもしれませんが、あなたのデザインはひどく過剰設計されているようです。1つだけをシリアル化WidgetするWidgetReaderWidgetWriterWidgetDatabaseReaderWidgetDatabaseWriterそれぞれがXML、JSON、バイナリエンコーディングの実装を持つ、、、インターフェイスを定義し、これらのクラスをすべて結び付けるファクトリを定義します。これは、次の理由で問題があります。

  • 私は非直列化したい場合はWidget、クラス、レッツ・コールそれをFoo、私はクラスのこの全体の動物園を再実装し、作成する必要がありFooReaderFooWriterFooDatabaseReaderFooDatabaseWriterインターフェース、回ずつシリアル化形式に加え、それもリモートで使用できるようにする工場のための3つ。コピー&ペーストは行われないと言わないでください!これらの各クラスが本質的に単一のメソッドのみを含んでいる場合でも、この組み合わせの爆発はかなり維持不可能であるようです。

  • Widget合理的にカプセル化することはできません。ゲッターメソッドを使用して、オープンワールドにシリアル化する必要のあるすべてのものを開くかfriend、すべてのWidgetWriter(そしておそらくすべてのWidgetReader)実装を実装する必要があります。どちらの場合も、シリアライゼーションの実装との間にかなりのカップリングを導入しますWidget

  • リーダー/ライター動物園は矛盾を招きます。にメンバーを追加するときは常にWidget、そのメンバーを格納/取得するために、関連するすべてのシリアル化クラスを更新する必要があります。これは正確性を静的にチェックすることができないものであるため、リーダーとライターごとに個別のテストを作成する必要もあります。現在の設計では、シリアル化するクラスごとに4 * 3 = 12のテストです。

    反対に、YAMLなどの新しいシリアル化形式を追加することも問題があります。シリアル化するクラスごとに、忘れずにYAMLリーダーとライターを追加し、そのケースを列挙型とファクトリーに追加する必要があります。繰り返しますが、これは、(あまりに)巧妙になり、独立したファクトリーのテンプレート化されたインターフェースをWidget作成し、各入出力操作の各シリアル化タイプの実装が確実に提供されない限り、静的にテストできないものです。

  • たぶん、Widgetそれはシリアライゼーションを担当しないので、今はSRPを満たしています。しかし、リーダーとライターの実装は明らかにそうではなく、「SRP =各オブジェクトには変更する理由が1つある」という解釈がありWidgetます。シリアル化形式が変更されたとき、または変更されたときに、実装を変更する必要があります。

最小限の時間を前もって投資できる場合は、この特別なクラスのもつれよりも一般的なシリアル化フレームワークを作成してみてください。たとえばSerializationInfo、JavaScriptのようなオブジェクトモデルを使用して、共通の交換表現を定義し、それをと呼びましょう。ほとんどのオブジェクトは、、、またはのようなプリミティブstd::map<std::string, SerializationInfo>として見ることができます。std::vector<SerializationInfo>int

各シリアル化形式について、そのストリームからのシリアル化表現の読み取りと書き込みを管理するクラスが1つあります。また、シリアル化するクラスごとに、インスタンスをシリアル化表現との間で変換するメカニズムがいくつかあります。

私はcxxtools(homepageGitHubserialization demo)でそのような設計を経験しており、ほとんどの場合、非常に直感的で、広く適用可能であり、私のユースケースに満足できます。逆シリアル化中に予期するオブジェクトの種類を正確に知ること、およびその逆シリアル化は、後で初期化できるデフォルトで構築可能なオブジェクトを意味します。これは人為的な使用例です:

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を使用したり、そのデザインを正確にコピーしたりする必要があると言っているわけではありませんが、私の経験では、シリアル化形式にあまり注意を払わない限り、そのデザインにより、小さな1回限りのクラスでもシリアル化を追加するのは簡単です。たとえば、デフォルトのXML出力では、要素名としてメンバー名が使用され、データの属性は使用されません)。

ストリームのバイナリ/テキストモードの問題は解決できないようですが、それほど悪くはありません。一つには、それはバイナリフォーマットにのみ関係し、私がプログラムする傾向がないプラットフォームでは;-)より深刻なことに、それはあなたが文書化し、誰もが正しく使用することを望んでいるあなたのシリアライゼーションインフラストラクチャの制限です。リーダーまたはライター内でストリームを開くことはあまりにも柔軟性がなく、C ++には、テキストをバイナリデータと区別するための組み込み型レベルメカニズムがありません。


これらのDAOが基本的に既に「シリアル化情報」クラスであるとすると、アドバイスはどのように変わりますか?これらはPOJOに相当するC ++ です。これらのオブジェクトがどのように使用されるかについて、もう少し情報を追加して、質問も編集します。
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.