Vous avez vu, lors du chapitre précédent, que la composition (= "a un") est souvent préférable à l'héritage (= "est un") : vous aviez défini de nouveaux comportements pour vos objets en créant un super-type d'objet par comportement.
Ce pattern aussi utilise la composition comme principe de base ! En fait, au final, vous allez voir que nos objets seront composés d'autres objets. La différence résidera dans le fait que nos nouvelles fonctionnalités ne seront pas obtenues uniquement en créant de nouveaux objets ,
mais en associant ceux-ci avec des objets existants.
Ce sera cette association qui créera de nouvelles fonctionnalités !
Tout ça a l'air bien beau, mais on ne comprend pas grand-chose...
Vous allez voir que tout va devenir limpide.
Nous allons procéder de la façon suivante :
- nous allons créer un objet DecorJour ;
- nous allons lui ajouter un nuage ;
- nous allons aussi lui ajouter un arbre ;
- nous appellerons la méthode qui dessine dans notre composant, et celle-ci dessinera le tout !
Tout ceci démarre avec un concept fondamental :
l'objet de base et les objets qui le décorent DOIVENT avoir le même type !
Ceci pour une bonne raison : polymorphisme,
polymorphisme et
polymorphisme !
Ouh là, on ne te suit pas du tout !
Vous allez comprendre. En fait, les objets qui vont décorer notre décor vont avoir la même méthode
paintComponent(Graphics g)
que notre objet principal et nous allons faire fondre cet objet dans les autres !
Ceci signifie que nos objets qui vont servir de décorateur vont avoir une instance de type
Item en leur sein ! Ceux-ci vont englober les instances les unes après les autres et du coup, nous pourrons appeler la méthode
paintComponent(Graphics g)
de manière récursive !
Vous pouvez voir les décorateurs comme des poupées russes : vous pouvez mettre une poupée dans une autre !
Ce qui signifie que si nous décorons notre décor avec un objet nuage, la situation pourrait être symbolisée comme suit :
Vous pouvez voir que nos deux objets ont une méthode
paintComponent(Graphics g)
et que l'instance de notre décor se trouve maintenant dans notre objet
Nuage. À ce stade, si nous voulons rajouter un élément de décoration, il nous suffit d'appliquer le même principe. Voici un schéma symbolisant l'ajout d'un arbre décorant le nouvel objet :
L'arbre contient l'instance de la classe
Nuage qui, elle, contient l'instance de
DecorJour...
En fait, on va passer notre instance d'objet en objet !
À peu de chose près, c'est ça ! Sauf que seuls les éléments décorant prendront une instance en paramètre...
Et comment tu vas faire pour ajouter les fonctionnalités des objets décorant ?
Tout simplement en appelant la méthode
paintComponent(Graphics g)
de l'instance se trouvant dans l'objet avant de faire les traitements de la même méthode de l'objet courant !
Souvenez-vous lorsque j'expliquais comment fonctionne la pile d'invocation des méthodes dans un thread.
- La méthode de l'objet le plus global, Arbre, est appelé en premier.
- Celle-ci appelle la méthode de l'objet de type Item, ici, un objet Nuage, se trouvant en son sein.
- La méthode du dit objet est à son tour invoquée, mais invoque aussi la méthode de l'objet Item qu'il englobe ; nous arrivons à notre objet DecorJour.
- Celui-ci va tracer la pelouse, le soleil et le ciel.
- La méthode terminée, les instructions de l'objet Nuage sont exécutées, celui-ci trace un nuage.
- Et pour finir, les instructions de l'objet Arbre sont exécutées, un arbre apparaît !
Voici un schéma résumant la situation :
Je pense que vous devez y voir un peu plus clair... Mais un exemple concret est toujours plus parlant.
Voici le diagramme de classe de notre programme :
Heu...
Pourquoi tu as mis une classe abstraite entre les objets qui vont nous servir de décorateur et la super-classe
Item ?
Tout simplement parce nous voulons que seuls les objets décorant aient une instance d'
Item en leur sein ! Si nous n'avions pas la relation entre
Decorator et
Item, nous aurions eu une liaison de notre super-classe vers elle-même... Et du coup, nos objets de décor (
DecorJour et
DecorNuit) auraient eu une instance d'eux-mêmes, ce qui aurait signifié qu'un objet
DecorJour peut en décorer un autre ! Et nous ne voulons pas ça !
Voilà pourquoi il y a une classe abstraite.
Tout comme pour le pattern strategy, l'utilisation d'une classe abstraite permet de définir un super-type d'objet. Nous aurions très bien pu utiliser une interface...
Voici le code source des classes rajoutées dans notre hiérarchie, mais avant de regarder, essayez de créer ces implémentations tous seuls :
Secret (cliquez pour afficher)
Decorator.java
Code : Java 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24 | package com.sdz.decorator;
import java.awt.Graphics;
import javax.swing.JPanel;
public abstract class Decorator extends Item {
protected Item item;
/**
* @param pan
*/
public Decorator(JPanel pan, Item item) {
super(pan);
this.item = item;
}
public void paintComponent(Graphics g) {
//Nous appelons la méthode de la super-classe
super.paintComponent(g);
//Enfin nous appelons la méthode de notre instance !
this.item.paintComponent(g);
}
}
|
Nuage.java
Code : Java 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47 | package com.sdz.decorator;
import java.awt.Color;
import java.awt.Graphics;
import javax.swing.JPanel;
public class Nuage extends Decorator {
/**
* @param pan
* @param item
*/
public Nuage(JPanel pan, Item item) {
super(pan, item);
}
public void paintComponent(Graphics g) {
//On invoque la méthode de la classe Decorator
super.paintComponent(g);
//On trace notre nuage de taille 30 de haut
//à un endroit au hasard dans le ciel
//Ordonnée à ne pas dépasser
int y = this.parent.getHeight() - this.parent.getHeight()/3;
int x = this.parent.getWidth();
int ordY = y, ordX = x;
//tant que les coordonnées ne sont pas bonnes
do{
ordY = (int)(Math.random() * 100);
ordX = (int)(Math.random() * 100);
}while((ordY > y && ordY < 55) && (ordX > x && ordX < x+80));
//Couleur des nuages : blanc
g.setColor(Color.white);
//On dessine des ronds blancs de différentes dimensions
//presque collés
g.fillOval(ordX+30, ordY, 20, 20);
g.fillOval(ordX+42, ordY-8, 28, 28);
g.fillOval(ordX+60, ordY-14, 34, 34);
g.fillOval(ordX+80, ordY-8, 28, 28);
g.fillOval(ordX+100, ordY, 20, 20);
}
}
|
Arbre.java
Code : Java 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50 | package com.sdz.decorator;
import java.awt.Color;
import java.awt.Graphics;
import javax.swing.JPanel;
public class Arbre extends Decorator {
/**
* @param pan
* @param item
*/
public Arbre(JPanel pan, Item item) {
super(pan, item);
}
public void paintComponent(Graphics g) {
//On invoque la méthode de la classe Decorator
super.paintComponent(g);
//On trace notre nuage de taille 30 de haut
//à un endroit au hasard dans le ciel
//Ordonnée à ne pas dépasser
int y = this.parent.getHeight() - this.parent.getHeight()/3;
int x = this.parent.getWidth();
int ordY = y, ordX = x;
//tant que les coordonnées ne sont pas bonnes
do{
do{
ordY = (int)(Math.random() * 1000);
ordX = (int)(Math.random() * 100);
}while(ordY < (this.parent.getHeight() - this.parent.getHeight()/3));
}while(ordY > this.parent.getHeight());
//System.out.println("ordY : " + ordY);
//Couleur du tronc : marron
g.setColor(new Color(169, 97, 36));
//On crée les coordonnées de nos points
int[] tabX = {ordX, ordX+10, ordX+30, ordX+40, ordX+35, ordX+35, ordX+5, ordX+5, ordX};
int[] tabY = {ordY, ordY+5, ordY+5, ordY , ordY-5, ordY-60, ordY-60, ordY-5, ordY};
g.fillPolygon(tabX, tabY, 9);
g.setColor(new Color(0, 140, 0));
g.fillOval(ordX-15, ordY-150, 70, 100);
}
}
|
Panneau.java
Code : Java 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33 | import java.awt.Color;
import java.awt.Graphics;
import javax.swing.JPanel;
import com.sdz.decorator.*;
public class Panneau extends JPanel {
private Item decor;
/**
* @param decor
*/
public Panneau(Item decor) {
super();
this.decor = decor;
}
/**
* Par défaut
*/
public Panneau(){
this.decor = new DecorJour(this);
this.decor = new Nuage(this, this.decor);
this.decor = new Nuage(this, this.decor);
this.decor = new Arbre(this, this.decor);
}
public void paintComponent(Graphics g){
this.decor.paintComponent(g);
}
}
|
Vous pouvez voir ce que ce code donne avec le screenshot ci-dessous :
Vous avez remarqué que le seul morceau de code qui a changé se trouvait dans le constructeur de notre objet
Panneau ! Voici le morceau de code en question :
Code : Java1
2
3
4
5
6
7
8 | //On crée notre décor de fond
this.decor = new DecorJour(this);
//On ajoute un nuage
this.decor = new Nuage(this, this.decor);
//Un deuxième nuage
this.decor = new Nuage(this, this.decor);
//Et un arbre
this.decor = new Arbre(this, this.decor);
|
Comme je vous l'ai expliqué plus haut, lorsque notre objet va invoquer la méthode
paintComponent(Graphics g)
de son décor, l'invocation va se faire comme mentionné précédemment.
Ici, nous voulons que les éléments se décorent dans un ordre distinct :
- le fond se peint en premier ;
- ensuite, les décorateurs (nuages, arbre).
Vous devez comprendre pourquoi : si nous avions peint les décorateurs en premier et le fond en dernier, vous n'auriez vu que le fond puisqu'il recouvre toute la surface de notre conteneur ! Les décorateurs auraient été effacés, tout simplement.
C'est pour cette raison que, dans la méthode
paintComponent(Graphics g)
de la super-classe
Decorator, nous avons mis l'invocation de cette même méthode de l'objet
Item en son sein avant de faire toute chose ! Ainsi, les décorateurs sont peints après le fond et donc recouvrent celui-ci par une nuage, un arbre...
Vous devez savoir que le pattern decorator fonctionne aussi dans l'autre sens, c'est-à-dire que vous pouvez exécuter le code du premier objet en tout premier lieu et invoquer la même méthode de l'objet Item ensuite.
Dans notre cas, ça n'a que peu d'intérêt pour la raison que nous venons de voir...
Voilà, je vous félicite d'avoir appris à utiliser votre deuxième pattern de conception !
L'avantage de tels modèles de conception doit vous paraître de plus en plus évidente, maintenant.
C'est sûr que là, on comprend mieux. Il était difficile de croire qu'on puisse ajouter des fonctionnalités à des objets sans modifier le code source de ceux-ci, mais c'est vrai !
En plus, nous avons utilisé un principe de conception, très important, sans que je vous le dise :
il est préférable, dans les limites du possible, de restreindre les possibilités de changement d'un objet.
Pour faire simple, il faut éviter qu'un objet soit habilité à faire des actions différentes. Par exemple, vous avez vu que, dans le package
java.io
, les classes sont regroupées par fonctionnalité :
- les classes qui lisent les flux ;
- les classes qui écrivent sur les flux.
Ceci car une classe qui aurait pour rôle de faire les deux actions pourrait être amenée à changer si la façon de lire change OU si la façon d'écrire change, OU les deux !
Vous avez pu constater, lors du chapitre précédent, que les changements éventuels peuvent être des ennemis redoutables : inutile, donc, de leur faciliter la tâche !
Au fait, tu nous as dit que ce pattern était utilisé dans des classes Java...
Tout à fait ! Le moment est venu de vous révéler ce grand secret.