« Préférez composition de l' héritage » est juste une bonne heuristique
Vous devriez considérer le contexte, car ce n'est pas une règle universelle. Ne le prenez pas pour signifier ne jamais utiliser l'héritage quand vous pouvez faire de la composition. Si tel était le cas, vous le corrigeriez en interdisant l'héritage.
J'espère clarifier ce point le long de ce post.
Je n'essaierai pas de défendre le mérite de la composition par lui-même. Ce que je considère hors sujet. Au lieu de cela, je parlerai de certaines situations où le développeur peut envisager un héritage qui serait préférable d'utiliser la composition. Sur cette note, l'héritage a ses propres mérites, que je considère également hors sujet.
Exemple de véhicule
J'écris sur des développeurs qui essaient de faire les choses de façon stupide, à des fins narratives
Allons pour une variante des exemples classiques que certains cours POO utilisent ... Nous avons une Vehicle
classe, nous dérivons Car
, Airplane
, Balloon
et Ship
.
Remarque : Si vous devez mettre cet exemple à la terre, faites comme s'il s'agissait de types d'objets dans un jeu vidéo.
Ensuite, Car
et Airplane
peuvent avoir un code commun, car ils peuvent tous les deux rouler sur terre. Les développeurs peuvent envisager de créer une classe intermédiaire dans la chaîne d'héritage pour cela. Pourtant, en fait, il existe également un code partagé entre Airplane
et Balloon
. Ils pourraient envisager de créer une autre classe intermédiaire dans la chaîne d'héritage pour cela.
Ainsi, le développeur serait à la recherche d'héritage multiple. Au point où les développeurs recherchent l'héritage multiple, la conception a déjà mal tourné.
Il est préférable de modéliser ce comportement sous forme d'interfaces et de composition, afin de pouvoir le réutiliser sans avoir à exécuter l'héritage de plusieurs classes. Si les développeurs, par exemple, créent la FlyingVehicule
classe. Ils diraient que Airplane
c'est un FlyingVehicule
(héritage de classe), mais nous pourrions plutôt dire qu'il Airplane
a un Flying
composant (composition) et Airplane
est unIFlyingVehicule
(héritage d'interface).
En utilisant des interfaces, si nécessaire, nous pouvons avoir plusieurs héritages (d'interfaces). De plus, vous n'êtes pas couplé à une implémentation particulière. Augmenter la réutilisabilité et la testabilité de votre code.
N'oubliez pas que l'héritage est un outil de polymorphisme. De plus, le polymorphisme est un outil de réutilisation. Si vous pouvez augmenter la réutilisabilité de votre code en utilisant la composition, faites-le. Si vous n'êtes pas certain que la composition offre ou non une meilleure réutilisabilité, "Préférez la composition à l'héritage" est une bonne heuristique.
Tout cela sans le mentionner Amphibious
.
En fait, nous n'avons peut-être pas besoin de choses qui décollent. Stephen Hurn a un exemple plus éloquent dans ses articles «Favoriser la composition sur l'héritage», partie 1 et partie 2 .
Substituabilité et encapsulation
Doit A
hériter ou composer B
?
Si A
une spécialisation de B
celui-ci doit respecter le principe de substitution de Liskov , l'hérédité est viable, voire souhaitable. S'il y a des situations où il A
n'y a pas de substitution valide, B
alors nous ne devrions pas utiliser l'héritage.
Nous pourrions être intéressés par la composition comme une forme de programmation défensive, pour défendre la classe dérivée . En particulier, une fois que vous commencez à utiliser B
à d'autres fins, il peut y avoir une pression pour le modifier ou l'étendre pour qu'il soit plus adapté à ces fins. S'il y a un risque qui B
peut exposer des méthodes qui pourraient entraîner un état invalide, A
nous devrions utiliser la composition au lieu de l'héritage. Même si nous sommes l'auteur des deux B
et A
, c'est une chose de moins à s'inquiéter, donc la composition facilite la réutilisation de B
.
Nous pouvons même affirmer que s'il y a des fonctionnalités B
qui A
n'en ont pas besoin (et nous ne savons pas si ces fonctionnalités pourraient entraîner un état non valide pourA
, dans la mise en œuvre actuelle ou à l'avenir), c'est une bonne idée d'utiliser la composition au lieu de l'héritage.
La composition présente également les avantages de permettre la mise en œuvre des commutations et de faciliter la simulation.
Remarque : il existe des situations où nous souhaitons utiliser la composition malgré la validité de la substitution. Nous archivons cette substituabilité à l'aide d'interfaces ou de classes abstraites (laquelle utiliser quand est un autre sujet), puis utilisons la composition avec injection de dépendance de l'implémentation réelle.
Enfin, bien sûr, il y a l'argument selon lequel nous devrions utiliser la composition pour défendre la classe parent parce que l'héritage rompt l'encapsulation de la classe parent:
L'héritage expose une sous-classe aux détails de l'implémentation de son parent, on dit souvent que «l'héritage rompt l'encapsulation»
- Modèles de conception: éléments de logiciels orientés objet réutilisables, groupe de quatre
Eh bien, c'est une classe parent mal conçue. C'est pourquoi vous devriez:
Concevez pour l'héritage, ou interdisez-le.
- Java efficace, Josh Bloch
Problème Yo-Yo
Un autre cas où la composition aide est le problème Yo-Yo . Ceci est une citation de Wikipedia:
Dans le développement logiciel, le problème yo-yo est un anti-modèle qui se produit lorsqu'un programmeur doit lire et comprendre un programme dont le graphe d'héritage est si long et compliqué qu'il doit continuer à basculer entre de nombreuses définitions de classes différentes afin de suivre le flux de contrôle du programme.
Vous pouvez résoudre, par exemple: Votre classe C
n'héritera pas de la classe B
. Au lieu de cela, votre classe C
aura un membre de type A
, qui peut ou non être (ou avoir) un objet de type B
. De cette façon, vous ne programmerez pas contre le détail de mise en œuvre de B
, mais contre le contrat que A
propose l'interface (de) .
Exemples de compteur
De nombreux cadres favorisent l'héritage plutôt que la composition (ce qui est le contraire de ce que nous avons soutenu). Le développeur peut effectuer cette opération car il a mis beaucoup de travail dans sa classe de base que son implémentation avec la composition aurait augmenté la taille du code client. Parfois, cela est dû aux limitations de la langue.
Par exemple, un framework PHP ORM peut créer une classe de base qui utilise des méthodes magiques pour permettre l'écriture de code comme si l'objet avait des propriétés réelles. Au lieu de cela, le code géré par les méthodes magiques ira dans la base de données, recherchera le champ particulier demandé (peut-être le mettra en cache pour une demande future) et le renverra. Le faire avec la composition nécessiterait que le client crée des propriétés pour chaque champ ou écrive une version du code des méthodes magiques.
Addendum : Il existe d'autres façons de permettre l'extension des objets ORM. Ainsi, je ne pense pas que l'héritage soit nécessaire dans ce cas. C'est moins cher.
Pour un autre exemple, un moteur de jeu vidéo peut créer une classe de base qui utilise du code natif en fonction de la plate-forme cible pour effectuer le rendu 3D et la gestion des événements. Ce code est complexe et spécifique à la plate-forme. Il serait coûteux et sujet aux erreurs pour l'utilisateur développeur du moteur de gérer ce code, en fait, cela fait partie de la raison d'utiliser le moteur.
De plus, sans la partie de rendu 3D, c'est ainsi que de nombreux frameworks de widgets fonctionnent. Cela vous évite de vous soucier de la gestion des messages du système d'exploitation… en fait, dans de nombreuses langues, vous ne pouvez pas écrire un tel code sans une forme d'enchère native. De plus, si vous le faisiez, cela restreindrait votre portabilité. Au lieu de cela, avec héritage, à condition que le développeur ne casse pas la compatibilité (trop); vous pourrez à l'avenir facilement porter votre code sur toutes les nouvelles plates-formes qu'ils prennent en charge.
De plus, considérez que plusieurs fois nous ne voulons remplacer que quelques méthodes et laisser tout le reste avec les implémentations par défaut. Si nous avions utilisé la composition, nous aurions dû créer toutes ces méthodes, ne serait-ce que pour déléguer à l'objet encapsulé.
Par cet argument, il y a un point où la composition peut être pire pour la maintenabilité que l'héritage (lorsque la classe de base est trop complexe). Cependant, rappelez-vous que la maintenabilité de l'héritage peut être pire que celle de la composition (lorsque l'arbre d'héritage est trop complexe), ce à quoi je fais référence dans le problème du yo-yo.
Dans les exemples présentés, les développeurs ont rarement l'intention de réutiliser le code généré via l'héritage dans d'autres projets. Cela atténue la réutilisabilité réduite de l'utilisation de l'héritage au lieu de la composition. De plus, en utilisant l'héritage, les développeurs de framework peuvent fournir beaucoup de code facile à utiliser et à découvrir.
Dernières pensées
Comme vous pouvez le voir, la composition a un certain avantage sur l'héritage dans certaines situations, pas toujours. Il est important de considérer le contexte et les différents facteurs impliqués (tels que la réutilisabilité, la maintenabilité, la testabilité, etc.) pour prendre la décision. Retour au premier point: « Préférez composition sur l' héritage » est un juste bonne heuristique.
Vous pouvez également remarquer que la plupart des situations que je décris peuvent être résolues dans une certaine mesure avec Traits ou Mixins. Malheureusement, ce ne sont pas des fonctionnalités courantes dans la grande liste des langues, et elles s'accompagnent généralement d'un certain coût de performance. Heureusement, leurs méthodes d'extension et implémentations par défaut populaires de leurs cousins atténuent certaines situations.
J'ai un article récent où je parle de certains des avantages des interfaces dans pourquoi avons-nous besoin d'interfaces entre l'interface utilisateur, les entreprises et l'accès aux données en C # . Il aide au découplage et facilite la réutilisabilité et la testabilité, cela pourrait vous intéresser.