Accueil > Programmation orientée objet >
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.
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.
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
.
1
2
3
public interface Moves {
void move();
}
1
2
3
public interface Skill {
void applySkill();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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!!!
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 :
1
// interfaces Moves et Skill inchangées de l'exemple précédent
1
2
3
4
5
6
public class MarchingAndYelling implements Moves {
@Override
void move(){
System.out.println("Marching and yelling");
}
}
1
2
3
4
5
6
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.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
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();
}
}
1
2
3
4
5
6
7
public class Warrior extends Player{
public Warrior(){
super();
this.move = new MarchingAndYelling(); // l'enfant choisit l'implémentation spécifique
this.skill = new FightingWithSword();
}
}
1
2
3
4
5
6
7
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 :
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.À la fin de cette leçon vous devrez être en mesure de :
Recherche :