Modelli di design di Protobuf

19

Sto valutando i buffer del protocollo di Google per un servizio basato su Java (ma mi aspetto modelli di agnostica linguistica). Ho due domande:

Il primo è una domanda generale generica:

What patterns are we seeing people use? Said patterns being related to class organization (e.g., messages per .proto file, packaging, and distribution) and message definition (e.g., repeated fields vs. repeated encapsulated fields*) etc.

Ci sono pochissime informazioni di questo tipo nelle pagine della Guida di Google Protobuf e nei blog pubblici, mentre ci sono un sacco di informazioni per protocolli consolidati come XML.

Ho anche domande specifiche sui seguenti due diversi modelli:

  1. Rappresenta i messaggi nei file .proto, li impacchettano come contenitori separati e li spediscono ai consumatori target del servizio, che è fondamentalmente l'approccio predefinito.

  2. Fai lo stesso, ma includi anche wrapper fatti a mano (non sottoclassi!) attorno ad ogni messaggio che implementa un contratto che supporta almeno questi due metodi (T è la classe wrapper, V è la classe message (usando i generics ma sintassi semplificata per brevità):

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

Un vantaggio che vedo con (2) è che posso nascondere le complessità introdotte da V.newBuilder().addField().build() e aggiungere alcuni metodi significativi come isOpenForTrade() o isAddressInFreeDeliveryZone() ecc. nei miei wrapper. Il secondo vantaggio che vedo con (2) è che i miei clienti si occupano di oggetti immutabili (qualcosa che posso applicare nella classe wrapper).

Uno svantaggio che vedo con (2) è che io duplico il codice e devo sincronizzare le mie classi wrapper con i file .proto.

Qualcuno ha tecniche migliori o ulteriori critiche su uno dei due approcci?

* Incapsulando un campo ripetuto intendo messaggi come questo:

message ItemList {
    repeated item = 1;
}

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

al posto di messaggi come questo:

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

Mi piace il secondo, ma sono felice di sentire argomenti contro di esso.

    
posta Apoorv Khurasia 21.10.2012 - 09:23
fonte

2 risposte

13

Dove lavoro, è stata presa la decisione di nascondere l'uso di protobuf. Non distribuiamo i file .proto tra le applicazioni, ma ogni applicazione che espone un'interfaccia protobuf esporta una libreria client che può parlarci.

Ho lavorato solo su una di queste applicazioni che espongono protobuf, ma in questo, ogni messaggio protobuf corrisponde ad alcuni concetti nel dominio. Per ogni concetto, esiste una normale interfaccia Java. Esiste quindi una classe del convertitore, che può prendere un'istanza di un'implementazione e costruire un oggetto messaggio appropriato, e prendere un oggetto messaggio e costruire un'istanza di un'implementazione dell'interfaccia (come accade, di solito una semplice classe anonima o locale definita all'interno del convertitore). Le classi e i convertitori di messaggi generati da protobuf formano insieme una libreria che viene utilizzata sia dall'applicazione che dalla libreria client; la libreria client aggiunge una piccola quantità di codice per l'impostazione delle connessioni e l'invio e la ricezione di messaggi.

Le applicazioni client quindi importano la libreria client e forniscono implementazioni di qualsiasi interfaccia che desiderano inviare. In effetti, entrambe le parti fanno quest'ultima cosa.

Per chiarire, ciò significa che se hai un ciclo di richiesta-risposta in cui il client sta inviando un invito a una festa e il server sta rispondendo con un RSVP, allora le cose coinvolte sono:

  • Messaggio di PartyInvitation, scritto nel file .proto
  • PartyInvitationMessage class, generata da protoc
  • PartyInvitation interfaccia, definita nella libreria condivisa
  • ActualPartyInvitation , un'implementazione concreta di PartyInvitation definita dall'app client (in realtà non chiamata così!)
  • StubPartyInvitation , una semplice implementazione di PartyInvitation definita dalla libreria condivisa
  • PartyInvitationConverter , che può convertire PartyInvitation in PartyInvitationMessage e PartyInvitationMessage in StubPartyInvitation
  • Messaggio RSVP, scritto nel file .proto
  • RSVPMessage class, generata da protoc
  • RSVP interfaccia, definita nella libreria condivisa
  • ActualRSVP , un'implementazione concreta di RSVP definita dall'app del server (anche in realtà non chiamata così!)
  • StubRSVP , una semplice implementazione di RSVP definita dalla libreria condivisa
  • RSVPConverter , che può convertire RSVP in RSVPMessage e RSVPMessage in StubRSVP

Il motivo per cui abbiamo implementazioni distinte effettive e stub è che le implementazioni effettive sono generalmente classi di entità mappate JPA; il server li crea e li persiste, o li interroga dal database, quindi li consegna al livello protobuf per essere trasmesso. Non è stato ritenuto opportuno creare istanze di tali classi sul lato di ricezione della connessione, in quanto non sarebbero legate a un contesto di persistenza. Inoltre, le entità contengono spesso un numero di dati maggiore di quello che viene trasmesso sul filo, quindi non sarebbe nemmeno possibile creare oggetti intatti sul lato ricevente. Non sono del tutto convinto che questa sia stata la mossa giusta, perché ci ha lasciato con una classe in più per messaggio di quella che altrimenti avremmo.

In effetti, non sono del tutto convinto che usare il protobuf fosse una buona idea; se fossimo rimasti con il vecchio vecchio RMI e la serializzazione, non avremmo dovuto creare quasi altrettanti oggetti. In molti casi, potremmo semplicemente aver contrassegnato le nostre classi di entità come serializzabili e averle acquisite.

Ora, detto tutto ciò, ho un amico che lavora su Google, su una base di codice che fa un uso pesante di protobuf per la comunicazione tra i moduli. Hanno un approccio completamente diverso: non avvolgono affatto le classi di messaggi generate e le inoltrano con entusiasmo nel loro codice (ish). Questo è visto come una buona cosa, perché è un modo semplice per mantenere flessibili le interfacce. Non c'è alcun codice di scaffolding da mantenere sincronizzato quando i messaggi evolvono e le classi generate forniscono tutti i necessari metodi di hasFoo() necessari per ricevere il codice per rilevare la presenza o l'assenza di campi che sono stati aggiunti nel tempo. Tieni presente, tuttavia, che le persone che lavorano in Google tendono a essere (a) piuttosto intelligenti e (b) un po 'matti.

    
risposta data 22.10.2012 - 01:01
fonte
0

Per aggiungere la risposta di Andersons, c'è una linea sottile nel raggruppare i messaggi in modo intelligente l'uno nell'altro e esagerare. Il problema è che ogni messaggio crea una nuova classe dietro le quinte e tutti i tipi di accessor e handler per i dati. Ma c'è un costo per questo se devi copiare i dati o cambiare un valore o confrontare i messaggi. Questi processi possono essere molto lenti e dolorosi da fare se si hanno molti dati o sono vincolati dal tempo.

    
risposta data 22.07.2018 - 16:52
fonte

Leggi altre domande sui tag