web-dev-qa-db-fra.com

Modèles de conception protobuf

J'évalue des tampons de protocole Google pour A Java service basé (mais je m'attends à des motifs agnostiques linguistiques). J'ai deux questions:

Le premier est une question générale large:

Quels motifs voyons-nous que les gens utilisent-ils? Lesdits modèles étant liés à l'organisation de la classe (par exemple, messages par fichier .Proto, emballage et distribution) et définition de message (par exemple, champs répétés par rapport aux champs encapsulés répétés *), etc.

Il existe très peu d'informations sur ce type sur les pages d'aide Google Protobuf et des blogs publics, tandis que des informations sont des informations pour des protocoles établis tels que XML.

J'ai aussi des questions spécifiques sur les deux modèles différents suivants:

  1. Représenter les messages dans les fichiers .poto, les emballer comme un pot distinct et l'expédier pour cibler les consommateurs du service - qui est fondamentalement l'approche par défaut que je suppose.

  2. Faites de même mais d'inclure également des wrappers fabriqués à la main (pas sous-classes!) Autour de chaque message qui implémente un contrat prenant en charge au moins ces deux méthodes (T est la classe d'enveloppe, V est la classe de message (utilisant des génériques mais la syntaxe simplifiée pour la brièveté) :

    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 avantage que je vois avec (2) est que je peux cacher les complexités introduites par V.newBuilder().addField().build() et ajouter des méthodes significatives telles que isOpenForTrade() ou isAddressInFreeDeliveryZone() etc. mes wrappers. Le deuxième avantage que je vois avec (2) est que mes clients traitent d'objets immuables (quelque chose que je peux appliquer dans la classe wrapper).

Un inconvénient que je vois avec (2) est que j'ai dupliquer le code et devez synchroniser mes classes d'emballage avec des fichiers .poto.

Quelqu'un a-t-il de meilleures techniques ou des critiques supplémentaires sur l'une des deux approches?


* En encapsulant un champ répété, je veux dire des messages tels que celui-ci:

message ItemList {
    repeated item = 1;
}

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

au lieu de messages tels que celui-ci:

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

J'aime ce dernier mais je suis heureux d'entendre des arguments contre elle.

23
Apoorv Khurasia

Là où je travaille, la décision a été prise pour dissimuler l'utilisation de Protobuf. Nous ne distribuons pas les fichiers .proto Entre les applications, mais plutôt toute application qui expose une interface protobuf exporte une bibliothèque cliente qui peut en parler.

Je n'ai travaillé que sur l'une de ces applications de probuf-exposant, mais dans ce cas, chaque message protobuf correspond à un concept dans le domaine. Pour chaque concept, il y a une interface Normal Java. Il existe alors une classe de convertisseur, qui peut prendre une instance d'une implémentation et construire un objet de message approprié et prendre un objet de message et construire un objet instance d'une implémentation de l'interface (comme il se produit, généralement une simple classe anonyme ou locale définie à l'intérieur du convertisseur). Les classes de messages générées par Protobuf forment ensemble une bibliothèque utilisée à la fois par l'application et la bibliothèque client; le La bibliothèque cliente ajoute une petite quantité de code permettant de configurer les connexions et l'envoi et la réception de messages.

Applications clients Importez ensuite la bibliothèque cliente et fournissez des implémentations de toutes les interfaces qu'ils souhaitent envoyer. En effet, les deux côtés font la dernière chose.

Pour clarifier, cela signifie que si vous avez un cycle de requête-réponse où le client envoie une invitation à la partie, et que le serveur répond avec un RSVP, les choses impliquées sont les suivantes:

  • Message PartyInvitation, écrit dans le fichier .proto
  • PartyInvitationMessage classe, générée par protoc
  • PartyInvitation interface, définie dans la bibliothèque partagée
  • ActualPartyInvitation, une implémentation concrète de PartyInvitation défini par l'application client (non réellement appelée ça!)
  • StubPartyInvitation, une simple implication de PartyInvitation défini par la bibliothèque partagée
  • PartyInvitationConverter, qui peut convertir un PartyInvitation à un PartyInvitationMessage et un PartyInvitationMessage à un StubPartyInvitation
  • Message RSVP, écrit dans le fichier .proto
  • RSVPMessage classe, générée par protoc
  • RSVP interface, définie dans la bibliothèque partagée
  • ActualRSVP, une implémentation concrète de RSVP défini par l'application Server (non pas réellement appelée ça!)
  • StubRSVP, une simple implication de RSVP défini par la bibliothèque partagée
  • RSVPConverter, qui peut convertir un RSVP à un RSVPMessage et un RSVPMessage à un StubRSVP

La raison pour laquelle nous avons des implémentations réelles et des mises en œuvre distinctes est que les implémentations réelles sont généralement des classes d'entité mappées JPA; Le serveur les crée et le persiste, ou les interroge dans la base de données, puis les maintient à la couche protobuf à transmettre. Il n'a pas été estimé qu'il était approprié de créer des instances de ces classes du côté de la connexion, car ils ne seraient pas liés à un contexte de persistance. En outre, les entités contiennent souvent des données plutôt que de transmises sur le fil. Il ne serait même pas possible de créer des objets intacts sur le côté de la réception. Je ne suis pas entièrement convaincu que c'était le bon geste, car cela nous a laissé un autre classe par message que nous aurions autrement.

En effet, je ne suis pas entièrement convaincu que l'utilisation du protoBuf était une bonne idée; Si nous nous sommes bloqués avec un ancien RMI et une sérialisation uni, nous n'aurions pas dû créer presque autant d'objets. Dans de nombreux cas, nous pourrions simplement avoir marqué nos classes d'entité aussi sérialisables et y avoir eues.

Maintenant, après avoir dit tout cela, j'ai un ami qui travaille sur Google sur une base de code qui utilise de manière intensive de protobuf pour la communication entre les modules. Ils prennent une approche complètement différente: ils n'enveloppent pas du tout les classes de messages générés et les transmettent avec enthousiasme en profondeur (ish) dans leur code. Ceci est considéré comme une bonne chose, car c'est une simple façon de garder les interfaces flexibles. Il n'y a pas de code d'échafaudage à conserver en synchronisation lorsque les messages évoluent et les classes générées fournissent toutes les méthodes nécessaires hasFoo() nécessaires au code de la réception de la présence ou l'absence de champs qui ont été ajoutés au fil du temps. N'importe empêche cependant que les personnes qui travaillent sur Google ont tendance à être (a) plutôt intelligentes et (b) un peu de noix.

16
Tom Anderson

Pour ajouter sur Andersons Répondre Il y a une ligne fine dans la nidification de messages de nidification intelligemment une dans une autre et la dépassant. Le problème est que chaque message crée une nouvelle classe derrière les scènes et toutes sortes d'accesseurs et de gestionnaires pour les données. Mais il y a un coût pour que si vous devez copier les données ou modifier une valeur ou comparer les messages. Ces processus peuvent être très lents et douloureux de faire si vous avez beaucoup de données ou que vous êtes lié par le temps.

0
Marko Bencik