Aller au menu - Aller au contenu

Ajouter des fonctionnalités dynamiquement à vos objets : le pattern decorator


Informations sur le tutoriel

Avatar
Auteur : cysboy
Difficulté : Connaisseur (3 / 5)
Visualisations : 18 247


Plus d'informations Plus d'informations
Après votre initiation lors du chapitre précédent, nous allons continuer avec un autre pattern très utilisé.
Celui-ci est utilisé dans une hiérarchie de classes Java ! :waw:

Ne vous laissez pas abuser, vous verrez que plusieurs DP sont utilisés dans le langage Java. Je vais m'efforcer de vous les expliquer et de faire le rapprochement avec le langage...


Premier point important
: rappelez-vous que ce qui fait la force des DP, c'est de pouvoir avoir des classes hermétiques à la modification mais capables de s'adapter automatiquement !


Dans ce chapitre, nous allons voir qu'il est possible de rajouter des fonctionnalités à vos objets de façon dynamique, donc, sans modifier la moindre ligne de code source dans l'objet utilisant la dite fonctionnalité.

L'exemple que j'ai choisi est très simple, vous verrez...

Je sens que vous devez être impatients de commencer... ;)
Chapitre précédent Sommaire Chapitre suivant

Posons le problème

Vous êtes toujours un jeune développeur plein d'avenir dans un société de jeux vidéo.
Seulement, cette fois, vous devez créer un programme permettant de créer des décors. Vous avez déjà fait un premier jet de code qui semble très prometteur ! Voici le diagramme de classe de votre hiérarchie :

Image utilisateur


Cette structure doit vous être familière, maintenant... Nous avons une fenêtre héritée de JFrame qui a un objet hérité, lui, de JPanel. Ce dernier a un objet permettant de dessiner un décor de fond, qui n'est autre qu'une implémentation de l'interface Item (pattern strategy), afin de prévoir les modifications futures ! ;)

Voici un exemple de code représentant cette hiérarchie :

Secret (cliquez pour afficher)

Fenetre.java



Code : Java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
import javax.swing.JFrame;

public class Fenetre extends JFrame {
	
	public Fenetre(){
		this.setSize(300, 300);
		this.setLocationRelativeTo(null);
		this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		this.setTitle("Decorateur");
		this.setResizable(false);
		this.setContentPane(new Panneau());
	}
	
	public static void main(String[] args){
		Fenetre fen =  new Fenetre();
		fen.setVisible(true);
	}
}


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
import java.awt.Color;
import java.awt.Graphics;

import javax.swing.JPanel;

import com.sdz.decorator.DecorJour;
import com.sdz.decorator.Item;


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);
	}
	
	public void paintComponent(Graphics g){
		this.decor.paintComponent(g);
	}
}



Item.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
package com.sdz.decorator;

import java.awt.Graphics;

import javax.swing.JPanel;

public abstract class Item {

	/**
	 * conteneur parent
	 */
	protected JPanel parent;

	/**
	 * Constructeur avec paramètres
	 * @param width
	 * @param height
	 */
	public Item(JPanel pan) {
		this.parent = pan;
	}

	/**
	 * Constructeur par défaut
	 */
	public Item() {}
	
	public void paintComponent(Graphics g){
		this.parent.getGraphicsConfiguration();
	}	
}



DecorJour.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
package com.sdz.decorator;

import java.awt.Color;
import java.awt.Graphics;

import javax.swing.JPanel;

public class DecorJour extends Item {
	
	/**
	 * 
	 */
	public DecorJour() {
		super();
	}

	/**
	 * @param width
	 * @param height
	 */
	public DecorJour(JPanel pan) {
		super(pan);
	}

	public void paintComponent(Graphics g) {
		super.paintComponent(g);
		//Ciel bleu
		g.setColor(Color.blue);
		g.fillRect(0, 0, this.parent.getWidth(), this.parent.getHeight() - this.parent.getHeight()/3);
		//Pelouse
		g.setColor(Color.green);
		g.fillRect(0, this.parent.getHeight() - this.parent.getHeight()/3, this.parent.getWidth(), this.parent.getHeight());
		//Le soleil
		g.setColor(Color.yellow);
		g.fillOval(this.parent.getWidth() - this.parent.getWidth()/4, this.parent.getWidth()/12, this.parent.getWidth()/6, this.parent.getWidth()/6);
		
	}
}



DecorNuit.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
package com.sdz.decorator;

import java.awt.Color;
import java.awt.Graphics;

import javax.swing.JPanel;

public class DecorNuit extends Item {	
	/**
	 * 
	 */
	public DecorNuit() {
		super();
	}

	/**
	 * @param pan
	 */
	public DecorNuit(JPanel pan) {
		super(pan);
	}

	public void paintComponent(Graphics g) {
		super.paintComponent(g);
		//La nuit
		g.setColor(Color.black);
		g.fillRect(0, 0, this.parent.getWidth(), this.parent.getHeight() - this.parent.getHeight()/3);
		//Pelouse
		g.setColor(new Color(0, 138, 0));
		g.fillRect(0, this.parent.getHeight() - this.parent.getHeight()/3, this.parent.getWidth(), this.parent.getHeight());
		//La lune
		g.setColor(Color.white);
		g.fillOval(this.parent.getWidth() - this.parent.getWidth()/4, this.parent.getWidth()/12, this.parent.getWidth()/6, this.parent.getWidth()/6);
		
	}
}




