Modèles de conception Protobuf

19

J'évalue les tampons de protocole Google pour un service basé sur Java (mais j'attends des schémas indépendants du langage). J'ai deux questions:

La première est une vaste question générale:

Quels modèles voyons-nous les gens utiliser? Ces schémas étant liés à l'organisation des classes (par exemple, messages par fichier .proto, conditionnement et distribution) et à la définition des messages (par exemple, champs répétés vs champs encapsulés répétés *), etc.

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

J'ai également des questions spécifiques sur les deux modèles suivants:

  1. Représentez les messages dans des fichiers .proto, empaquetez-les dans un bocal séparé et envoyez-le aux clients cibles du service - ce qui est fondamentalement l'approche par défaut, je suppose.

  2. Faites de même, mais incluez également des wrappers fabriqués à la main (pas des sous-classes!) Autour de chaque message qui implémentent un contrat prenant en charge au moins ces deux méthodes (T est la classe wrapper, V est la classe du message (en utilisant des génériques mais une syntaxe simplifiée pour plus de concision) :

    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. dans mes wrappers. Le deuxième avantage que je vois avec (2) est que mes clients traitent des objets immuables (quelque chose que je peux appliquer dans la classe wrapper).

Un inconvénient que je vois avec (2) est que je duplique du code et que je dois synchroniser mes classes wrapper avec des fichiers .proto.

Quelqu'un a-t-il de meilleures techniques ou d'autres critiques sur l'une des deux approches?


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

message ItemList {
    repeated item = 1;
}

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

au lieu de messages comme 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.

Apoorv Khurasia
la source
J'ai besoin de 12 points supplémentaires pour créer de nouveaux tags et je pense que protobuf devrait être un tag pour ce post.
Apoorv Khurasia

Réponses:

13

Là où je travaille, la décision a été prise de cacher l'utilisation de protobuf. Nous ne distribuons pas les .protofichiers entre les applications, mais plutôt, toute application qui expose une interface protobuf exporte une bibliothèque client qui peut lui parler.

Je n'ai travaillé que sur une de ces applications exposant des protobufs, mais en ce sens, chaque message protobuf correspond à un concept du domaine. Pour chaque concept, il existe une interface Java normale. Il y a ensuite 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 une instance d'une implémentation de l'interface (comme il arrive, généralement une simple classe anonyme ou locale définie à l'intérieur du convertisseur). Les classes de messages et les convertisseurs générés par protobuf forment ensemble une bibliothèque qui est utilisée à la fois par l'application et la bibliothèque cliente; la bibliothèque cliente ajoute une petite quantité de code pour établir des connexions et envoyer et recevoir des messages.

Les applications clientes importent ensuite la bibliothèque cliente et fournissent les implémentations de toutes les interfaces qu'elles souhaitent envoyer. En effet, les deux parties font la dernière chose.

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

  • Message PartyInvitation, écrit dans le .protofichier
  • 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 PartyInvitationdéfinie par l'application cliente (pas réellement appelée comme ça!)
  • StubPartyInvitation, une implémentation simple de PartyInvitationdéfinie par la bibliothèque partagée
  • PartyInvitationConverter, qui peut convertir a PartyInvitationen a PartyInvitationMessageet a PartyInvitationMessageenStubPartyInvitation
  • Message RSVP, écrit dans le .protofichier
  • 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 RSVPdéfini par l'application serveur (également pas réellement appelé ça!)
  • StubRSVP, une implémentation simple de RSVPdéfinie par la bibliothèque partagée
  • RSVPConverter, qui peut convertir un RSVPen un RSVPMessageet un RSVPMessageen unStubRSVP

La raison pour laquelle nous avons des implémentations réelles et stub séparées est que les implémentations réelles sont généralement des classes d'entités mappées JPA; le serveur les crée et les conserve ou les interroge à partir de la base de données, puis les transmet à la couche de protobuf à transmettre. Il n'a pas été jugé approprié de créer des instances de ces classes du côté récepteur de la connexion, car elles ne seraient pas liées à un contexte de persistance. De plus, les entités contiennent souvent un peu plus de données que ce qui est transmis sur le câble, il ne serait donc même pas possible de créer des objets intacts du côté récepteur. Je ne suis pas entièrement convaincu que c'était la bonne décision, car cela nous a laissé une classe de plus par message que nous n'aurions autrement.

En effet, je ne suis pas entièrement convaincu que l'utilisation de protobuf était une bonne idée; si nous étions restés fidèles à l'ancien RMI et à la sérialisation, nous n'aurions pas dû créer presque autant d'objets. Dans de nombreux cas, nous aurions pu simplement marquer nos classes d'entités comme sérialisables et continuer.

Maintenant, après avoir dit tout cela, j'ai un ami qui travaille chez Google, sur une base de code qui fait un usage intensif de protobuf pour la communication entre les modules. Ils adoptent une approche complètement différente: ils n'encapsulent pas du tout les classes de messages générés et les transmettent avec enthousiasme (ish) profondément dans leur code. Cela est considéré comme une bonne chose, car c'est un moyen simple de maintenir la flexibilité des interfaces. Il n'y a pas de code d'échafaudage à synchroniser lorsque les messages évoluent, et les classes générées fournissent toutes les hasFoo()méthodes nécessaires pour recevoir du code afin de détecter la présence ou l'absence de champs qui ont été ajoutés au fil du temps. Gardez à l'esprit, cependant, que les gens qui travaillent chez Google ont tendance à être (a) plutôt intelligents et (b) un peu fous.

Tom Anderson
la source
À un moment donné, j'ai envisagé d'utiliser la sérialisation JBoss comme remplacement plus ou moins direct pour la sérialisation standard. C'était beaucoup plus rapide. Mais pas aussi vite que protobuf.
Tom Anderson
La sérialisation JSON à l'aide de jackson2 est également assez rapide. La chose que je déteste à propos de GBP est la duplication inutile des principales classes d'interface.
Apoorv Khurasia
0

Pour ajouter à la réponse d'Andersons, il y a une ligne fine dans l'imbrication intelligente des messages les uns dans les autres et leur utilisation excessive. Le problème est que chaque message crée une nouvelle classe dans les coulisses et toutes sortes d'accesseurs et de gestionnaires pour les données. Mais cela a un coût si vous devez copier les données ou modifier une valeur ou comparer les messages. Ces processus peuvent être très lents et pénibles à réaliser si vous avez beaucoup de données ou si vous êtes limité par le temps.

Marko Bencik
la source
2
cela ressemble plus à un commentaire tangentiel, voir Comment répondre
gnat
1
Eh bien ce n'est pas le cas: il n'y a pas de domaines, il y a des classes, c'est un problème de formulation à la fin (oh je développe toutes mes choses en C ++ mais cela ne doit pas être un problème)
Marko Bencik