Accueil > Programmation orientée objet >
đ Composition
Survol et attentes
Quand vous avez un peu dâexpĂ©rience avec lâhĂ©ritage, le polymorphisme, les classes abstraites et les interfaces, vous aurez probablement envie de dĂ©velopper des projets plus complexes. La composition est un concept important que vous devrez comprendre afin de planifier les relations entres les classes pour les rendre facile Ă utiliser et Ă modifier.
Câest un sujet avancĂ© en programmation orientĂ©e-objet. Cette leçon est donc optionnelle. Elle est destinĂ©e aux Ă©tudiants qui veulent aller plus loin dans leur apprentissage de la programmation orientĂ©e-objet. Elle nâest pas nĂ©cessaire pour la rĂ©ussite du cours ICS4U.
Définitions
La composition est une alternative Ă lâhĂ©ritage pour la rĂ©utilisation de comportements communs. Au lieu dâĂ©tendre ou dâimplĂ©menter une classe ou une interface, une classe applique la composition en dĂ©clarant des attributs (variables) du type de la classe ou de lâinterface quâelle veut rĂ©utiliser. Cette classe âse composeâ alors en intĂ©grant ces attributs spĂ©cifiques (et les mĂ©thodes quâelles dĂ©finissent).
La composition se fait gĂ©nĂ©ralement avec des interfaces parce que chaque interface est gĂ©nĂ©ralement trĂšs spĂ©cifique. Et on peut composer quelque chose avec peu ou avec beaucoup de composants, soit selon les besoins de lâobjet.
Pourquoi pas simplement implĂ©menter directement toutes ces interfaces? Dans lâimplĂ©mentation, la classe dĂ©finit elle-mĂȘme lâimplĂ©mentation de chaque interface. Elle fixe alors son comportement. Dans la composition, la classe dĂ©lĂšgue lâimplĂ©mentation Ă lâobjet quâelle contient. Câest-Ă -dire que son comportement peut changer selon lâobjet quâelle assigne Ă son attribut.
Avec lâhĂ©ritage et lâimplmentation directe
Voici un exemple de code pour un personnage de jeu vidéo qui peut se déplacer et attaquer. Il implémente directement les interfaces Moves et Skill.
public interface Moves {
void move();
}
public interface Skill {
void applySkill();
}
public class Warrior implements Moves, Skill {
private int health;
private int skillLevel;
// ...autres attributs, constructeurs, etc.
@Override
void move(){
System.out.println("Marching and yelling");
}
@Override
void applySkill(){
System.out.println("Fighting with sword");
}
}
public class Wizard implements Moves, Skill {
private int health;
private int skillLevel;
// ...autres attributs
@Override
void move(){
System.out.println("Floating on an energy ball");
}
@Override
void applySkill(){
System.out.println("Lightning bolt from a staff");
}
}
Tout ça est acceptable Ă la base, mais quâest-ce qui se passe si le Warrior se bat avec une arme diffĂ©rente ou se dĂ©place Ă cheval? Ou si le Wizard change Ă©galement ses comportements durant le jeu. Il faudrait crĂ©er des classes pour chaque variante possible dâun Warrior et dâun Wizard, p.ex : MountedWarriorWithSpear, MountedWarriorWithSword, etc. Câest beaucoup de classes Ă crĂ©er et Ă maintenir! Et si ces personnages devaient Ă©ventuellement incorporer dâautres comportements, il faudrait encore plus de classes!!!
Avec la composition
La composition nous offre une autre solution. Au lieu dâimplĂ©menter directement les interfaces dans les classes des diffĂ©rents types de personnages, on peut utiliser les interfaces comme attributs dans une classe parente. Si on implĂ©mente les interfaces dans des petites classes Ă part, les diffĂ©rents types de personnages ont simplement Ă sâassigner une des ces classes comme valeur de leur attribut. Et si dans le futur on ajoute de nouveaux comportments pour les interfaces existantes, rien ne change dans le code du personnage. En fait, mĂȘme sâil y a aucun nouveau code mais le personnage Ă©volue durant le jeu et obtient un nouveau comportement, câest aussi simple que de lui assigner la bonne implĂ©mentation : on peut changer les comportements durant lâexĂ©cution du programme! Finalement, si on ajoute un nouveau comportement dans une nouvelle version du programme, on ajoute juste un nouvel attribut et les mĂ©thodes nĂ©cessaires pour lâutiliser (accesseurs, mutateurs, etc.). Le code existant nâest pas affectĂ©.
Voici Ă quoi ça ressemble, prĂ©sumant que les interfaces Moves et Skill sont inchangĂ©es de lâexemple prĂ©cĂ©dent :
public class MarchingAndYelling implements Moves {
@Override
void move(){
System.out.println("Marching and yelling");
}
}
public class FightingWithSword implements Skill {
@Override
void applySkill(){
System.out.println("Fighting with sword");
}
}
..etc. Il y a plusieurs petites classes comme celles-ci, chacune implémentant une interface de façon spécifique.
public abstract class Player {
private int health;
private int skillLevel;
private Moves move; // voici la composition : inclut l'interface comme attribut
private Skill skill;
// ...autres attributs, constructeurs, etc.
public void move(){ // parce que c'est une interface on peut définir l'utilisation publique ici
move.move();
}
public void applySkill(){
skill.applySkill();
}
}
public class Warrior extends Player{
public Warrior(){
super();
this.move = new MarchingAndYelling(); // l'enfant choisit l'implémentation spécifique
this.skill = new FightingWithSword();
}
}
public class Wizard extends Player{
public Wizard(){
super();
this.move = new FloatingOnEnergyBall();
this.skill = new LightningBoltFromStaff();
}
}
Dans le cas de la composition, une classe parente abstraite dĂ©finit la structure dâun personnage. Cette structure inclut des attributs qui sont des interfaces (on le reprĂ©sente dans un diagramme UML avec un diamant sur la classe qui âse compose deâ lâautre classe). Les constructeurs des classes enfants dĂ©finissent les valeurs spĂ©cifiques Ă insĂ©rer dans la structure.
Lâarchitecture prĂ©sentĂ©e dans cet exemple avec les personnages et leur comportements sâappelle le patron de conception Strategy. Il y a plusieurs autres patrons de conception qui offrent des solutions pratiques Ă des problĂšmes courants en dĂ©veloppement logiciel, mais cela dĂ©passe le cadre de ce cours.
En utilisant une classe parente abstraite avec la composition, on applique plusieurs bonnes pratiques en programmation orientée-objet :
- La composition produit un couplage faible entre les classes. Les classes sont indĂ©pendantes les unes des autres. On peut changer une classe sans affecter les autres ou sans exiger la production dâune quantitĂ© Ă©norme de code pour acceuillir le changement.
- La composition rĂ©duit aussi le nombre de fois quâil faut supplanter les mĂ©thodes abstraites, notamment le nombre de fois quâon Ă©crit une implĂ©mentation qui sera immĂ©diatement supplantĂ©e par la classe enfant spĂ©cifique que le programme utilise.
- LâhĂ©ritage des attributs dâune classe parente Ă©vite dâavoir Ă copier la structure de la classe pour chaque type. Câest le principe DRY : âdonât repeat yourselfâ. Et si on voulait augmenter la capacitĂ© de
Player, en y ajoutant un nouvel attribut par exemple, on nâaurait quâĂ le faire dans la classe parente. Les classes enfants nâauraient quâĂ spĂ©cifier la partie qui leur est unique : le choix dâune implĂ©mentation pour le nouvel attribut.
Objectifs dâapprentissage
Ă la fin de cette leçon vous devrez ĂȘtre en mesure de :
- Explorer des architectures plus complexes qui appliquent la composition afin de voir comment utiliser la programmation orientée-objet dans un projet de développement logiciel plus réaliste.
CritĂšres de succĂšs
- Je peux incorporer la composition dans mes propres classes.
Exercices
Recherche :
- Faire une recherche sur un patron de conception qui vous intéresse.
- Tentez dâidentifier les concepts de la programmation orientĂ©e-objet qui sont appliquĂ©es : hĂ©ritage, interfaces, composition, etc.
- Identifier des cas oĂč ce patron de conception pourrait ĂȘtre utile dans un projet de dĂ©veloppement logiciel.