Avant d'expliquer ce qu'est un mix-in, il est utile de décrire les problèmes qu'il tente de résoudre. Disons que vous avez un tas d'idées ou de concepts que vous essayez de modéliser. Ils peuvent être liés d'une manière ou d'une autre, mais ils sont orthogonaux pour la plupart, ce qui signifie qu'ils peuvent être utilisés indépendamment les uns des autres. Vous pourriez modéliser cela par l'héritage et faire en sorte que chacun de ces concepts dérive d'une classe d'interface commune. Ensuite, vous fournissez des méthodes concrètes dans la classe dérivée qui implémente cette interface.
Le problème de cette approche est que cette conception n'offre aucun moyen intuitif clair de prendre chacune de ces classes concrètes et de les combiner ensemble.
L'idée avec les mix-ins est de fournir un tas de classes primitives, où chacune d'entre elles modélise un concept orthogonal de base, et de pouvoir les coller ensemble pour composer des classes plus complexes avec juste la fonctionnalité que vous voulez -- un peu comme des legos. Les classes primitives elles-mêmes sont destinées à être utilisées comme des blocs de construction. Elles sont extensibles puisque, plus tard, vous pourrez ajouter d'autres classes primitives à la collection sans affecter les classes existantes.
Pour en revenir au C++, une technique pour y parvenir consiste à utiliser des modèles et l'héritage. L'idée de base est de connecter ces blocs de construction ensemble en les fournissant via le paramètre du modèle. Vous les enchaînez ensuite, par exemple par le biais de typedef
pour former un nouveau type contenant la fonctionnalité que vous souhaitez.
En reprenant votre exemple, disons que nous voulons ajouter une fonctionnalité de refonte. Voici à quoi cela pourrait ressembler :
#include <iostream>
using namespace std;
struct Number
{
typedef int value_type;
int n;
void set(int v) { n = v; }
int get() const { return n; }
};
template <typename BASE, typename T = typename BASE::value_type>
struct Undoable : public BASE
{
typedef T value_type;
T before;
void set(T v) { before = BASE::get(); BASE::set(v); }
void undo() { BASE::set(before); }
};
template <typename BASE, typename T = typename BASE::value_type>
struct Redoable : public BASE
{
typedef T value_type;
T after;
void set(T v) { after = v; BASE::set(v); }
void redo() { BASE::set(after); }
};
typedef Redoable< Undoable<Number> > ReUndoableNumber;
int main()
{
ReUndoableNumber mynum;
mynum.set(42); mynum.set(84);
cout << mynum.get() << '\n'; // 84
mynum.undo();
cout << mynum.get() << '\n'; // 42
mynum.redo();
cout << mynum.get() << '\n'; // back to 84
}
Tu remarqueras que j'ai fait quelques changements par rapport à ton original :
- Les fonctions virtuelles ne sont pas vraiment nécessaires ici car nous savons exactement quel est le type de notre classe composée au moment de la compilation.
- J'ai ajouté une valeur par défaut
value_type
pour le second paramètre du modèle afin de rendre son utilisation moins contraignante. Ainsi, vous n'aurez pas à taper sans cesse <foobar, int>
à chaque fois que vous collez une pièce ensemble.
- Au lieu de créer une nouvelle classe qui hérite des pièces, une simple
typedef
est utilisé.
Notez qu'il s'agit d'un exemple simple pour illustrer l'idée du mixage. Il ne prend donc pas en compte les cas particuliers et les utilisations amusantes. Par exemple, l'exécution d'un undo
sans jamais définir un nombre ne se comportera probablement pas comme vous l'attendez.
À titre d'information, vous pourriez également trouver cet article utile.