埋め込みドキュメント

2013年6月4日

最近、サーバーを介してJSONデータ構造を流すのをよく見かけます。JSONドキュメントは、AggregateOrientedDatabaseを使用するか、リレーショナルデータベースでシリアライズされたLOBを使用することにより、直接永続化できます。JSONドキュメントは、Webブラウザーに直接提供したり、サーバー側のページレンダラーにデータを転送するために使用することもできます。JSONがこのように使用されている場合、オブジェクト指向言語を使用すると、JSONをオブジェクトに変換してから再度レンダリングする必要があるため、プログラミングの無駄になるため邪魔になると言われるのを耳にします[1]。無駄であるという点には同意しますが、これはオブジェクトの問題ではなく、カプセル化の理解不足であると主張します。

注文をJSONドキュメントとして保存し、わずかなサーバー側処理を加えて、再びJSONとして提供することを想像してみましょう。ドキュメントの例は次のようになります。

{ "id": 1234,
  "customer": "martin",
  "items": [
    {"product": "talisker", "quantity": 500},
    {"product": "macallan", "quantity": 800},
    {"product": "ledaig",   "quantity": 1100}
  ],
  "deliveries": [
    { "id": 7722,
      "shipDate": "2013-04-19",
      "items": [
        {"product": "talisker", "quantity": 300},
        {"product": "ledaig",   "quantity": 500}
      ]
    },
    { "id": 6533,
      "shipDate": "2013-04-18",
      "items": [
        {"product": "talisker", "quantity": 200},
        {"product": "ledaig",   "quantity": 300},
        {"product": "macallan", "quantity": 300}
      ]
    }
  ]
}

サーバー側の処理はあまり多くないが、いくつかはあると仮定します。また、オブジェクト指向言語を使用していると仮定します。単純なアプローチでは、JSONドキュメントを読み込み、データを適切なオブジェクトグラフ(注文、明細項目、配送など)に変換し、処理を適用し、オブジェクトグラフをクライアント用にJSONにシリアライズするかもしれません。

このような状況の多くでは、JSONのような形式でデータを保持しながら、操作を調整するためにオブジェクトでラップする方が良い方法です。ほとんどのプログラミング環境には、ドキュメントを取得して汎用データ構造にデシリアライズする汎用ライブラリが用意されています。そのため、JSONドキュメントはリストと辞書の構造にデシリアライズされ、XMLドキュメントはXMLノードのツリーにデシリアライズされます。次に、この汎用データ構造を取得して、注文オブジェクトのフィールドに入れることができます。以下はRubyとJSONの例です。

class Order...

  def initialize jsonDocument
    @data = JSON.parse(jsonDocument)
  end

データを操作したい場合は、通常どおりオブジェクトにメソッドを定義し、このデータ構造にアクセスして実装できます。

class Order...

  def customer
    @data['customer']
  end
  def quantity_for aProduct
    item = @data['items'].detect{|i| aProduct == i['product']}
    return item ? item['quantity'] : 0
  end

これには、より複雑なロジックを持つケースも含まれます。[2]

class Order...

  def outstanding_delivery_for aProduct
    delivered_amount = @data['deliveries'].
      map{|d| d['items']}.
      flatten.
      select{|d| aProduct == d['product']}.
      inject(0){|res, d| res += d['quantity']}
    return quantity_for(aProduct) - delivered_amount
  end

埋め込みドキュメントは、クライアントに送信する前にエンリッチできます。

class Order...

  def enrich
    @data['earliestShipDate'] = 
      @data['deliveries'].
      map{|d| Date.parse(d['shipDate'])}.
      min.
      to_s
  end

必要に応じて、埋め込みドキュメントのサブツリーに同様のオブジェクトを形成できます。

class Order...

  def deliveries
    @data['deliveries'].map{|d| Delivery.new(d)}
  end

class Delivery

  def initialize hash
    @data = hash
  end
  def ship_date
    Date.parse(@data['shipDate'])
  end

ここで注意すべきことの1つは、このようなオブジェクトラッパーは通常のオブジェクトとまったく同じではないということです。上記のコードフラグメントで返される配送オブジェクトは、より一般的な構造で配置されたオブジェクトから期待されるのと同じ等価性セマンティクスを持っていません。

比較的まれではありますが、埋め込みドキュメントはオブジェクト指向によく適合します。カプセル化されたデータのポイントは、データ構造を隠蔽することであり、オブジェクトのユーザーが注文の内部構造を知ったり気にしたりしないようにすることです。

関数型プログラミングに詳しい人は、一連の関数を通じて汎用データ構造を流すスタイルを認識するでしょう。オブジェクトは、汎用データ構造を操作するための名前空間を提供すると考えることができます。

埋め込みドキュメントの最適な場所は、データストアから取得したものと同じ形式でドキュメントを提供する場合ですが、そのデータの操作も実行したい場合です。JSONドキュメントの内容にアクセスする必要がない場合は、汎用データ構造にデシリアライズする必要さえありません。注文オブジェクトに必要なのは、コンストラクターとJSON表現を返すメソッドだけです。一方、データに対してより多くの作業(より多くのサーバー側ロジック、異なる表現への変換)を行う場合は、データをオブジェクトグラフに変換する方が簡単かどうかを検討する価値があります。

注記

1: 一部の人は、計算の無駄でもあると主張するかもしれませんが、それが重要である場合は驚きでしょう。測定が伴わない限り、オブジェクトグラフへの変換に対するパフォーマンスの議論は絶対に受け入れません。あらゆるパフォーマンスの議論と同様です。

2: このメソッドのコレクションパイプラインのチェーンに注目してください。私の不満の1つは、一部の関数型ファンが、このスタイルのコードはオブジェクト指向ではないと言っているのを聞くことです。C++/Javaのバックグラウンドを持つ人には異質に思えるかもしれませんが、このスタイルはスモールトーカーには完全に自然です。