Aller au contenu principal

Composition plutôt qu'héritage

"Composition plutôt qu'héritage" (également connu sous le nom de "privilégier la composition à l'héritage") est un principe de conception en programmation orientée objet qui suggère que les classes devraient acquérir de nouvelles fonctionnalités en composant des objets de classes existantes (c'est-à-dire en contenant des instances d'autres classes) plutôt qu'en héritant d'elles. Ce principe favorise une plus grande flexibilité, un couplage réduit et une meilleure réutilisabilité du code.

Le problème avec l'héritage

Bien que l'héritage (mot-clé extends en Java) soit un concept fondamental de la POO pour la réutilisation du code et le polymorphisme, il peut entraîner plusieurs problèmes, en particulier dans les systèmes complexes :

  1. Couplage fort : L'héritage crée une relation forte et rigide entre la classe parente (super-classe) et la classe enfant (sous-classe). La sous-classe devient dépendante des détails d'implémentation de la super-classe. Les modifications apportées à la super-classe peuvent affecter de manière inattendue les sous-classes (le "problème de la classe de base fragile").
  2. Violations du Principe de Substitution de Liskov (LSP) : Parfois, les développeurs utilisent l'héritage uniquement pour la réutilisation du code, même lorsque la relation "est un" (qu'implique l'héritage) n'est pas strictement vraie. Cela peut conduire à des sous-classes qui ne peuvent pas être substituées à leur super-classe sans compromettre la correction du programme.
  3. Flexibilité limitée : Une classe ne peut hériter que d'une seule super-classe en Java (héritage simple). Si une classe a besoin de fonctionnalités provenant de plusieurs sources non liées, l'héritage devient problématique.
  4. Exposition des internes de la super-classe : L'héritage expose souvent les membres protégés, voire privés, de la super-classe aux sous-classes, ce qui affaiblit l'encapsulation.
  5. Difficulté à modifier le comportement à l'exécution : Le comportement hérité d'une super-classe est fixé au moment de la compilation. Il est difficile de modifier dynamiquement le comportement hérité d'un objet.

Qu'est-ce que la composition ?

La composition implique la construction d'objets complexes en combinant des objets plus simples et existants. Au lieu d'hériter du comportement d'une classe de base, une classe acquiert un nouveau comportement en détenant une instance d'une autre classe et en déléguant des tâches à cette instance. Cela crée une relation "a un".

Exemple : Une Voiture "a un" Moteur, plutôt qu'une Voiture "est un" Moteur.

Avantages de la composition

  1. Flexibilité et adaptabilité :
    • Changements de comportement à l'exécution : Les comportements peuvent être modifiés à l'exécution en remplaçant l'objet composé par un autre objet qui implémente la même interface. C'est une idée fondamentale derrière les stratégies et l'injection de dépendances.
    • Comportements multiples : Une classe peut composer plusieurs objets différents, réalisant ainsi efficacement plusieurs comportements sans les limitations de l'héritage simple.
  2. Couplage faible :
    • Dépendances réduites : La classe composante interagit avec l'objet composé via son interface, et non ses détails d'implémentation. Cela réduit la dépendance entre les classes.
    • Encapsulation : L'implémentation interne de l'objet composé est masquée de la classe composante, favorisant une meilleure encapsulation.
  3. Tests plus faciles : Étant donné que les composants sont moins couplés, ils sont généralement plus faciles à tester de manière isolée.
  4. Meilleure réutilisabilité du code : Les composants individuels (les objets composés) peuvent être réutilisés dans différents contextes en étant composés dans diverses classes.
  5. Meilleure adéquation aux relations du monde réel : Souvent, les relations du monde réel sont des relations "a un" (composition) plutôt que "est un" (héritage).

Inconvénients de la composition

  1. Augmentation du nombre d'objets : Vous pourriez vous retrouver avec plus d'objets et de classes par rapport à une hiérarchie d'héritage pure, ce qui pourrait sembler plus complexe au début.
  2. Surcharge de délégation : Vous devez souvent écrire du code de "délégation" – des méthodes dans la classe composante qui appellent simplement des méthodes sur l'objet composé. Cela peut parfois ressembler à du code passe-partout, bien que les IDE et parfois les fonctionnalités du langage puissent aider.
  3. Dépendance à l'interface : L'efficacité de la composition repose fortement sur des interfaces bien définies pour les objets composés.

