En C++, les relations fondamentales entre classes sont l'héritage, la composition et la délégation. Ces structures permettent de modéliser des interactions complexes entre objets.
Composition
La composition exprime une relation de type "est-un" et s'apparente à l'utilisation de structures en C. Elle permet de construire des objets complexes en intégrant d'autres objets comme membres.
Un exemple classique provient de l'implémentation d'une file d'attente (queue) dans la bibliothèque standard. La classe queue encapsule un conteneur sous-jacent (par défaut deque) et redirige ses opérations vers celui-ci, adaptant les noms des fonctions (push_back devient push, etc.).
template <typename T, typename ConteneurSousJacent = std::deque<T>>
class FileAttente {
protected:
ConteneurSousJacent donnees;
public:
bool estVide() const { return donnees.empty(); }
std::size_t taille() const { return donnees.size(); }
T& premier() { return donnees.front(); }
T& dernier() { return donnees.back(); }
void enfiler(const T& valeur) { donnees.push_back(valeur); }
void defiler() { donnees.pop_front(); }
};
Cette structure illustre le Pattern Adapter de la méthode GoF, où une classe modifie l'interface d'une autre pour l'adapter à un besoin spécifique. La relation de composition se représente en UML par un losange plein reliant les deux classes.
D'un point de vue mémoire, l'objet composé contient intégralement l'objet composant. Lors de la construction, le constructeur de la classe contenante appelle d'abord le constructeur par défaut du membre avant d'exécuter son propre corps. Lors de la destruction, l'ordre est inversé : l'objet contenante se détruit avant ses membres.
Conteneur::Conteneur(...) : Membre() { /* ... */ };
Conteneur::~Conteneur() { /* ... */ ~Membre(); };
Délégation (Composition par Référence)
La délégation, ou composition par référence, implique qu'une classe maintient un pointeur vers une instance d'une autre classe. Cela crée une realtion plus lâche où les cycles de vie des objets ne sont pas synchronisés.
Cette technique est visible dans des implémentations de type Handle/Body, où une classe gère l'interface publique tandis qu'une autre (souvent privée) contient les données et l'implémentation. C'est un fondement du Pattern Bridge et offre une flexibilité importante pour modifier l'implémentation sans affecter l'interface client (effet de « pare-feu de compilation »).
// En-tête public
class RepresentationChaine;
class Chaine {
public:
Chaine();
Chaine(const char* texte);
~Chaine();
// ... autres fonctions publiques ...
private:
RepresentationChaine* representant;
};
// Implémentation cachée
namespace {
class RepresentationChaine {
friend class Chaine;
RepresentationChaine(const char* texte);
~RepresentationChaine();
int compteReference;
char* contenu;
};
}
Lorsque plusieurs objets Chaine partagent le même contenu, un mécanisme de comptage de références peut être utilisé. La modification du contenu par un objet entraîne alors la copie de celui-ci (stratégie copy-on-write).
Héritage
L'héritage définit une relation « est-un » et est particulièrement puissant lorsqu'il est combiné avec les fonctions virtuelles.
En C++, l'héritage peut utiliser les mots-clés public, protected ou private. L'héritage public est le plus courant et le seul utilisé dans des langages comme Java.
struct NoeudBaseListe {
NoeudBaseListe* suivant;
NoeudBaseListe* precedent;
};
template <typename T>
struct NoeudListe : public NoeudBaseListe {
T donnee;
};
L'ordre des appels aux constructeurs et destructeurs suit une hiérarchie précise. La construction procède de la classe de base vers la classe dérivée, tandis que la destruction suit l'ordre inverse. Il est impératif que le destructeur de la classe de base soit virtuel si l'héritage est utilisé polymorphiquement.
ClasseDerivee::ClasseDerivee(...) : ClasseBase() { /* ... */ };
ClasseDerivee::~ClasseDerivee() { /* ... */ ~ClasseBase(); };
Les Fonctions Virtuelles
Les fonctions virtuelles permettent de spécialiser le comportement dans les classes dérivées.
- Fonction non-virtuelle : Ne doit pas être redéfinie par les classes dérivées.
- Fonction virtuelle : Peut être redéfinie ; possède une implémentation par défaut.
- Fonction virtuelle pure : Doit être redéfinie ; n'a pas d'implémentation par défaut (classe abstraite).
Elles forment la base du Pattern Template Method, où un algorithme est défini dans la classe de base mais certaines étapes sont laissées à l'implémentation des classes dérivées.
// Dans une application (ex: MFC)
class Document : public CDocument {
virtual void Serialiser() = 0; // Implémentation déléguée
};
class MonDocument : public Document {
void Serialiser() override {
// Logique spécifique de sérialisation
}
};
Lorsqu'une classe combine héritage et composition (ses propres membres ou ceux de sa classe de base), l'ordre de construction est le suivant : les classes de base d'abord, puis les membres de composition, enfin le corps de la classe dérivée.
Combinaison de Relations et Patterns
La combinaison de délégation et d'héritage est au cœur de nombreux designs robustes, notamment dans les interfaces graphiques.
Pattern Observer
Ce pattern permet à un objet sujet de notifier un ensemble d'objets observateurs de tout changement d'état.
class Sujet {
int valeur;
std::vector<Observateur*> observateurs;
public:
void attacher(Observateur* obs) {
observateurs.push_back(obs);
}
void definirValeur(int v) {
valeur = v;
notifier();
}
void notifier() {
for (auto obs : observateurs)
obs->mettreAJour(this, valeur);
}
};
class Observateur {
public:
virtual void mettreAJour(Sujet* sujet, int valeur) = 0;
};
Pattern Composite
Ce pattern permet de composer des objets dans des structures arborescentes, traitant les objets individuels et les compositions de manière uniforme.
class Composant {
protected:
int identifiant;
public:
Composant(int id) : identifiant(id) {}
virtual void ajouter(Composant* composant) {}
};
class ElementPrimaire : public Composant {
public:
ElementPrimaire(int id) : Composant(id) {}
};
class Composite : public Composant {
std::vector<Composant*> enfants;
public:
Composite(int id) : Composant(id) {}
void ajouter(Composant* elem) override {
enfants.push_back(elem);
}
};
Pattern Prototype
Ce pattern permet de créer de nouveaux objets en clonant une enstance existante (prototype), contournant ainsi la nécessité de connaître la classe concrète à la compilation.
class Prototype {
public:
virtual Prototype* cloner() const = 0;
virtual void afficher() const = 0;
};
class PrototypeConcretA : public Prototype {
// ... données spécifiques
public:
Prototype* cloner() const override {
return new PrototypeConcretA(*this);
}
void afficher() const override {
// ... affichage
}
};
// Gestionnaire de prototypes
class RegistrePrototypes {
std::map<std::string, Prototype*> prototypes;
public:
void enregistrer(const std::string& cle, Prototype* proto) {
prototypes[cle] = proto;
}
Prototype* creer(const std::string& cle) const {
auto it = prototypes.find(cle);
return it != prototypes.end() ? it->second->cloner() : nullptr;
}
};