Notre logiciel fait abstraction du matériel, et nous avons des classes qui représentent l'état de ce matériel et ont beaucoup de membres de données pour toutes les propriétés de ce matériel externe. Nous devons régulièrement mettre à jour d'autres composants sur cet état, et pour cela nous envoyons des messages codés en protobuf via MQTT et d'autres protocoles de messagerie. Il y a différents messages qui décrivent différents aspects du matériel, donc nous devons envoyer différentes vues des données de ces classes. Voici une esquisse :
struct some_data {
Foo foo;
Bar bar;
Baz baz;
Fbr fbr;
// ...
};
Supposons que nous ayons besoin d'envoyer un message contenant foo
y bar
et un autre contenant bar
y baz
. Notre façon actuelle de procéder consiste en une multitude de textes passe-partout :
struct foobar {
Foo foo;
Bar bar;
foobar(const Foo& foo, const Bar& bar) : foo(foo), bar(bar) {}
bool operator==(const foobar& rhs) const {return foo == rhs.foo && bar == rhs.bar;}
bool operator!=(const foobar& rhs) const {return !operator==(*this,rhs);}
};
struct barbaz {
Bar bar;
Baz baz;
foobar(const Bar& bar, const Baz& baz) : bar(bar), baz(baz) {}
bool operator==(const barbaz& rhs) const {return bar == rhs.bar && baz == rhs.baz;}
bool operator!=(const barbaz& rhs) const {return !operator==(*this,rhs);}
};
template<> struct serialization_traits<foobar> {
static SerializedFooBar encode(const foobar& fb) {
SerializedFooBar sfb;
sfb.set_foo(fb.foo);
sfb.set_bar(fb.bar);
return sfb;
}
};
template<> struct serialization_traits<barbaz> {
static SerializedBarBaz encode(const barbaz& bb) {
SerializedBarBaz sbb;
sfb.set_bar(bb.bar);
sfb.set_baz(bb.baz);
return sbb;
}
};
Celui-ci peut alors être envoyé :
void send(const some_data& data) {
send_msg( serialization_traits<foobar>::encode(foobar(data.foo, data.bar)) );
send_msg( serialization_traits<barbaz>::encode(barbaz(data.foo, data.bar)) );
}
Étant donné que les ensembles de données à envoyer sont souvent beaucoup plus grands que deux éléments, que nous devons également décoder ces données et que nous avons des tonnes de ces messages, il y a beaucoup plus de texte passe-partout impliqué que ce qui se trouve dans ce croquis. J'ai donc cherché un moyen de réduire ce nombre. Voici une première idée :
typedef std::tuple< Foo /* 0 foo */
, Bar /* 1 bar */
> foobar;
typedef std::tuple< Bar /* 0 bar */
, Baz /* 1 baz */
> barbaz;
// yay, we get comparison for free!
template<>
struct serialization_traits<foobar> {
static SerializedFooBar encode(const foobar& fb) {
SerializedFooBar sfb;
sfb.set_foo(std::get<0>(fb));
sfb.set_bar(std::get<1>(fb));
return sfb;
}
};
template<>
struct serialization_traits<barbaz> {
static SerializedBarBaz encode(const barbaz& bb) {
SerializedBarBaz sbb;
sfb.set_bar(std::get<0>(bb));
sfb.set_baz(std::get<1>(bb));
return sbb;
}
};
void send(const some_data& data) {
send_msg( serialization_traits<foobar>::encode(std::tie(data.foo, data.bar)) );
send_msg( serialization_traits<barbaz>::encode(std::tie(data.bar, data.baz)) );
}
J'ai réussi à faire fonctionner ce système, et il réduit considérablement le texte passe-partout. (Pas dans ce petit exemple, mais si vous imaginez qu'une douzaine de points de données sont codés et décodés, la disparition de nombreuses listes répétitives de membres de données fait une grande différence). Cependant, cela présente deux inconvénients :
-
Cela repose sur
Foo
,Bar
yBaz
étant des types distincts. S'ils sont tousint
nous devons ajouter un type de balise fictif au tuple.C'est possible, mais cela rend cette idée beaucoup moins attrayante.
-
Ce qui était des noms de variables dans l'ancien code devient des commentaires et des numéros dans le nouveau code. C'est plutôt mauvais, et étant donné qu'il est probable qu'un bogue confondant deux membres soit présent aussi bien dans l'encodage que dans le décodage, il ne peut pas être attrapé dans de simples tests unitaires, mais nécessite des composants de test créés par d'autres technologies (donc des tests d'intégration) pour attraper de tels bogues.
Je n'ai aucune idée de la façon de réparer cela.
Quelqu'un a-t-il une meilleure idée de la façon de réduire le texte passe-partout pour nous ?
Note :
- Pour l'instant, nous sommes coincés avec C++03. Oui, vous avez bien lu. Pour nous, c'est
std::tr1::tuple
. Pas de lambda. Et pas deauto
soit. - Nous avons une tonne de code qui utilise ces traits de sérialisation. Nous ne pouvons pas jeter tout le système et faire quelque chose de complètement différent. Je suis à la recherche d'une solution pour simplifier l'intégration du code futur dans le cadre existant. Toute idée qui nous oblige à tout réécrire sera très probablement rejetée.
2 votes
Il semble que vous souhaitiez écrire un programme qui lise un fichier dans un langage simple et génère ensuite tout le code C++ pour vous, que vous compilez ensuite. Un générateur de code pour la victoire. Un simple analyseur yacc/bison avec une grammaire simple peut même faire l'affaire.
0 votes
@JesperJuhl : C'est en effet l'une des solutions que nous avons envisagées. Je préfère cependant trouver une solution en C++, plutôt que d'ajouter encore un autre générateur de code à notre processus de construction que les gens devront maintenir lorsque les programmeurs actuels auront tous pris leur retraite depuis longtemps...
0 votes
Puisque les messages sont codés en protobuf, pourquoi ne pas générer le code avec protobuf ?
0 votes
@Jens : Avez-vous des indications sur la façon d'écrire un backend protobuf ? Cependant, c'était l'une des nombreuses simplifications de cette question : Nos messages sont principalement protobuf-encodé. Actuellement . Nous avons déjà quelques projets où nous envoyons des messages JSON. Et qui sait ce que nous ferons à l'avenir...
2 votes
"... maintenir quand les programmeurs actuels auront tous pris leur retraite depuis longtemps..." - Vous êtes l'un de ces "programmeurs actuels". Lorsque vous serez à la retraite, vous n'aurez plus besoin de vous en soucier ;-) (veuillez noter le smiley qui fait un clin d'œil).
0 votes
@sbi Je n'ai jamais eu besoin d'écrire un backend. Que voulez-vous réaliser avec un backend personnalisé ? Les messages peuvent être sérialisés et désérialisés vers/depuis, par exemple, des flux et ensuite être envoyés avec n'importe quelle bibliothèque de messagerie que vous voulez. Je l'ai utilisé avec zeroMQ. Si je voulais utiliser le format JSON, j'utiliserais une bibliothèque JSON.
0 votes
Vous pouvez automatiser la génération du modèle de base avec des macros. Est-ce inacceptable ?
0 votes
@Jesper : J'aime ce que nous faisons.
:-)
0 votes
@sbi Je connais ce sentiment :)
0 votes
@Jens : Supposons que
SerializedFooBar
pour être un type généré par protobuf. Et maintenant ?0 votes
@sbi Peut-être que je ne comprends pas la question, mais vous pouvez
obj.SerializeToOstream(&output)
oobj.ParseFromIstream(&input)
toutSerializedFooBar obj;
. A quoi vous attendez-vous ?0 votes
@jhx : Les macros ne sont pas agréables, mais lorsqu'elles réduisent la charge de travail, elles sont acceptables.
0 votes
@Jens : Cette question concerne la traduction entre des ensembles spécifiques de membres de données et des types générés par protobuf. (Ou d'autres types sérialisables.) Ces types sérialisables sont alors effectivement sérialisés selon leur spécification.
0 votes
Votre préprocesseur comprend-il les arguments variables pour la syntaxe des macros ? Êtes-vous capable d'utiliser P99 ?
0 votes
@jxh : Malheureusement non.
0 votes
Comment se fait-il que votre système ait décidé de ne pas créer d'encodeur personnalisé pour
some_data
elle-même ?0 votes
@jxh : Comme je l'ai écrit, il pourrait y avoir un nombre quelconque de messages envoyés transportant des données copiées à partir de différents membres de
some_data
. Mais c'est simplifié. Il existe également des messages qui combinent des membres de différentes classes en un seul message.0 votes
Pouvez-vous modifier la classe SerializedXY ? Par exemple, pourriez-vous avoir Serialised<X,Y> à la place ? Si oui, quelle interface souhaitez-vous pour cette classe ? J'ai pensé à une solution possible en utilisant boost fusion... êtes-vous autorisé à utiliser boost ?
0 votes
Umm, une chose dont je semble me souvenir est que par le design un message Protobuf peut être construit à partir de concaténations de charges utiles. Pouvez-vous exploiter cela d'une manière ou d'une autre ?
0 votes
Pourquoi avez-vous besoin de combiner les messages, par exemple, pourquoi est-ce que
foo
ybar
combinée à un message foobar ? Ces ensembles de données sont-ils liés d'une manière ou d'une autre ? Pour moi, il semble plutôt que le message passe-partout ne soit qu'une combinaison inutile de données ! Par ailleurs, est-il possible de modifier légèrement les structures de données (struct Foo, Bar, etc.), par exemple en leur ajoutant une fonction ? Et comment décodez-vous les messages ?0 votes
@jhx : Pouvez-vous être plus précis ? Quel système ? Quel encodeur ?
0 votes
@linuxfever : Le site
Serialized...
sont générées. Je n'ai aucun contrôle sur elles.0 votes
@IwillnotexistIdonotexist : Je ne sais pas. A toi de me le dire !
0 votes
@user1810087 : La façon dont les données sont structurées dans nos applications est le résultat de décisions d'implémentation. La façon dont les données publiées sont structurées est le résultat de décisions spécifiques au projet. Souvent, ces données doivent correspondre à des interfaces externes sur lesquelles nous n'avons aucun contrôle. On nous demande (à juste titre, IMO) de faire abstraction de notre architecture interne et de faire la traduction entre celle-ci et de nombreuses demandes externes différentes.
0 votes
Je vois. Pouvez-vous développer un peu plus le problème n°1 ? Si tous les membres sont des int, std::get<0>, std::get<1>... récupérera toujours le bon élément, non ? De même, pour le problème n°2, pourquoi ne pouvons-nous pas écrire notre propre fonction get<T> où T peut être le nom de votre type (Foo, Bar, etc.) ?
0 votes
Le problème est que
serialization_traits<foo>
yserialization_traits<bar>
sont du même type, lorsquefoo
ybar
sont des tuples avec les mêmes listes de types. Que ces listes de types soient sémantiquement différentes n'a aucune importance pour la syntaxe.0 votes
Il semble que vous essayez essentiellement d'écrire une couche de généralisation qui fera abstraction des nombreuses
SerializedXY
permettant d'y accéder par une interface simplifiée (votre choix étant les spécialisations deserialization_traits
). Est-ce un bon résumé ? De ce point de vue, les choses que vous avez essayées sont utiles en tant qu'exemple de ce que vous recherchez, mais des informations sur les points suivants seraient encore plus utilesSerializedXY
types. Pourriez-vous ajouter à votre question quelques informations à leur sujet, comme la façon dont ils sont générés (pourquoi cela ne peut pas changer) et quelle est leur interface publique ?0 votes
@JaMiT : Le
Serialized...
sont générés à partir d'une certaine IDL. Actuellement, la plupart d'entre eux sont générés par protobuf et certains sont des conteneurs JSON, mais cela pourrait changer. Nous voulons les garder en dehors de notre code, car nous avons peu de contrôle sur leur apparence et parce qu'ils pourraient changer. C'est l'une des raisons de cette couche de traduction pour copier entre nos données internes et elles.0 votes
Il y a une différence entre "peu de contrôle" et "aucun contrôle". Si vous n'aviez aucun contrôle, alors peut-être qu'un jour le
SerializedFooBar
renommerait sa classeset_foo
pour simplementfoo
tandis que l'autreSerializedFooY
les classes conserventset_foo
. Est-ce une possibilité ? Si c'est le cas, il s'agit d'une information essentielle dans la mesure où elle invalide certaines approches. Dans le cas contraire, il s'agit d'une information sur leur interface publique (comme je l'ai demandé). Sur quoi avez-vous un contrôle ? Que peut-on supposer à leur sujet ? Pourquoi ont-ils ce bel uniformeSerializedXY
système de dénomination ? Est-il sujet à des changements extérieurs ?0 votes
J'aimerais reconnaître que "généré à partir de certains IDL" répond à l'une des clarifications que j'ai demandées. Cependant, j'ai demandé que la clarification soit placée dans la question, et non enterrée dans les commentaires... (Pendant que je commente, j'ajouterai : qui génère ces définitions de type ?)
0 votes
@JaMiT : Eh bien, la
Serialized...
sont générés à partir de notre IDL, nous avons donc un certain contrôle. Cependant, ils ne sont pas générés directement (actuellement nous générons des fichiers protobuf, à partir desquels le C++ est généré) et leur interface exacte dépend de l'interface qui peut changer à tout moment. Donc, oui,set_foo()
pourrait changer. C'est pourquoi nous avons les traits de sérialisation, après tout : ils sont censés isoler notre code de ces interfaces externes. Je veux juste que ce soit un peu plus déclaratif, et moins répétitif.