Modelli di design Protobuf


19

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

La prima è un'ampia domanda generale:

Quali schemi stiamo vedendo la gente usare? Detti schemi sono collegati all'organizzazione della classe (ad es. Messaggi per file .proto, impacchettamento e distribuzione) e alla definizione del messaggio (ad es. Campi ripetuti vs. campi incapsulati ripetuti *) ecc.

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 schemi:

  1. Rappresenta i messaggi nei file .proto, li impacchetta come un vaso separato e li spedisce ai destinatari del servizio, che è fondamentalmente l'approccio predefinito immagino.

  2. Fai lo stesso ma includi anche wrapper fatti a mano (non sottoclassi!) Attorno a ciascun messaggio che implementa un contratto che supporti almeno questi due metodi (T è la classe wrapper, V è la classe del messaggio (usando generici 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 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 hanno a che fare con oggetti immutabili (qualcosa che posso applicare nella classe wrapper).

Uno svantaggio che vedo con (2) è che 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;
}

invece 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 contrari.


Ho bisogno di altri 12 punti per creare nuovi tag e penso che protobuf dovrebbe essere un tag per questo post.
Apoorv Khurasia,

Risposte:


13

Dove lavoro, è stata presa la decisione di nascondere l'uso di protobuf. Non distribuiamo i .protofile tra le applicazioni, ma piuttosto qualsiasi applicazione che espone un'interfaccia protobuf esporta una libreria client che può parlare con essa.

Ho lavorato solo su una di queste applicazioni che espongono il protobuf, ma in ciò ogni messaggio di protobuf corrisponde a un concetto nel dominio. Per ogni concetto, esiste una normale interfaccia Java. Esiste quindi una classe di convertitore, che può prendere un'istanza di un'implementazione e costruire un oggetto messaggio appropriato, 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 di messaggi e i convertitori 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 impostare connessioni e inviare e ricevere messaggi.

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

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

  • Messaggio di PartyInvitation, scritto nel .protofile
  • PartyInvitationMessage classe, generata da protoc
  • PartyInvitation interfaccia, definita nella libreria condivisa
  • ActualPartyInvitation, un'implementazione concreta PartyInvitationdefinita dall'app client (in realtà non chiamata così!)
  • StubPartyInvitation, una semplice implementazione di PartyInvitationdefinita dalla libreria condivisa
  • PartyInvitationConverter, che può convertire a PartyInvitationin a PartyInvitationMessagee a PartyInvitationMessagein aStubPartyInvitation
  • Messaggio RSVP, scritto nel .protofile
  • RSVPMessage classe, generata da protoc
  • RSVP interfaccia, definita nella libreria condivisa
  • ActualRSVP, un'implementazione concreta di RSVPdefinita dall'app server (che in realtà non l'ha chiamata!)
  • StubRSVP, una semplice implementazione di RSVPdefinita dalla libreria condivisa
  • RSVPConverter, che può convertire an RSVPin an RSVPMessagee an RSVPMessagein aStubRSVP

Il motivo per cui abbiamo implementazioni separate e effettive stub è che le implementazioni effettive sono generalmente classi di entità mappate su JPA; il server li crea e li persiste oppure li interroga dal database, quindi li passa al livello protobuf da trasmettere. Non è stato ritenuto appropriato creare istanze di quelle classi sul lato ricevente della connessione, poiché non sarebbero state legate a un contesto di persistenza. Inoltre, le entità contengono spesso un numero di dati piuttosto elevato rispetto a quello 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 una classe in più per messaggio di quanto avremmo altrimenti.

In effetti, non sono del tutto convinto che usare il protobuf sia stata una buona idea; se avessimo bloccato con la semplice vecchia RMI e la serializzazione, non avremmo dovuto creare quasi altrettanti oggetti. In molti casi, avremmo potuto semplicemente contrassegnare le nostre classi di entità come serializzabili e andare avanti con esso.

Ora, detto tutto ciò, ho un amico che lavora in Google, su una base di codice che fa un uso intensivo di protobuf per la comunicazione tra i moduli. Adottano un approccio completamente diverso: non racchiudono affatto le classi di messaggi generate e le passano con entusiasmo nel loro codice. Questo è visto come una cosa positiva, perché è un modo semplice per mantenere flessibili le interfacce. Non esiste un codice di scaffolding da mantenere sincronizzato quando i messaggi si evolvono e le classi generate forniscono tutti i hasFoo()metodi 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 su Google tendono ad essere (a) piuttosto intelligenti e (b) un po 'pazze.


A un certo punto, ho esaminato l'uso della serializzazione JBoss come sostituto più o meno drop-in per la serializzazione standard. È stato molto più veloce. Non veloce come il protobuf, però.
Tom Anderson,

Anche la serializzazione JSON tramite jackson2 è piuttosto veloce. La cosa che odio di GBP è la duplicazione non necessaria delle principali classi di interfaccia.
Apoorv Khurasia,

0

Per aggiungere la risposta di Anderson, c'è una linea sottile nel nidificare i messaggi in modo intelligente l'uno nell'altro e nel superarlo. Il problema è che ogni messaggio crea una nuova classe dietro le quinte e tutti i tipi di accessori e gestori per i dati. Ma c'è un costo 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 dispone di molti dati o se si è vincolati dal tempo.


2
questo sembra più un commento tangenziale, vedi Come rispondere
moscerino il

1
Beh, non lo è: non ci sono domini, ci sono classi, è tutto un problema di formulazione alla fine (oh, sto sviluppando tutte le mie cose in C ++, ma questo non deve essere un problema ape)
Marko Bencik,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.