web-dev-qa-db-ja.com

Protobufデザインパターン

JavaベースのサービスのGoogle Protocol Buffersを評価しています(ただし、言語にとらわれないパターンを期待しています)。2つの質問があります。

最初は広い一般的な質問です:

人々がどのようなパターンを使用していると思いますか?上記のパターンは、クラスの編成(.protoファイルごとのメッセージ、パッケージ化、配布など)およびメッセージの定義(たとえば、繰り返しフィールドと繰り返しカプセル化されたフィールド*)に関連しています。

Google Protobufのヘルプページや公開ブログには、この種の情報はほとんどありませんが、XMLなどの確立されたプロトコルに関する情報はたくさんあります。

また、次の2つの異なるパターンについて具体的な質問があります。

  1. メッセージを.protoファイルで表現し、それらを別個のjarとしてパッケージ化して、サービスのターゲットユーザーに送信します。これは、基本的には私が推測するデフォルトのアプローチです。

  2. 同じことを行うだけでなく、少なくともこれら2つのメソッドをサポートするコントラクトを実装する各メッセージの周囲に手作りのラッパー(サブクラスではない!)を含めます(Tはラッパークラス、Vはメッセージクラスです(ジェネリックを使用していますが、簡潔にするために構文を簡略化しています)。 :

    _public V toProtobufMessage() {
        V.Builder builder = V.newBuilder();
        for (Item item : getItemList()) {
            builder.addItem(item);
        }
        return builder.setAmountPayable(getAmountPayable()).
                       setShippingAddress(getShippingAddress()).
                       build();
    }
    
    public static T fromProtobufMessage(V message_) { 
        return new T(message_.getShippingAddress(), 
                     message_.getItemList(),
                     message_.getAmountPayable());
    }
    _

(2)で得られる利点の1つは、V.newBuilder().addField().build()によって導入される複雑さを隠し、isOpenForTrade()またはisAddressInFreeDeliveryZone()などの意味のあるメソッドを追加できることです。私のラッパー。私が(2)で目にする2番目の利点は、クライアントが不変オブジェクト(ラッパークラスで適用できるもの)を処理することです。

(2)で見られる1つの欠点は、コードを複製し、ラッパークラスを.protoファイルと同期させる必要があることです。

誰もが2つのアプローチのいずれかについてより良い技術やさらなる批判を持っていますか?


*繰り返しフィールドをカプセル化するとは、次のようなメッセージを意味します。

_message ItemList {
    repeated item = 1;
}

message CustomerInvoice {
    required ShippingAddress address = 1;
    required ItemList = 2;
    required double amountPayable = 3;
}
_

このようなメッセージの代わりに:

_message CustomerInvoice {
    required ShippingAddress address = 1;
    repeated Item item = 2;
    required double amountPayable = 3;
}
_

私は後者が好きですが、反対の意見を聞いてうれしいです。

23
Apoorv Khurasia

私が働いている場所では、protobufの使用を隠すことにしました。アプリケーション間で_.proto_ファイルを配布するのではなく、protobufインターフェースを公開するすべてのアプリケーションが、それに通信できるクライアントライブラリをエクスポートします。

私はこれらのprotobufを公開するアプリケーションの1つだけに取り組みましたが、その中で、各protobufメッセージはドメイン内のいくつかの概念に対応しています。コンセプトごとに、通常のJavaインターフェースがあります。次に、実装のインスタンスを取得して適切なメッセージオブジェクトを構築し、メッセージオブジェクトを取得してインターフェースの実装のインスタンス(通常、コンバーター内で定義された単純な匿名またはローカルクラス)。protobufで生成されたメッセージクラスとコンバーターは、アプリケーションとクライアントライブラリの両方で使用されるライブラリーを形成します。クライアントライブラリは、接続をセットアップし、メッセージを送受信するための少量のコードを追加します。

次に、クライアントアプリケーションはクライアントライブラリをインポートし、送信したいインターフェイスの実装を提供します。実際、双方が後者のことを行います。

つまり、クライアントがパーティーの招待状を送信する要求/応答サイクルがあり、サーバーがRSVPで応答している場合、次のことが関係します。

  • _.proto_ファイルに記述されたPartyInvitationメッセージ
  • PartyInvitationMessageクラス、protocによって生成
  • PartyInvitationインターフェイス、共有ライブラリで定義
  • ActualPartyInvitation、クライアントアプリによって定義されたPartyInvitationの具体的な実装(実際にはそれを呼び出さない!)
  • StubPartyInvitation、共有ライブラリによって定義されたPartyInvitationの単純な実装
  • PartyInvitationConverterは、PartyInvitationPartyInvitationMessageに、PartyInvitationMessageStubPartyInvitationに変換できます。
  • _.proto_ファイルに記述されたRSVPメッセージ
  • RSVPMessageクラス、protocによって生成
  • RSVPインターフェイス、共有ライブラリで定義
  • ActualRSVP、サーバーアプリによって定義されたRSVPの具体的な実装(実際にはそれとも呼ばれません!)
  • StubRSVP、共有ライブラリによって定義されたRSVPの単純な実装
  • RSVPConverterは、RSVPRSVPMessageに、RSVPMessageStubRSVPに変換できます。

実際の実装とスタブ実装を分離している理由は、実際の実装は一般にJPAにマップされたエンティティークラスであるためです。サーバーはそれらを作成して永続化するか、データベースからそれらを照会してから、送信するためにそれらをprotobuf層に渡します。接続の受信側でこれらのクラスのインスタンスを作成することが適切であるとは思われませんでした。永続化コンテキストに関連付けられていないからです。さらに、エンティティには多くの場合、ネットワーク経由で送信されるよりも多くのデータが含まれているため、受信側で完全なオブジェクトを作成することもできません。これにより、メッセージごとにクラスが1つ増えることになります。

確かに、protobufを使用することはまったく良い考えであると私は完全に確信していません。単純な古いRMIとシリアル化にこだわっていれば、ほとんど同じ数のオブジェクトを作成する必要はなかったでしょう。多くの場合、エンティティークラスをシリアル化可能としてマークし、そのまま使用できます。

とはいえ、モジュール間の通信にprotobufを多用するコードベースで、Googleで働いている友人がいます。それらは完全に異なるアプローチをとります:生成されたメッセージクラスをまったくラップせず、熱心にそれらをdeep(ish)をコードに渡します。これはインターフェースを柔軟に保つための簡単な方法であるため、良いことだと考えられています。メッセージが進化したときに同期を保つための足場コードはなく、生成されたクラスは、時間の経過とともに追加されたフィールドの有無を検出するコードを受信するために必要なすべてのhasFoo()メソッドを提供します。ただし、Googleで働く人々は(a)かなり賢く、(b)少し頭が悪い傾向があることを覚えておいてください。

16
Tom Anderson

アンダーソンズの回答を追加するために、メッセージを巧妙に入れ子にして入れ子にして、やり過ぎるという細い線があります。問題は、各メッセージが舞台裏で新しいクラスを作成し、あらゆる種類のデータのアクセサーとハンドラーを作成することです。ただし、データをコピーしたり、1つの値を変更したり、メッセージを比較したりする必要がある場合は、コストがかかります。大量のデータがある場合や時間に制約がある場合、これらのプロセスは非常に遅く、実行するのが大変です。

0
Marko Bencik