Choisir entre composition et héritage La composition et l'héritage placent tous deux des sous-objets dans votre nouvelle classe. Tous deux utilisent la liste d'initialisation du constructeur pour construire ces sous-objets. A présent, peut-être que vous vous demandez quel est la différence entre les deux, et quand choisir l'un ou l'autre. La composition est généralement utilisée quand vous voulez trouver les fonctionnalités d'une classe existante au sein de votre nouvelle classe, mais pas son interface. C'est-à-dire que vous enrobez un objet pour implémenter des fonctionnalités de votre nouvelle classe, mais l'utilisateur de votre nouvelle classe voit l'interface que vous avez définie plutôt que l'interface de la classe d'origine. Pour ce faire, vous suivez la démarche type d'implantation d'objets de classes existantes private dans votre nouvelle classe. Parfois, cependant, il est logique de permettre à l'utilisateur de la classe d'accéder directement à la composition de votre nouvelle classe, c'est-à-dire de rendre l'objet membre public. Les objets membres utilisent eux-mêmes le contrôle d'accès, et c'est donc une démarche sure et quand l'utilisateur sait que vous assemblez différents morceaux, cela rend l'interface plus compréhensible. Une classe Car (Voiture, NdT) est un bon exemple : //: C14:Car.cpp // Composition publique class Engine { public: void start() const {} void rev() const {} void stop() const {} }; class Wheel { public: void inflate(int psi) const {} }; class Window { public: void rollup() const {} void rolldown() const {} }; class Door { public: Window window; void open() const {} void close() const {} }; class Car { public: Engine engine; Wheel wheel[4]; Door left, right; // 2-portes }; int main() { Car car; car.left.window.rollup(); car.wheel[0].inflate(72); } ///:~ Comme la composition d'un Car fait partie de l'analyse du problème (et pas simplement de la conception sous-jacente), rendre les membres public aide le programmeur-client à comprendre comment utiliser la classe et requiert un code moins complexe de la part du créateur de la classe. En y réfléchissant un peu, vous verrez aussi que cela n'aurait aucun sens de composer un Car en utilisant un objet “Véhicule” – une voiture ne contient pas un véhicule, c'est un véhicule. La relation est un est exprimée par l'héritage, et la relation a un est exprimée par la composition.
Sous-typer A présent, supposez que vous vouliez créer un objet de type ifstream qui non seulement ouvre un fichier mais en plus conserve le nom du fichier. Vous pouvez utiliser la composition et inclure un objet ifstream et un objet string dans la nouvelle classe : //: C14:FName1.cpp // Un fstream avec nom de fichier #include "../require.h" #include <iostream> #include <fstream> #include <string> using namespace std; class FName1 { ifstream file; string fileName; bool named; public: FName1() : named(false) {} FName1(const string& fname) : fileName(fname), file(fname.c_str()) { assure(file, fileName); named = true; } string name() const { return fileName; } void name(const string& newName) { if(named) return; // N'écrasez pas fileName = newName; named = true; } operator ifstream&() { return file; } }; int main() { FName1 file("FName1.cpp"); cout << file.name() << endl; // Erreur: close() n'est pas un membre : //! file.close(); } ///:~ Il y a un problème, cependant. On essaye de permettre l'utilisation de l'objet FName1 partout où un objet ifstream est utilisé en incluant un opérateur de conversion de type automatique de FName1 vers un ifstream&. Mais dans main, la ligne file.close(); ne compilera pas car la conversion de type automatique n'a lieu que dans les appels de fonctions, pas pendant la sélection de membre. Cette approche ne fonctionnera donc pas. Une deuxième approche consiste à ajouter la définition de close( ) à FName1: void close() { file.close(); } Ceci ne fonctionnera que si il n'y a que quelques fonctions que vous voulez apporter depuis la classe ifstream. Dans ce cas, vous n'utilisez que des parties de la classe et la composition est appropriée. Mais que se passe-t-il si vous voulez que tous les éléments de la classe soient transposés ? On appelle cela sous-typage parce que vous créez un nouveau type à partir d'un type existant, et vous voulez que votre nouveau type ait exactement la même interface que le type existant (plus toutes les autres fonctions membres que vous voulez ajouter), si bien que vous puissiez l'utiliser partout où vous utiliseriez le type existant. C'est là que l'héritage est essentiel. Vous pouvez voir que le sous-typage résoud parfaitement le problème de l'exemple précédent : //: C14:FName2.cpp // Le sous-typage résoud le problème #include "../require.h" #include <iostream> #include <fstream> #include <string> using namespace std; class FName2 : public ifstream { string fileName; bool named; public: FName2() : named(false) {} FName2(const string& fname) : ifstream(fname.c_str()), fileName(fname) { assure(*this, fileName); named = true; } string name() const { return fileName; } void name(const string& newName) { if(named) return; // N'écrasez pas fileName = newName; named = true; } }; int main() { FName2 file("FName2.cpp"); assure(file, "FName2.cpp"); cout << "name: " << file.name() << endl; string s; getline(file, s); // Cela fonctionne aussi ! file.seekg(-200, ios::end); file.close(); } ///:~ A présent, toute fonction membre disponible pour un objet ifstream l'est également pour un objet FName2. Vous pouvez constater aussi que des fonctions non membres comme getline( ) qui attendent un ifstream peuvent également fonctionner avec un FName2. C'est le cas parce qu'un FName2 est un type d'ifstream ; il n'en contient pas simplement un. C'est un problème très important qui sera abordé à la fin de ce chapitre et dans le suivant.
héritage privé Vous pouvez hériter d'une classe de base de manière privée en laissant de coté le public dans la liste des classes de base, ou en disant explicitement private (probablement une meilleure stratégie parce qu'alors c'est clair pour l'utilisateur que c'est ce que vous voulez). Quand vous héritez de façon privée, vous “implémentez en termes de” c'est-à-dire que vous créez une nouvelle classe qui a toutes les données et les fonctionnalités de la classe de base, mais ces fonctionnalités sont cachées, si bien qu'elles font seulement partie de l'implémentation sous-jacente. L'utilisateur de la classe n'a aucun accès à la fonctionnalité sous-jacente, et un objet ne peut pas être traité comme une instance de la classe de base (comme c'était le cas dans FName2.cpp). Peut-être vous demandez-vous le but de l'héritage privée, parce que l'alternative d'utiliser la composition pour créer un objet privé dans la nouvelle classe semble plus appropriée. L'héritage privé est inclus dans le langage pour des raisons de complétude, mais ne serait-ce que pour réduire les sources de confusion, vous aurez généralement intérêt à utiliser la composition plutôt que l'héritage privé. Toutefois, il peut y avoir parfois des situations où vous voulez produire une partie de la même interface que la classe de base et ne pas autoriser le traitement de l'objet comme s'il était du type de la classe de base. L'héritage privé fournit cette possibilité. Rendre publics les membres hérités de manière privée Quand vous héritez de manière privée, toutes les fonctions membres public de la classe de base deviennent private. Si vous voulez que n'importe lesquelles d'entre elles soit visibles, dites simplement leur nom (sans argument ni valeur de retour) avec le mot-clef using dans la section public de la classe dérivée : //: C14:PrivateInheritance.cpp class Pet { public: char eat() const { return 'a'; } int speak() const { return 2; } float sleep() const { return 3.0; } float sleep(int) const { return 4.0; } }; class Goldfish : Pet { // Héritage privé public: using Pet::eat; // Nommez les membres à rendre public using Pet::sleep; // Les deux fonctions surchargées sont exposées }; int main() { Goldfish bob; bob.eat(); bob.sleep(); bob.sleep(1); //! bob.speak();// Erreur : fonction membre privée } ///:~ Ainsi, l'héritage privé est utile si vous voulez dissimuler une partie des fonctionnalités de la classe de base. Notez qu'exposer le nom d'une fonction surchargée rend publiques toutes les versions de la fonction surchargée dans la classe de base. Vous devriez bien réfléchir avant d'utiliser l'héritage privé au lieu de la composition ; l'héritage privé entraîne des complications particulières quand il est combiné avec l'identification de type à l'exécution (RTTI NdT) (c'est le sujet d'un chapitre du deuxième volume de ce livre, téléchargeable depuis www.BruceEckel.com).