Dans tous les SI, on trouve des collections de données. Pour des raisons fonctionnelles ou historiques, ces données peuvent avoir plusieurs modélisations et plusieurs systèmes d’identification au sein du SI. Aujourd’hui, les départements et métiers d’une entreprise sont de plus en plus interconnectés, il faut donc mettre en place des systèmes de transcodification pour faire transiter ces données d’un référentiel à un autre.
Prenons le cas d’une collection de titres de musique. On aura plusieurs provider pour chaque titre :
- Itunes, pour les clients Apple
- Amazon, pour le mp3
- CDDB, pour obtenir des informations
- Spotify pour le streaming
Pour chaque morceau de musique les providers ont une modélisation et un système d’identifiant unique qui leur sont propres. Les titres de musique sont les données évoquées plus haut, les providers sont les métiers ou départements.
Le problème qui nous intéresse aujourd’hui c’est la transcodification, c’est-à-dire le mapping d’identifiant entre provider. Par exemple : à partir d’un ID Amazon, je veux connaitre l’ID CDDB. On veut un système rapide et adaptable facilement à tous types de collection. On exclut donc une solution reposant sur une base de données dédiée (lent et peu adaptable).
Disons que les données sont persistées dans un fichier XML (collection de titre de musique) :
<!-- … --> <titre> <Itunes>UnIdItunes<Itunes> <Amazon>UnIdAmazon</Amazon> <Spotify>unIdSpotify</Spotify> <CDDB>UnIdCDDB</CDDB> <description>Mickael Jackson, Thriller</description> </titre> <!-- … -->
L’implémentation va reposer sur boost::multindex. C’est un container qui gère une collection d’éléments et plusieurs indexes associés. Cela permet de limiter l’empreinte mémoire (les données ne sont pas dupliquées par index). Le boost::multindex maintient la cohérence des indexes avec l’ajout suppression des données.
On modélise un Titre ainsi:
struct Title { std::string itunes; std::string amazon; int cddb; // pour l’exemple , on prend un type different des autres std::string spotify; std::string description; };
Dans notre cas on a une collection de titres, on va les indexer dans chaque référentiel (Itunes, Amazon, CDDB, Spotify). La description n’est pas indexée, car il n’y a pas de référentiel associé et l’unicité n’est pas assurée.
Le container va s’écrire :
namespace bmi = boost::multi_index; typedef boost::multi_index_container< Title, bmi::indexed_by< bmi::ordered_unique< bmi::member< Title, std::string, &Title::itunes> >, bmi::ordered_unique< bmi::member< Title, std::string, &Title::amazon> >, bmi::ordered_unique< bmi::member< Title, int, &Title::cddb> >, bmi::ordered_unique< bmi::member< Title, std::string, &Title::spotify> > > > container;
On a donc une structure de données, un container, on va maintenant écrire la classe Transcoder qui exposera les méthodes de transcodification. On donne l’implementation du passage de Itunes -> Spotify.
class Transcoder { public: std::string getSpotifyfromItunes( const std::string& index ) { auto it = datas.get< 0 >().find( index ); if ( it == datas.get< 0 >().end() ) throw std::invalid_argument( index ); return it->spotify; } std::string getSpotifyfromAmazon( const std::string& index ); std::string getSpotifyfromCddb( int index ); std::string getItunesfromAmazon( const std::string& index ); // … private: container datas; };
Les problèmes commencent ici ! Pour implémenter toutes les transcos possibles, il faut écrire 12 méthodes getXXXfromYYYY qui sont pourtant très proches. C’est ici qu’intervient la méta-programmation. On va écrire une méthode get générique à base de Template, et le compilateur implémentera les 12 versions pour toutes les combinaisons de référentiels.
En méta-programmation, on va faire un usage massif des types fantômes. Il faut voir ces types comme des sortes de Tag ou des identifiants pour le compilateur. Dans la plupart des cas ce sont des structs qui n’ont pas de données membres. C’est le même principe que les traits.
On va créer des types fantômes pour décrire les champs d’un titre de musique de notre exemple. Tous les champs d’un titre ne sont pas std::string (par exemple : cddb est un int), on va donc ajouter cette information dans les traits. On va aussi renseigner le caractère indexable du champ d’un titre de musique. Dans notre exemple, la description d’un titre de musique n’est pas indexable.
Ce qui donne les traits suivants :
#include <boost/mpl/bool.hpp> struct itunes { typedef std::string value; typedef boost::mpl::true_ indexable; }; struct amazon { typedef std::string value; typedef boost::mpl::true_ indexable; }; struct cddb { typedef int value; typedef boost::mpl::true_ indexable; }; struct spotify { typedef std::string value; typedef boost::mpl::true_ indexable; }; struct description { typedef std::string value; typedef boost::mpl::false_ indexable; };
Et la signature de la méthode Transcoder::get générique :
template<typename FROM,typename TO> typename TO::value get( const typename FROM::value& index );
On y va pour l’implémentation de cette méthode, on va se baser sur l’exemple getSpotifyfromItunes
Problème : Dans l’exemple on avait « hardcodé » return it->spotify. Dans la méthode get Template, le référentiel destination (spotify) est porté par le type TO. On veut la valeur de l’index du référentiel TO.
Pour cela on va écrire Title autrement et le passer en méta-programmation. On va utiliser une boost::fusion::map . C’est un dictionnaire clefs, valeurs. Les clefs n’ont pas de données, ce sont des types (traits). Appliqué à notre exemple, on va stocker les données d’un titre de musique dans une boost::fusion::map :
typedef boost::fusion::result_of::make_map < itunes, amazon, cddb, spotify, description, itunes::value, amazon::value, cddb::value, spotify::value, description::value >::type key_value_map;
Pour accéder à un élément, on a boost::fusion::at_key.
Rappel : on est en méta-programmation, c’est-à-dire que le code va être généré par le compilateur, la complexité algorithmique sera en 0(1). L’accès à un élément de boost::fusion::map est sinon gratuit au moins constant.
On va donc faire une classe qui gère la boost::fusion::map et expose une méthode index pour accéder à un élément. Elle est plus générique que Title, elle dépend simplement des traits qu’on lui passe, on l’appelle item.
#include <boost/fusion/sequence.hpp> #include <boost/fusion/include/map.hpp> #include <boost/fusion/include/make_map.hpp> namespace bf = boost::fusion; template <typename K0, typename K1, typename K2, typename K3, typename K4> class item { public: typedef typename bf::result_of::make_map < K0, K1, K2, K3, K4, typename K0::value, typename K1::value, typename K2::value, typename K3::value, typename K4::value >::type key_value_map; template <typename Key> typename Key::value index() const { return bf::at_key<Key>( kv_map ); } private: key_value_map kv_map; };
On peut maintenant réécrire notre classe Title à partir d’une spécialisation d’item :
typedef item<itunes, amazon, cddb, spotify, description> Title;
On va retravailler un peu le container pour le rendre plus générique, sur le principe d’item. Il aura les mêmes paramètres Template qu’item. On utilise une autre signature du constructeur de boost::multi_index_container plus adaptée à notre contexte.
#include <boost/type_traits/is_same.hpp> #include <boost/fusion/view/single_view.hpp> #include <boost/fusion/algorithm.hpp> #include <boost/mpl/joint_view.hpp> #include <boost/mpl/transform_view.hpp> #include <boost/mpl/copy_if.hpp> #include <boost/multi_index_container.hpp> #include <boost/multi_index/sequenced_index.hpp> #include <boost/multi_index/ordered_index.hpp> #include "Item.h" namespace bmi = boost::multi_index; namespace bf = boost::fusion; template< typename K > struct getIndex // Syntaxic sugar { typedef typename K::value result_type; template< typename ITEM > result_type operator() ( const ITEM& item )const { return item.index<K>(); } }; template < typename K > struct makeIndex { typedef bmi::ordered_unique< bmi::tag<K>, getIndex<K> > type; }; template < typename KEYS > struct all_keys { typedef typename boost::mpl::transform < KEYS, makeIndex<boost::mpl::_> >::type type; }; template <typename K0, typename K1, typename K2, typename K3, typename K4> struct container { typedef boost::fusion::vector<K0, K1, K2, K3, K4> keys; typedef typename item<K0, K1, K2, K3, K4> item_t; typedef boost::multi_index_container < item_t, typename boost::mpl::joint_view< bf::single_view< bmi::sequenced<> >, typename all_keys<keys>::type >::type > type; };
Dans les grandes lignes, on a :
- getIndex qui donne la valeur d’un index pour un référentiel (type template).
- MakeIndex et all_keys qui nous permettent de créer les types nécessaires à la construction du boost::multi_index_container.
- Le type container dont la définition est un peu différente, c’est simplement pour faciliter l’écriture (et la lecture !) du code.
Il nous reste maintenant à filtrer les référentiels indexables. Pour cela on va regarder parmi les types template (K0, K1, K2, K3, K4) lesquels ont un alias de type indexable qui vaut boost::mpl::true_. Et voici la version finale du container :
#include <boost/type_traits/is_same.hpp> #include <boost/fusion/view/single_view.hpp> #include <boost/fusion/algorithm.hpp> #include <boost/mpl/joint_view.hpp> #include <boost/mpl/transform_view.hpp> #include <boost/mpl/copy_if.hpp> #include <boost/multi_index_container.hpp> #include <boost/multi_index/sequenced_index.hpp> #include <boost/multi_index/ordered_index.hpp> #include "Item.h" namespace bmi = boost::multi_index; namespace bf = boost::fusion; template< typename K > struct getIndex // Syntaxic sugar { typedef typename K::value result_type; template< typename ITEM > result_type operator() ( const ITEM& item )const { return item.index<K>(); } }; template < typename K > struct make_index { typedef bmi::ordered_unique< bmi::tag<K>, getIndex<K> > type; }; template < typename T > struct is_indexable : boost::is_same< typename T::indexable, boost::mpl::true_ > { }; template < typename KEYS > struct indexable_keys { typedef typename boost::mpl::transform < typename boost::mpl::copy_if< KEYS, is_indexable<boost::mpl::_> >::type, make_index<boost::mpl::_> >::type type; }; template <typename K0, typename K1, typename K2, typename K3, typename K4> struct container { typedef boost::fusion::vector<K0, K1, K2, K3, K4> keys; typedef typename item<K0, K1, K2, K3, K4> item_t; typedef boost::multi_index_container < item_t, typename boost::mpl::joint_view< bf::single_view< bmi::sequenced<> >, typename all_keys<keys>::type >::type > type; };
Voila on a presque terminé, on a écrit une structure de données item et un container générique. C’est le compilateur qui va générer le code. Il ne reste plus qu’a écrire le code client (appelant).
#include "container.h" #include "Title.h" template <typename K0, typename K1, typename K2, typename K3, typename K4> class Transcoder { public: template<typename FROM,typename TO> typename TO::value get( const typename FROM::value& index ) { auto it = datas.get< FROM >().find( index ); if ( it == datas.get< FROM >().end() ) throw std::invalid_argument( index ); return it->index<TO>(); } private: typename container<K0, K1, K2, K3, K4>::type datas; }; int main(int argc, const char** argv) { Transcoder<itunes, amazon, cddb, spotify, description> transco; transco.get<itunes, amazon>("UnIdItunes"); transco.get<itunes, spotify>("UnIdItunes"); return 0; }
Le code appelant est concis et facile. On pourrait facilement écrire un transcodeur sur une collection de films (avec iTunes, canalPlay, imdb, …).
On voit donc les intérêts de la méta-programmation :
- Factorisation : Moins de lignes de code = moins de bugs potentiels
- Vitesse au Runtime.
- Sécurité et fiabilité (typage fort, immutabilité)
En contre partie on a :
- Des temps de compilation qui peuvent exploser.
- Difficile de lire / debugger
La bonne pratique consiste à ne pas mettre partout de la méta-programmation et toujours faire des objets de plus haut niveau qui encapsulent les Template et facilitent la compréhension.
Lors d’une mission j’ai dû écrire un Transcoder pour gérer une dizaine de collections de données. J’avais écris un Transcoder sur ce principe. Une difficulté supplémentaire venait du fait que toutes les données n’étaient pas homogènes en termes de nombre de champs (5 dans notre exemple). J’avais donc introduit un axe de généricité supplémentaire. Mes items et container étaient Template d’un boost::fusion::vector contenant les traits. J’aurais pu aussi utiliser les templates variadic mais cela nécessitait le passage à C++ 11. Le parsing des collections de données XML était également écrit en méta-programmation.