Protobuf Designmuster


19

Ich evaluiere Google Protocol Buffers für einen Java-basierten Dienst (erwarte jedoch sprachunabhängige Muster). Ich habe zwei Fragen:

Die erste ist eine allgemeine Frage:

Welche Muster sehen wir Menschen verwenden? Diese Muster stehen im Zusammenhang mit der Klassenorganisation (z. B. Nachrichten pro .proto-Datei, Paketierung und Verteilung) und der Nachrichtendefinition (z. B. wiederholte Felder im Vergleich zu wiederholten gekapselten Feldern *) usw.

Auf den Google Protobuf-Hilfeseiten und in öffentlichen Blogs finden sich nur sehr wenige Informationen dieser Art, während für etablierte Protokolle wie XML eine Menge Informationen zur Verfügung stehen.

Ich habe auch spezielle Fragen zu den folgenden zwei verschiedenen Mustern:

  1. Stellen Sie Nachrichten in .proto-Dateien dar, verpacken Sie sie als separate JAR-Datei und senden Sie sie an die Zielgruppe des Dienstes. Dies ist meiner Meinung nach der Standardansatz.

  2. Tun Sie dasselbe, schließen Sie jedoch auch handgefertigte Wrapper (keine Unterklassen!) In jede Nachricht ein, die einen Vertrag implementieren, der mindestens diese beiden Methoden unterstützt (T ist die Wrapper-Klasse, V ist die Nachrichtenklasse). :

    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());
    }
    

Ein Vorteil, den ich bei (2) sehe, ist, dass ich die Komplexität, die durch meine Wrapper eingeführt wurde, verbergen V.newBuilder().addField().build()und einige sinnvolle Methoden wie isOpenForTrade()oder isAddressInFreeDeliveryZone()in meine Wrapper einfügen kann. Der zweite Vorteil, den ich mit (2) sehe, ist, dass meine Clients mit unveränderlichen Objekten umgehen (etwas, das ich in der Wrapper-Klasse erzwingen kann).

Ein Nachteil, den ich bei (2) sehe, ist, dass ich Code dupliziere und meine Wrapper-Klassen mit .proto-Dateien synchronisieren muss.

Hat jemand bessere Techniken oder weitere Kritiken zu einem der beiden Ansätze?


* Mit dem Einkapseln eines wiederholten Feldes meine ich Nachrichten wie diese:

message ItemList {
    repeated item = 1;
}

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

anstelle von Nachrichten wie dieser:

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

Letzteres gefällt mir, aber ich freue mich über Argumente dagegen.


Ich brauche 12 weitere Punkte, um neue Tags zu erstellen, und ich denke, protobuf sollte ein Tag für diesen Beitrag sein.
Apoorv Khurasia

Antworten:


13

Wo ich arbeite, wurde die Entscheidung getroffen, die Verwendung von Protobuf zu verbergen. Wir verteilen die .protoDateien nicht zwischen Anwendungen, sondern jede Anwendung, die eine protobuf-Schnittstelle zur Verfügung stellt, exportiert eine Client-Bibliothek, die mit ihr kommunizieren kann.

Ich habe nur an einer dieser protobuf-exponierenden Anwendungen gearbeitet, aber darin entspricht jede protobuf-Nachricht einem bestimmten Konzept in der Domäne. Für jedes Konzept gibt es eine normale Java-Oberfläche. Es gibt dann eine Konverterklasse, die eine Instanz einer Implementierung nehmen und ein geeignetes Nachrichtenobjekt erstellen kann, und ein Nachrichtenobjekt nehmen und eine Instanz einer Implementierung der Schnittstelle erstellen kann (normalerweise wird eine einfache anonyme oder lokale Klasse definiert) im Konverter). Die von protobuf generierten Nachrichtenklassen und Konverter bilden zusammen eine Bibliothek, die sowohl von der Anwendung als auch von der Client-Bibliothek verwendet wird. Die Clientbibliothek fügt eine kleine Menge Code zum Einrichten von Verbindungen sowie zum Senden und Empfangen von Nachrichten hinzu.

Clientanwendungen importieren dann die Clientbibliothek und stellen Implementierungen aller Schnittstellen bereit, die sie senden möchten. In der Tat tun beide Seiten das letztere.