Vous devriez comprendre ce code sans problème si vous avez lu la partie 3 du tutoriel ! ;)
La seule chose qui change, c'est l'appel à la méthode getGraphicsConfiguration() qui donne l'autorisation de peindre dans notre composant depuis l'extérieur.

Voici le rendu des deux classes dérivant de la classe Item :

Image utilisateur Image utilisateur


La différence entre les deux affichages est minime... Les couleurs changent, c'est tout... ;)
Maintenant, afin de ne pas trop compliquer les choses, nous n'allons travailler que sur l'ajout de fonctionnalités sur la classe DecorJour. Ce que nous voulons faire, c'est trouver un moyen simple et efficace de pouvoir ajouter des éléments à notre décor :
  • un arbre ;
  • un nuage ;
  • ...

Vu que vous connaissez le pattern strategy, vous pouvez trouver une méthode simple : créer une collection pouvant contenir plusieurs objets de type Item et balayer celle-ci lors de l'appel à la méthode painComponent(Graphics g) de votre objet DecorJour. Cette façon de faire est bonne mais il y a une autre façon de faire...
Et si je vous dit en prime qu'il y a un moyen d'arriver à faire ceci sans utiliser de boucle et sans modifier le code source de notre classe DecorJour ? :waw:

On serait curieux de savoir comment tu vas t'y prendre !

Tout simplement en utilisant le pattern decorator. Vous allez voir que celui-ci peut s'avérer très utile dans certaines situations !

Le pattern decorator

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 :

Image utilisateur


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 :

Image utilisateur


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 :

Image utilisateur


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 :

Image utilisateur


Heu... :euh:
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 :

Image utilisateur


Dans les méthodes de nos objets servant à décorer, vous avez pu voir que je génère ces derniers de façon aléatoire dans une zone spécifique : ce screenshot ne sera peut-être pas ce que vous aurez au final... Relancez le programme pour avoir plusieurs aperçus ! ;)


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 : Java
1
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. ^^

Les mystères de java.io

Lorsque nous avons vu les flux d'entrée / sortie en Java, vous avez pu constater qu'il y a un nombre colossal de classes dans le package java.io .

Voici un schéma donnant un aperçu des ces classes :

Image utilisateur


Image utilisateur


Elles ne sont pas toutes présentes, mais ça vous donne un rendu...

Cette quantité de classes s'explique car celles-ci utilisent le pattern decorator !
Les classes présentes dans le deuxième schéma correspondent aux décorateurs des classes se trouvant dans le premier schéma !
Par exemple, vous pouvez avoir ce genre de code :

Code : Java
1
2
3
4
5
FileInputStream fis = new FileInputStream("toto.txt");
//Ou encore
BufferedInputStream bis = new BufferedInputStream(new FileInputStream("toto.txt"));
//Ou alors
DataInputStream dis = new DataInputSteam(new BufferedInputStream(new FileInputStream("toto.txt")));


Vous voyez que nous retrouvons la logique du décorateur dans cette façon de faire !
En fait, chaque décorateur ajoute une fonctionnalité à l'objet de base, ce qui rend celui-ci plus performant ou plus intuitif à utiliser...

Vous pouvez même créer vos propres décorateurs pour ce package ! :D


Maintenant que vous connaissez et savez utiliser le pattern decorator, vous devriez avoir une meilleurs approche de ce package.

Bon : ceci dit, il est temps de faire un tour sur le topo...

Ce qu'il faut retenir

  • Le pattern decorator permet d'ajouter des fonctionnalités de façon dynamique à un objet.
  • Afin d'utiliser le polymorphisme, les décorateurs et les objets destinés à être décorés doivent dériver d'une même super-classe.
  • Les objets décorateurs ont une instance du type de leur super-classe en leur sein.
  • Grâce à cette instance, ils peuvent invoquer la méthode commune et ainsi rajouter des traitements à celle-ci.
  • La façon d'utiliser un décorateur peut se faire dans deux sens :
    • soit en exécutant les traitements de l'objet du super-type en premier. Les méthodes seront exécutées du premier objet instancié vers le dernier,
    • soit en exécutant le code du décorateur en premier : l'exécution se fait en sens contraire, du dernier objet instancié vers le dernier.

Encore un chapitre riche en nouveautés !
J'espère qu'il vous a plu... :)

Vous commencez à apercevoir la toute puissance des patterns de conception.
Nous allons donc continuer avec un pattern non moins pratique : le pattern observer !
Chapitre précédent Sommaire Chapitre suivant

Informations sur le tutoriel

Retour en haut Retour en haut

Créé : Le 21/06/2006 à 15:02:22
Modifié : Le 17/06/2009 à 11:19:46
Avancement : 100%
Licence : Copie non autorisée

2 commentaires