Préférer la composition ne concerne pas seulement le polymorphisme. Bien que cela en fasse partie, et vous avez raison (du moins dans les langues nominalement typées), ce que les gens veulent vraiment dire, c'est "préférez une combinaison de composition et d'implémentation d'interface". Mais, les raisons de préférer la composition (dans de nombreuses circonstances) sont profondes.
Le polymorphisme consiste en une chose à se comporter de différentes manières. Ainsi, les génériques / modèles sont une fonctionnalité "polymorphe" dans la mesure où ils permettent à un seul morceau de code de faire varier son comportement avec les types. En fait, ce type de polymorphisme est vraiment le meilleur comportement et est généralement appelé polymorphisme paramétrique, car la variation est définie par un paramètre.
De nombreux langages fournissent une forme de polymorphisme appelée "surcharge" ou polymorphisme ad hoc, dans lequel plusieurs procédures portant le même nom sont définies de manière ad hoc et où l'une d'entre elles est choisie par le langage (peut-être la plus spécifique). Il s’agit du type de polymorphisme le moins bien comporté puisque rien ne relie le comportement des deux procédures à l’exception de la convention développée.
Un troisième type de polymorphisme est le polymorphisme de sous - type . Ici, une procédure définie sur un type donné peut également fonctionner sur toute une famille de "sous-types" de ce type. Lorsque vous implémentez une interface ou étendez une classe, vous déclarez généralement votre intention de créer un sous-type. Les vrais sous-types sont régis par le principe de substitution de Liskovqui dit que si vous pouvez prouver quelque chose à propos de tous les objets d’un supertype, vous pouvez le faire à propos de toutes les instances d’un sous-type. La vie devient cependant dangereuse, car dans des langages tels que C ++ et Java, les gens ont généralement des hypothèses non appliquées et souvent non documentées sur les classes qui peuvent être ou non vraies à propos de leurs sous-classes. Autrement dit, le code est écrit comme si plus de preuves que ce qui est réellement prouvable ne sont générées, ce qui produit toute une série de problèmes lorsque vous sous-tapez négligemment.
L'héritage est en réalité indépendant du polymorphisme. Étant donné quelque chose "T" qui a une référence à lui-même, l'héritage se produit lorsque vous créez une nouvelle chose "S" à partir de "T" en remplaçant "T" par une référence à "S". Cette définition est intentionnellement vague, car l'héritage peut se produire dans de nombreuses situations, mais la plus courante consiste à sous-classer un objet, ce qui a pour effet de remplacer le this
pointeur appelé par des fonctions virtuelles par le this
pointeur sur le sous-type.
L'héritage est dangereux, comme toutes les choses très puissantes. L'héritage a le pouvoir de causer des ravages. Par exemple, supposons que vous substituez une méthode lorsque vous héritez d'une classe: tout va bien jusqu'à ce qu'une autre méthode de cette classe suppose que la méthode dont vous avez hérité se comporte d'une certaine manière, après tout ce que l'auteur de la classe d'origine a conçue . Vous pouvez vous protéger partiellement contre cela en déclarant toutes les méthodes appelées par une autre de vos méthodes privées ou non virtuelles (final), sauf si elles sont conçues pour être remplacées. Même si cela ne suffit pas toujours. Parfois, vous pouvez voir quelque chose comme ceci (en pseudo Java, lisible pour les utilisateurs C ++ et C #)
interface UsefulThingsInterface {
void doThings();
void doMoreThings();
}
...
class WayOfDoingUsefulThings implements UsefulThingsInterface{
private foo stuff;
public final int getStuff();
void doThings(){
//modifies stuff, such that ...
...
}
...
void doMoreThings(){
//ignores stuff
...
}
}
vous pensez que c'est beau et avez votre propre façon de faire des "choses", mais vous utilisez l'héritage pour acquérir la capacité de faire "plus de choses",
class MyUsefulThings extends WayOfDoingUsefulThings{
void doThings {
//my way
}
}
Et tout va bien. WayOfDoingUsefulThings
a été conçu de manière à ce que le remplacement d’une méthode ne modifie pas la sémantique d’une autre ... sauf attendre, non. Cela ressemble à ça, mais a doThings
changé d'état mutable qui importait. Donc, même s'il n'appelle aucune fonction pouvant être remplacée,
void dealWithStuff(WayOfDoingUsefulThings bar){
bar.doThings()
use(bar.getStuff());
}
fait maintenant quelque chose de différent de ce à quoi vous vous attendez lorsque vous le passez MyUsefulThings
. Quoi de pire, vous pourriez même ne pas savoir que WayOfDoingUsefulThings
fait ces promesses. Peut-être dealWithStuff
provient-il de la même bibliothèque WayOfDoingUsefulThings
et getStuff()
n'est-il même pas exporté par la bibliothèque (pensez aux classes d'amis en C ++). Pire encore, vous avez vaincu les vérifications statiques de la langue sans vous en rendre compte: vous avez dealWithStuff
pris la WayOfDoingUsefulThings
peine de vous assurer qu’il aurait une getStuff()
fonction qui se comporterait d’une certaine manière.
En utilisant la composition
class MyUsefulThings implements UsefulThingsInterface{
private way = new WayOfDoingUsefulThings()
void doThings() {
//my way
}
void doMoreThings() {
this.way.doMoreThings();
}
}
ramène la sécurité de type statique. En général, la composition est plus facile à utiliser et plus sûre que l'héritage lors de la mise en œuvre de sous-typage. Il vous permet également de remplacer les méthodes finales, ce qui signifie que vous devriez pouvoir déclarer tout ce qui est final / non virtuel, sauf la plupart du temps dans les interfaces.
Dans un monde meilleur, les langues inséreraient automatiquement le passe-partout avec un delegation
mot clé. La plupart ne le font pas, donc un inconvénient est les classes plus grandes. Cependant, vous pouvez demander à votre IDE d’écrire l’instance de délégation pour vous.
Maintenant, la vie ne concerne pas seulement le polymorphisme. Vous pouvez ne pas avoir besoin de sous-type tout le temps. L'objectif du polymorphisme est généralement de réutiliser le code, mais ce n'est pas le seul moyen d'atteindre cet objectif. Souvent, il est judicieux d’utiliser une composition, sans polymorphisme de sous-type, comme moyen de gestion des fonctionnalités.
En outre, l'héritage comportemental a ses utilisations. C'est l'une des idées les plus puissantes en informatique. C'est juste que, la plupart du temps, les bonnes applications POO peuvent être écrites en utilisant uniquement l'héritage et les compositions d'interface. Les deux principes
- Interdire l'héritage ou la conception pour cela
- Préfère la composition
sont un bon guide pour les raisons ci-dessus et n'engendrent pas de coûts substantiels.