Exemple illustratif : Une Voiture et son Moteur

Utilisation de l'héritage (moins idéal pour ce scénario) :

// Pas une bonne conception pour la relation typique voiture/moteur
class Engine {
public void start() {
System.out.println("Le moteur démarre.");
}
public void stop() {
System.out.println("Le moteur s'arrête.");
}
}

class Car extends Engine { // Problème : Une Voiture "est un" Moteur, ce qui n'est pas vrai
public void drive() {
start(); // Méthode héritée
System.out.println("La voiture roule.");
}
}

// Problème : Que se passe-t-il si nous voulons changer le type de moteur de la Voiture à l'exécution ?
// Ou avoir différents types de moteurs comme ElectricEngine, PetrolEngine ?

Utilisation de la composition (préféré) :

// Définir une interface pour le comportement dont nous avons besoin
interface Startable {
void start();
void stop();
}

// Implémentations concrètes du comportement
class PetrolEngine implements Startable {
@Override
public void start() {
System.out.println("Le moteur essence démarre avec un rugissement.");
}
@Override
public void stop() {
System.out.println("Le moteur essence s'arrête.");
}
}

class ElectricEngine implements Startable {
@Override
public void start() {
System.out.println("Le moteur électrique bourdonne et prend vie.");
}
@Override
public void stop() {
System.out.println("Le moteur électrique s'arrête silencieusement.");
}
}

// La classe Car compose un Moteur (ou tout objet Startable)
class Car {
private Startable engine; // La Voiture "a un" Startable (type d'interface)

// Injection par constructeur : le moteur est fourni de l'extérieur
public Car(Startable engine) {
this.engine = engine;
}

// Injection par mutateur : permet de changer le moteur à l'exécution
public void setEngine(Startable engine) {
this.engine = engine;
}

public void drive() {
if (engine != null) {
engine.start(); // Déléguer à l'objet composé
System.out.println("La voiture roule.");
} else {
System.out.println("Pas de moteur pour faire rouler la voiture.");
}
}

public void stopDriving() {
if (engine != null) {
engine.stop(); // Déléguer à l'objet composé
System.out.println("La voiture s'est arrêtée.");
}
}

public static void main(String[] args) {
Car petrolCar = new Car(new PetrolEngine());
petrolCar.drive();
petrolCar.stopDriving();

System.out.println("--- Changement de moteur ---");

Car electricCar = new Car(new ElectricEngine());
electricCar.drive();
electricCar.stopDriving();

// Exemple de changement de moteur à l'exécution (si la conception le permet)
petrolCar.setEngine(new ElectricEngine());
petrolCar.drive();
}
}

Dans l'exemple de composition, la classe Car n'hérite pas de Engine. Au lieu de cela, elle contient une instance d'un objet qui implémente l'interface Startable. Cela nous permet de remplacer facilement différents types de moteurs (par exemple, PetrolEngine, ElectricEngine) sans modifier la classe Car elle-même, ce qui offre une plus grande flexibilité.

Quand choisir l'un ou l'autre

  • Choisissez l'héritage lorsque :

    • Il existe une relation "est un" claire et indéniable (par exemple, un Chien "est un" Animal).
    • Vous souhaitez réutiliser une implémentation commune et l'étendre.
    • La classe de base et les sous-classes sont fortement couplées et forment une unité unique et cohésive.
    • Vous souhaitez tirer parti du polymorphisme où les sous-classes sont directement substituables à la super-classe (le LSP est respecté).
  • Choisissez la composition lorsque :

    • Il existe une relation "a un" (par exemple, une Voiture "a un" Moteur).
    • Vous souhaitez obtenir de la flexibilité et la capacité de modifier le comportement à l'exécution.
    • Vous souhaitez réduire le couplage entre les classes.
    • Vous avez besoin de réutiliser le comportement de plusieurs sources non liées.
    • Le comportement dont vous avez besoin est indépendant de la hiérarchie des classes.

Dans la conception POO moderne, la "Composition plutôt qu'héritage" est généralement préférée car elle conduit à des systèmes plus flexibles, maintenables et robustes. L'héritage doit être utilisé avec discernement, généralement pour créer des hiérarchies bien définies où la relation "est un" est forte et où les avantages de l'implémentation partagée l'emportent sur le potentiel de couplage fort.