Qu’est-ce que la composition ?
Disons que j’ai une classe pour enregistrer un nouvel utilisateur, et qu’à présent je souhaite envoyer un e-mail pour avertir l’utilisateur de la création de son compte.
Au lieu d’ajouter une méthode envoyerEmail
dans ma classe, ou d’étendre ma classe et d’ajouter cette méthode dans la classe enfant, le choix de la composition nous dicte plutôt de :
créer une nouvelle classe dédiée à l’envoi de ce mail, et
donner une instance de cette classe à la première.
La mise en place d'une composition ne peut pas s'effectuer simplement au fil de l'eau. Le nombre de classes et la portée de leurs actions doivent être réfléchis en amont. Généralement, ça passe par la création d'interfaces :
les classes implémentent les interfaces ;
les interfaces sont développées et ajoutées aux classes ;
et les classes les réclament soit par nouvelles créations (instanciation), soit par injection en argument de constructeur ou de méthode.
Comment savoir quand choisir l'héritage ou la composition ?
Choisissez l'héritage lorsque vous pouvez dire "c'est un" ; par exemple :
Un administrateur est un utilisateur.
Un pont levant est un pont.
Choisissez la composition lorsque vous pouvez dire "possède un/des" ou "est composé de", comme :
Un pont levant possède un moteur.
Un match possède des joueurs.
La composition est un principe qui donne plus de souplesse à la conception et à l'évolution de votre code. C'est plus naturel de construire des classes en en possédant d'autres qui sont spécialisées, plutôt que d'essayer de trouver des points communs entre elles, et de créer un arbre généalogique.
Cette approche permet de s'adapter plus facilement aux modifications futures ! Dans le cas où vous utilisez l'héritage, si vous effectuez un changement relativement important dans une classe située très haut dans l'arbre généalogique, vous impactez tous les enfants. Souvent ça implique de restructurer ces classes-ci, ainsi que celles qui en dépendent... Pour cela, la composition est préférable quand c’est possible.
Composez, structurez et faites dialoguer vos classes
Faites attention à la loi de Demeter
Nous venons de créer des classes, de les structurer et de les faire dialoguer entre elles ! C'est formidable.
Il arrive que (par fainéantise) l'on rencontre des objets un peu trop ouverts... et qu'on en abuse. Le danger du comportement que je vais exprimer juste après, c'est qu'il est insidieux, car on ne constate les dégâts que très tard ! Quand on doit faire des changements simples et que d'un coup, on se rend compte que l'impact est global sur notre application (j'exagère, mais à peine)...
Le problème survient lorsqu'un objet manipule directement la dépendance d'une de ses dépendances. Cette phrase n'est pas évidente, c'est pourquoi j'ai préféré vous faire la démonstration en screencast.
Exercez-vous
Reprenons la classe QueuingPlayer
. Elle hérite de la classe Player
. Ce n'est pas idéal, car le fait d'être en attente d'un match ne justifie pas nécessairement d'étendre la classe Player
. Modifiez la classe QueuingPlayer
pour que le Player
soit une propriété au lieu d'un héritage.
Si vous devez faire des ajustements à d'autres endroits du code existant, n'hésitez pas.
Vous trouverez le code sur la branche P3C5, et la correction sur la branche P3C5-correction.
En résumé
Préférez la composition plutôt que l'héritage quand c'est possible.
Pour savoir quand choisir l'un ou l'autre, posez-vous la question "est un"/"possède un".
N'utilisez jamais les dépendances d'une dépendance directement, au risque d'avoir à reconstruire trop de choses après un simple changement.
La force de ce genre de structure, c'est que les classes, et donc les objets, peuvent se manipuler avec beaucoup de flexibilité, et faire partie d'engrenages complexes ! C'est vraiment ce qui m'éclate dans le développement. Mais avant tout, un code complet, c'est un code qui gère correctement les cas d'erreur. Voyons comment les gérer avec la programmation orientée objet, dans le prochain chapitre.