Zur Verdeutlichung bedeutet dies, dass in einem Anforderungs-Antwort-Zyklus, in dem der Client eine Partyeinladung sendet und der Server mit einem RSVP antwortet, folgende Dinge zu beachten sind:

  • PartyInvitation-Nachricht, in die .protoDatei geschrieben
  • PartyInvitationMessage Klasse, generiert von protoc
  • PartyInvitation Schnittstelle, die in der gemeinsam genutzten Bibliothek definiert ist
  • ActualPartyInvitation, eine konkrete Umsetzung der PartyInvitationvon der Client-App definierten (eigentlich nicht so genannten!)
  • StubPartyInvitation, eine einfache Implementierung der PartyInvitationvon der Shared Library definierten
  • PartyInvitationConverter, die a PartyInvitationzu a PartyInvitationMessageund a PartyInvitationMessagezu a konvertieren könnenStubPartyInvitation
  • Antwortnachricht, in die .protoDatei geschrieben
  • RSVPMessage Klasse, generiert von protoc
  • RSVP Schnittstelle, die in der gemeinsam genutzten Bibliothek definiert ist
  • ActualRSVP, eine konkrete Implementierung der RSVPvon der Server-App definierten (auch eigentlich nicht so genannten!)
  • StubRSVP, eine einfache Implementierung der RSVPvon der Shared Library definierten
  • RSVPConverter, die ein RSVPin ein RSVPMessageund ein RSVPMessagein ein konvertieren könnenStubRSVP

Der Grund für die Trennung von tatsächlichen und Stub-Implementierungen besteht darin, dass es sich bei den tatsächlichen Implementierungen im Allgemeinen um JPA-zugeordnete Entitätsklassen handelt. Der Server erstellt sie entweder und speichert sie oder fragt sie aus der Datenbank ab und übergibt sie dann an die zu übertragende Protobuf-Schicht. Es schien nicht angebracht, Instanzen dieser Klassen auf der Empfängerseite der Verbindung zu erstellen, da sie nicht an einen Persistenzkontext gebunden wären. Darüber hinaus enthalten die Entitäten häufig eher mehr Daten als über das Kabel übertragen werden, sodass es nicht einmal möglich wäre, auf der Empfängerseite intakte Objekte zu erstellen. Ich bin nicht ganz davon überzeugt, dass dies der richtige Schritt war, da wir dadurch eine Klasse mehr pro Nachricht haben als sonst.

In der Tat bin ich nicht ganz davon überzeugt, dass die Verwendung von protobuf überhaupt eine gute Idee war. Wenn wir uns an einfaches altes RMI und Serialisierung gehalten hätten, hätten wir nicht annähernd so viele Objekte erstellen müssen. In vielen Fällen hätten wir unsere Entitätsklassen einfach als serialisierbar markieren und damit weitermachen können.

Nachdem ich das alles gesagt habe, habe ich einen Freund, der bei Google an einer Codebasis arbeitet, die protobuf intensiv für die Kommunikation zwischen Modulen nutzt. Sie verfolgen einen völlig anderen Ansatz: Sie binden die generierten Nachrichtenklassen überhaupt nicht ein und geben sie begeistert tief in ihren Code ein. Dies wird als eine gute Sache angesehen, da es eine einfache Möglichkeit ist, die Schnittstellen flexibel zu halten. Es gibt keinen Gerüstcode, der bei der Nachrichtenentwicklung synchron gehalten werden muss, und die generierten Klassen bieten alle erforderlichen hasFoo()Methoden zum Empfangen von Code, um das Vorhandensein oder Fehlen von Feldern zu erkennen, die im Laufe der Zeit hinzugefügt wurden. Bedenken Sie jedoch, dass die Leute, die bei Google arbeiten, (a) eher schlau und (b) etwas verrückt sind.


An einem Punkt habe ich mir überlegt , JBoss Serialization als mehr oder weniger einfachen Ersatz für die Standardserialisierung zu verwenden. Es war viel schneller. Allerdings nicht so schnell wie protobuf.
Tom Anderson

Die JSON-Serialisierung mit jackson2 ist auch ziemlich schnell. Was ich an GBP hasse, ist die unnötige Verdoppelung der Hauptschnittstellenklassen.
Apoorv Khurasia

0

Um Andersons Antwort zu ergänzen, besteht eine feine Linie darin, Nachrichten geschickt ineinander zu verschachteln und zu übertreiben. Das Problem ist, dass jede Nachricht hinter den Kulissen eine neue Klasse und alle Arten von Zugriffsmechanismen und Handlern für die Daten erstellt. Dies ist jedoch mit Kosten verbunden, wenn Sie die Daten kopieren oder einen Wert ändern oder die Nachrichten vergleichen müssen. Diese Prozesse können sehr langsam und schmerzhaft sein, wenn Sie viele Daten haben oder an die Zeit gebunden sind.


2
Dies liest sich eher wie ein tangentialer Kommentar, siehe Wie man antwortet
Mücke

1
Nun, es ist nicht so: Es gibt keine Domains. Es gibt Klassen. Am Ende ist alles ein Textproblem (oh, ich entwickle all meine Dinge in C ++, aber das muss kein Problem sein)
Marko Bencik
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.