Introduction
On appelle patrons conceptuels (ou « Design pattern « ) un arrangement éprouvé de classes afin de répondre à un problème classique. Un patron de conception est considéré comme un ensemble de bonnes pratiques dans le domaine du développement logiciel.
Les patrons de création
Singleton
Un singleton est une instance unique d’une classe dans un programme.
On utilise un singleton dans le cas où il est souhaitable ou obligatoire qu’une classe n’ait qu’une seule instance dans l’exécution du programme.
Afin de garder le contrôle sur l’utilisation de ce singleton (et ainsi éviter les problèmes de variables globales), le singleton doit avoir un point d’accès unique, traçable et documenté.
Java:
import java.util.List;
import java.util.ArrayList;
public class MonSingleton {
static private MonSingleton instance;
static public MonSingleton getInstance() {
if (instance == null) {
instance = new MonSingleton();
}
return instance;
}
private MonSingleton(){
attribut1 = 0;
attribut2 = new ArrayList<String>();
}
protected int attribut1;
protected ArrayList<String> attribut2;
public void setAttribut1(int aAttribut1) {
attribut1 = aAttribut1;
}
public int getAttribut1() {
return attribut1;
}
public void setAttribut2(List<String> aAttribut2) {
attribut2 = new ArrayList<String> (aAttribut2);
}
public List<String> getAttribut2() {
return new ArrayList<String> (attribut2);
}
}
Les trois (3) éléments importants de cet exemple sont les trois (3) premiers attributs et méthodes de la classe.
En premier lieu, l’attribut privé statique « instance » permet de placer l’instance unique de la classe « MonSingleton » dans un attribut statique. Un attribut statique représente la même valeur, peu importe d’où elle est accédé. Ça permet également d’utiliser l’attribut à partir d’une méthode statique.
La méthode publique statique « getInstance » permet aux autres classes du système d’accéder à l’instance du singleton sans nécessairement permettre l’accès global de l’attribut « instance » (afin d’éviter qu’une autre classe y assigne une valeur). On peut également voir que la méthode « getInstance » regarde si l’instance existe avant de le retourner et si l’instance n’existe pas, il le crée. On appelle cette technique le « Lazy Loading ». Il est également important de noter qu’il n’est pas nécessaire d’avoir une instance de classe pour lancer une méthode statique. Il est possible de faire un appel de méthode, appliqué directement sur la classe.
Finalement, on voit que le constructeur de la classe « MonSingleton » est déclaré comme privé. Cette fonctionnalité permet de nous assurer que le constructeur ne sera pas lancé à partir d’une autre classe que la classe en cours (dans ce cas, « MonSingleton »).
On utilise le singleton de la manière suivante:
public class Programme {
static public void main(String[] args) {
MonSingleton monSingleton = MonSingleton.getInstance();
monSingleton.setAttribut1(10);
}
}
Eiffel:
class
MON_SINGLETON
create {MON_SINGLETON_SHARED}
make
feature {NONE} -- Initialisation
make
do
attribut1 := 0
create {ARRAYED_LIST[STRING]} internal_attribut2.make (0)
end
feature -- Access
attribut1:INTEGER assign set_attribut1
set_attribut1(a_attribut1:INTEGER)
do
attribut1 := a_attribut1
end
attribut2:LIST[STRING] assign set_attribut2
do
Result := internal_attribut2.twin
end
set_attribut2(a_attribut2:LIST[STRING])
do
create {ARRAYED_LIST[STRING]}
internal_attribut2.make_from_iterable (a_attribut2)
end
feature {NONE} -- Implémentation
internal_attribut2:LIST[STRING]
end
La classe « MON_SINGLETON » est une classe complètement standard. La seule et unique particularité à observer est l’utilisation d’un constructeur ami avec la classe « MON_SINGLETON_SHARED »:
create {MON_SINGLETON_SHARED} make
La classe permettant le point d’accès au singleton est la classe « MON_SINGLETON_SHARED » qui est faite de cette manière:
deferred class
MON_SINGLETON_SHARED
feature {NONE} -- Implementation
mon_singleton:MON_SINGLETON
once
create Result.make
end
end
La particularité ici, est le mot clé « once » qui agit d’une manière similaire au « static » de java (pour les méthodes seulement, pas les attributs). Ce qu’une méthode « once » fait, c’est qu’elle exécute la méthode une seule fois dans le programme et si cette méthode retourne une valeur, donc la valeur retournée sera toujours la même.
Finalement, pour accéder au singleton, il ne reste qu’à hériter de la classe « MON_SINGLETON_SHARED » et d’utiliser la méthode « mon_singleton ». Par exemple:
class
APPLICATION
inherit
MON_SINGLETON_SHARED
create
make
feature {NONE} -- Initialisation
make
-- Exécute l'application.
do
mon_singleton.set_attribut1 (10)
end
end
À noter à propos des singletons
-
- Il est important de prendre en note que l’instance créée dans le cas d’un singleton sera en mémoire à partir de sa première utilisation et pour l’entièreté de l’exécution du programme. En d’autres mots, le ramasse-miette (« garbage collector ») ramassera seulement cet objet à la fermeture du programme.
- Cette note est particulièrement importante dans le cas où le contenue du singleton est très lourd en mémoire et peu utile.
- Cette note peut également être importante dans le cas où le singleton utilise des ressources externes (fichiers, libraires chargées dynamiquement, etc.) qui nécessitent d’être libéré avant la fin de l’exécution du programme.
- Une autre note importante est à propos de l’utilisation des singletons dans un contexte multithread. Puisque chaque objet en exécution partage le même singleton, le risque de conflit augmente significativement.
- Enfin, il est important de comprendre que chaque langage a une manière propre qui permet de gérer les singletons. Mais dans le cas où il n’y a aucune manière de créer des singletons dans un langage, un singleton peut également être un objet créé dans le « Main » du programme et envoyé en attribut aux constructeurs de tous les objets du système.
- Il est important de prendre en note que l’instance créée dans le cas d’un singleton sera en mémoire à partir de sa première utilisation et pour l’entièreté de l’exécution du programme. En d’autres mots, le ramasse-miette (« garbage collector ») ramassera seulement cet objet à la fermeture du programme.
Fabrique
Une fabrique (ou « factory« ) est une classe (généralement un singleton), permettant la création de différents types d’objet. Une fabrique peut être utile lorsqu’on veut cacher l’implémentation de la création d’un certain type d’objet, ou bien qu’on veut éviter la duplication d’objet dans le système. Exemple:
Java:
public class BlocFabrique {
public BlocFabrique() {
imageBlocVert = new Image("image_bloc_vert.png");
imageBlocRose = new Image("image_bloc_rose.png");
imageBlocBleu = new Image("image_bloc_bleu.png");
imageBlocJaune = new Image("image_bloc_jaune.png");
imageBlocRouge = new Image("image_bloc_rouge.png");
imageBlocMetal = new Image("image_bloc_metal.png");
}
public Bloc getBlocVert() {
return new Bloc(imageBlocVert);
}
public Bloc getBlocRose() {
return new Bloc(imageBlocRose);
}
public Bloc getBlocBleu() {
return new Bloc(imageBlocBleu);
}
public Bloc getBlocJaune() {
return new Bloc(imageBlocJaune);
}
public Bloc getBlocRouge() {
return new Bloc(imageBlocRouge);
}
public Bloc getBlocMetal() {
return new MetalBloc(imageBlocMetal);
}
private Image imageBlocVert;
private Image imageBlocRose;
private Image imageBlocBleu;
private Image imageBlocJaune;
private Image imageBlocRouge;
private Image imageBlocMetal;
}
Eiffel:
class
BLOC_FABRIQUE
create
make
feature {NONE} -- Initialisation
make
do
create image_bloc_vert.make ("image_bloc_vert.png")
create image_bloc_rose.make ("image_bloc_rose.png")
create image_bloc_bleu.make ("image_bloc_bleu.png")
create image_bloc_jaune.make ("image_bloc_jaune.png")
create image_bloc_rouge.make ("image_bloc_rouge.png")
create image_bloc_metal.make ("image_bloc_metal.png")
end
feature -- Accès
bloc_vert:BLOC
do
create Result.make(image_bloc_vert)
end
bloc_rose:BLOC
do
create Result.make(image_bloc_rose)
end
bloc_bleu:BLOC
do
create Result.make(image_bloc_bleu)
end
bloc_jaune:BLOC
do
create Result.make(image_bloc_jaune)
end
bloc_rouge:BLOC
do
create Result.make(image_bloc_rouge)
end
bloc_metal:BLOC
do
create Result.make(image_bloc_metal)
end
feature {NONE} -- Implémentation
image_bloc_vert:IMAGE
image_bloc_rose:IMAGE
image_bloc_bleu:IMAGE
image_bloc_jaune:IMAGE
image_bloc_rouge:IMAGE
image_bloc_metal:IMAGE
end
Ces exemples sont pris d’un jeu Arkanoid. Dans ces exemples, on présume qu’une image est très lourde en mémoire RAM, donc il est préférable de ne pas dupliquer ces images. Par contre, il est illogique de placer ces images ailleurs que dans une classe spécialisée sur les « Bloc ». L’idéal serait probablement que la classe « Bloc » elle-même charge l’image, mais se faisant, l’image d’un bloc d’une certaine couleur sera dupliquée dans tous les blocs de cette couleur. En faisant une fabrique de blocs, on crée une classe spécialisée dans la création de blocs et peut s’occuper de ne pas dupliquer l’image de chaque couleur de blocs.
Dans l’exemple d’Eiffel précédent, on pourrait également s’assurer que seule la classe {FABRIQUE_BLOC} peut créer un {BLOC} en rendant la classe {BLOC} ami avec {FABRIQUE_BLOC} (voir mécanique dans le singleton).
Fabrique abstraite
Lorsque le programme gère plusieurs variantes différentes des mêmes objets, on peut utiliser une fabrique abstraite (ou « abstract factory« ) afin d’éviter au programmeur d’avoir à se soucier du type d’objet à gérer.
Monteur
Un monteur (ou « builder« ) est une classe qui permet d’assigner certains attributs d’un objet avant de le créer. Souvent, un monteur est utilisé lorsqu’un objet doit être initialisé directement avec ses attributs préassignés, mais qu’en utilisant seulement des arguments du constructeur, ces derniers seraient trop complexes.
Les patrons structurels
Adaptateur
Un adaptateur (ou « Adapter« ) permet de faire fonctionner ensemble deux objets ayant des interfaces différentes, mais permettant un travail similaire. Par exemple, nous allons développer les classes suivantes:
L’objectif de la classe Adaptateur_Jouet, est de permettre à un Jouet d’être utilisé comme un Oiseau. Voici l’implémentation de ces classes.
Java:
public abstract class Oiseau {
public abstract void crier();
}
public class Corneille extends Oiseau {
public void crier() {
System.out.println("Haar Haar");
}
}
public class Merle extends Oiseau {
public void crier() {
System.out.println("Pit Pit");
}
}
public abstract class Jouet {
public abstract void ecraser();
}
public class CanardDeBain extends Jouet {
public void ecraser() {
System.out.println("Squick Squick");
}
}
public class AdaptateurJouet extends Oiseau {
private Jouet jouet;
public AdaptateurJouet(Jouet aJouet) {
jouet = aJouet;
}
public void crier() {
jouet.ecraser();
}
}
Et voici un exemple d’utilisation de l’adaptateur:
import java.util.List;
import java.util.ArrayList;
public class Programme {
static public void main(String[] args) {
List<Oiseau> oiseaux = new ArrayList<Oiseau>(3);
Jouet canard = new CanardDeBain();
oiseaux.add(new Corneille());
oiseaux.add(new Merle());
oiseaux.add(new AdaptateurJouet(canard));
for(Oiseau oiseau : oiseaux) {
oiseau.crier();
}
}
}
Eiffel:
deferred class
OISEAU
feature -- Accès
crier
deferred
end
end
class
CORNEILLE
inherit
OISEAU
feature -- Accès
crier
do
print("Haar Haar")
end
end
class
MERLE
inherit
OISEAU
feature -- Accès
crier
do
print("Pit Pit")
end
end
deferred class
JOUET
feature -- Accès
ecraser
deferred
end
end
class
CANARD_DE_BAIN
inherit
JOUET
feature -- Accès
ecraser
do
print("Squick Squick")
end
end
class
ADAPTATEUR_JOUET
inherit
OISEAU
create
make
feature {NONE} -- Initialisation
make(a_jouet:JOUET)
do
jouet := a_jouet
end
feature -- Accès
crier
do
jouet.ecraser
end
feature {NONE} -- Implementation
jouet:JOUET
end
Pour utiliser l’adaptateur:
class
APPLICATION
create
make
feature {NONE} -- Initialisation
make
-- Exécute l'application.
local
oiseaux:LIST[OISEAU]
canard:CANARD_DE_BAIN
do
create {ARRAYED_LIST[OISEAU]} oiseaux.make(3)
create canard
oiseaux.extend (create {CORNEILLE})
oiseaux.extend (create {MERLE})
oiseaux.extend (create {ADAPTATEUR_JOUET}.make(canard))
across oiseaux as la_oiseaux loop
la_oiseaux.item.crier
io.put_new_line
end
end
end
Lors de l’exécution de ce code, on obtient le résultat suivant:
Haar Haar Pit Pit Squick Squick
Décorateur
Un décorateur (ou « decorator« ) permet d’ajouter des fonctionnalités à une classe, sans avoir à modifier cette classe directement. Ce type de patron peut être utile, par exemple, dans le cas où l’on ne voulait pas avoir à modifier tout le code d’un programme à cause d’une modification d’une classe. Également, ce type de patron peut être utile pour s’assurer qu’une classe n’ait pas trop de responsabilités. Par exemple, voyez le diagramme suivant:
L’objectif de ce « desing » est d’ajouter un menu à une Page Web, sans nécessairement avoir à implanter ce menu dans la classe de la page Web (donc, en gardant cette dernière simple).
public class PageWeb {
public String getTexte() {
return "Le texte de la page Web";
}
}
public abstract class DecorateurPage {
private PageWeb page;
public DecorateurPage(PageWeb aPage) {
page = aPage;
}
public String getTexte() {
return page.getTexte();
}
}
public class DecorateurPageMenu extends DecorateurPage {
public DecorateurPageMenu(PageWeb aPage) {
super(aPage);
}
public String getTexte() {
String textePage = super.getTexte();
String resultat;
// Ajouter le menu dans le textePage et mettre le résultat dans resultat.
return resultat;
}
}
public class Programme {
static public void main(String[] args) {
DecorateurPage page = new DecorateurPageMenu(new PageWeb());
System.out.println(page.getTexte());
}
}
Eiffel:
class
PAGE_WEB
feature -- Accès
texte:STRING
do
Result := "Le texte de la page web."
end
end
deferred class
DECORATEUR_PAGE
feature {NONE} -- Initialisation
make(a_page:PAGE_WEB)
do
page := a_page
end
feature -- Accès
texte:STRING
do
Result := page.texte
end
feature {NONE} -- Implémentation
page:PAGE_WEB
end
class
DECORATEUR_PAGE_MENU
inherit
DECORATEUR_PAGE
redefine
texte
end
create
make -- Le constructeur de cette classe est le make de {DECORATEUR_PAGE}
feature -- Accès
texte:STRING
local
l_texte_page:STRING
do
l_texte_page := Precursor
-- Ajouter le menu dans le texte l_texte_page et mettre le résultat dans Result
end
end
class
APPLICATION
create
make
feature {NONE} -- Initialisation
make
-- Exécuter l'application.
local
l_page:DECORATEUR_PAGE
do
create {DECORATEUR_PAGE_MENU} l_page.make (create {PAGE_WEB})
print(l_page.texte)
io.put_new_line
end
end
Modèle-Vue-Controlleur (MVC)
Le principe du patron MVC est de séparer les classes en 3 parties: les classes du modèle, les vues et les contrôleurs.
les classes du modèle
Les classes du modèle sont les classes directement reliées au domaine d’affaire que le programme permet une solution. Par exemple, si vous faites un programme pour un magasin, les classes du modèle seront des classes comme: Produit, Vendeur, Caisse, etc. Si vous faites un programme pour une école, les classes du modèle seront des classes comme: Étudiant, Enseignant, Classe, Bulletin, etc.
Les vues
Les vues sont toutes les classes qui gèrent l’interface graphique. Les classes de vues sont les classes qui ont la responsabilité de gérer des interfaces homme-machine comme: l’affichage (fenêtre, ligne de commande, etc.) ou les entrées (clavier, souris, bouton, case de texte, etc.) L’utilisateur manipule directement les classes de la vue.
Les contrôleurs
Les contrôleurs sont les classes contenant la logique applicative (les algorithmes de l’application). Ils font le lien entre les classes du modèle et les classes de la vue.
La séparation
L’important dans le patron MVC, c’est la séparation des différents types de classes (modèle, vue et contrôleur). Voici un diagramme d’interdépendance entre ces types:
Dans ce diagramme, les flèches représentent les dépendances entre les types de classes. Il faut y lire que la vue dépend du modèle seulement; les contrôleurs dépendent du modèle et de la vue et le modèle est complètement indépendant. Une autre façon de se le représenter, c’est que la vue « connaît » le modèle, mais pas le contrôleur; le contrôleur « connaît » le modèle et la vue et le modèle ne « connaît » rien d’autre que lui-même.
Patrons de comportement
Chaine de responsabilité
Une chaine de responsabilité permet de transmettre la responsabilité d’une gestion vers un autre objet en suivant une chaine ou un arbre d’objets interreliés entre eux. Par exemple:
On voit qu’il y a un principe parent/enfant dans une vue. À un endroit donné d’une Vue, il peut y avoir plusieurs Vues différentes (par exemple: une fenêtre, une grille de Vues et un Bouton). Donc, lorsque la Fenêtre reçoit une clique de souris, en plus de traiter lui-même ce clique, il va lancer le clique de ses Vues enfants.
Java:
import java.util.LinkedList;
import java.util.ArrayList;
import java.util.List;
import java.lang.IllegalArgumentException;
public abstract class Vue {
private LinkedList<Vue> enfants;
private Vue parent;
public Vue(Vue aParent) {
enfants = new LinkedList<Vue>();
parent = aParent;
if (parent != null) {
parent.ajouterEnfant(this);
}
}
public List<Vue> getEnfants() {
return new ArrayList<Vue>(enfants);
}
public void ajouterEnfant(Vue aEnfant) {
if (aEnfant.parent == this) {
enfants.add(aEnfant);
} else {
throw new IllegalArgumentException();
}
}
}
public abstract class CliquableVue extends Vue{
public CliquableVue(Vue aParent) {
super(aParent);
}
public void gestionClique(int aX, int aY) {
CliquableVue cliquableEnfant;
for(Vue enfant : getEnfants()) {
if (enfant instanceof CliquableVue) {
cliquableEnfant = (CliquableVue) enfant;
cliquableEnfant.gestionClique(aX, aY);
}
}
}
}
public class Grille extends CliquableVue {
public Grille(Vue aParent) {
super(aParent);
}
@Override
public void gestionClique(int aX, int aY) {
super.gestionClique(aX, aY);
// Gestion particulière du clique de souris pour la grille
}
}
public class Fenetre extends CliquableVue {
public Fenetre(Vue aParent) {
super(aParent);
}
@Override
public void gestionClique(int aX, int aY) {
super.gestionClique(aX, aY);
// Gestion particulière du clique de souris pour la Fenetre
}
}
public class Bouton extends CliquableVue {
public Bouton(Vue aParent) {
super(aParent);
}
@Override
public void gestionClique(int aX, int aY) {
super.gestionClique(aX, aY);
// Gestion particulière du clique de souris pour le bouton.
}
}
public class Programme {
static public void main(String[] args) {
Fenetre fenetre = new Fenetre(null);
Grille grille = new Grille(fenetre);
Bouton bouton1 = new Bouton(grille);
Bouton bouton2 = new Bouton(grille);
fenetre.gestionClique(10, 10);
}
}
Eiffel:
deferred class
VUE
feature {NONE} -- Initilisation
make(aParent:detachable VUE)
do
create {LINKED_LIST[VUE]} internal_enfants.make
parent := aParent
if attached parent as laParent then
laParent.ajouter_enfant(Current)
end
end
feature -- Accès
parent:detachable VUE
enfants:LIST[VUE]
do
Result := internal_enfants.twin
end
feature {VUE} -- Implémentation
ajouter_enfant(a_enfant:VUE)
require
Is_Parent: a_enfant.parent = Current
do
internal_enfants.extend(a_enfant)
end
feature {NONE} -- Implémentation
internal_enfants:LIST[VUE]
end
deferred class
CLIQUABLE_VUE
inherit
VUE
feature -- Accès
gestion_clique(a_x, a_y:INTEGER)
do
across enfants as la_enfants loop
if attached {CLIQUABLE_VUE} la_enfants.item as la_enfant then
la_enfant.gestion_clique(a_x, a_y)
end
end
end
end
class
FENETRE
inherit
CLIQUABLE_VUE
redefine
gestion_clique
end
create
make
feature -- Accès
gestion_clique(a_x, a_y:INTEGER)
do
Precursor(a_x, a_y)
-- Gestion particulière du clique de souris pour la fenêtre.
end
end
class
GRILLE
inherit
CLIQUABLE_VUE
redefine
gestion_clique
end
create
make
feature -- Accès
gestion_clique(a_x, a_y:INTEGER)
do
Precursor(a_x, a_y)
-- Gestion particulière du clique de souris pour le bouton.
end
end
class
BOUTON
inherit
CLIQUABLE_VUE
redefine
gestion_clique
end
create
make
feature -- Accès
gestion_clique(a_x, a_y:INTEGER)
do
Precursor(a_x, a_y)
-- Gestion particulière du clique de souris pour le bouton.
end
end
class
APPLICATION
create
make
feature {NONE} -- Initialisation
make
-- Exécuter l'application.
local
fenetre:FENETRE
grille:GRILLE
bouton1, bouton2:BOUTON
do
create fenetre.make (Void)
create grille.make (fenetre)
create bouton1.make (grille)
create bouton2.make (grille)
fenetre.gestion_clique (10, 10)
end
end
Itérateur
Un itérateur permet d’itérer sur une structure de donnée sans avoir besoin de se soucier le la mécanique interne de la structure.
Médiateur
Un médiateur (ou « mediator« ) permet de transférer les messages entre différentes classes. Ce type de classe permet de diminuer l’interdépendance entre les différentes classes. En effet, en général, on essaie de diminuer l’interdépendance des classes (souvent appelé le principe du faible couplage), afin d’optimiser la réutilisabilité.
Si dans mon projet, j’ai par exemple une fenêtre et un produit. Dans la fenêtre, il y a un champ texte permettant de modifier le nom du produit et un bouton OK permettant d’accepter la modification du nom du produit. De son côté, le produit a une méthode « change_nom » permettant de modifier le nom du produit qui vérifie que le nom est valide. Si le nom n’est pas valide, le produit doit en informer la fenêtre afin que cette dernière affiche un message d’erreur pour l’utilisateur.
On pourrait représenter cet exemple avec le diagramme de classe suivant:
Par contre, ici, la fenêtre et le produit deviennent dépendants un de l’autre puisque la fenêtre nécessite le produit pour lancer sa méthode « changer_nom » et le produit nécessite la fenêtre pour lancer la méthode « erreur_nom ».
En utilisant un médiateur, on peut s’assurer de garder l’interdépendance entre les deux classes. Voici un exemple:
Java:
public class Fenetre {
public Fenetre() {
texte = "";
}
private String texte;
public void setTexte(String aTexte) {
texte = aTexte;
}
public String getTexte() {
return texte;
}
public void boutonClique() {
if (mediateur != null) {
mediateur.changeTexte(texte);
}
}
public void setMediateur(MediateurFenetre aMediateur) {
mediateur = aMediateur;
}
public void erreurTexte() {
System.out.println("Le texte n'est pas valide.");
}
private MediateurFenetre mediateur;
}
public class Produit {
public Produit() {
nom = "";
}
private String nom;
public String getNom() {
return nom;
}
public void changeNom(String aNom) {
if (aNom == null || aNom.isEmpty()) {
mediateur.erreurNom();
} else {
nom = aNom;
}
}
public void setMediateur(MediateurProduit aMediateur) {
mediateur = aMediateur;
}
MediateurProduit mediateur;
}
public interface MediateurFenetre {
public void changeTexte(String aTexte);
}
public interface MediateurProduit {
public void erreurNom();
}
public class MediateurFenetreProduit implements MediateurFenetre, MediateurProduit {
public MediateurFenetreProduit(Fenetre aFenetre, Produit aProduit) {
fenetre = aFenetre;
produit = aProduit;
fenetre.setMediateur(this);
produit.setMediateur(this);
}
private Fenetre fenetre;
private Produit produit;
public void erreurNom() {
fenetre.erreurTexte();
}
public void changeTexte(String aTexte) {
produit.changeNom(aTexte);
}
}
Eiffel:
class
FENETRE
create
make
feature {NONE} -- Initialisation
make
do
texte := ""
end
feature -- Accès
texte:STRING assign set_texte
set_texte(a_texte:STRING)
do
texte := a_texte
end
bouton_clique
do
if attached mediateur as la_mediateur then
la_mediateur.change_texte(texte)
end
end
erreur_texte
do
print("Le texte n'est pas valide.%N")
end
mediateur:detachable MEDIATEUR_FENETRE assign set_mediateur
set_mediateur(a_mediateur:detachable MEDIATEUR_FENETRE)
do
mediateur := a_mediateur
end
end
class
PRODUIT
create
make
feature {NONE} -- Initialisation
make
do
nom := ""
end
feature -- Accès
nom:STRING
change_nom(a_nom:STRING)
do
if attached mediateur as la_mediateur then
la_mediateur.erreur_nom
end
end
mediateur:detachable MEDIATEUR_PRODUIT assign set_mediateur
set_mediateur(a_mediateur:detachable MEDIATEUR_PRODUIT)
do
mediateur := a_mediateur
end
end
deferred class
MEDIATEUR_FENETRE
feature -- Accès
change_texte(a_texte:STRING)
deferred
end
end
deferred class
MEDIATEUR_PRODUIT
feature -- Accès
erreur_nom
deferred
end
end
class
MEDIATEUR_FENETRE_PRODUIT
inherit
MEDIATEUR_FENETRE
MEDIATEUR_PRODUIT
create
make
feature {NONE} -- Initialisation
make(a_fenetre:FENETRE; a_produit:PRODUIT)
do
fenetre := a_fenetre
produit := a_produit
fenetre.set_mediateur (Current)
produit.set_mediateur (Current)
end
feature -- Accès
fenetre:FENETRE
produit:PRODUIT
change_texte(a_texte:STRING)
do
produit.change_nom(a_texte)
end
erreur_nom
do
fenetre.erreur_texte
end
end
class
APPLICATION
create
make
feature {NONE} -- Initialisation
make
-- Exécute l'application.
local
produit:PRODUIT
fenetre:FENETRE
mediateur:MEDIATEUR_FENETRE_PRODUIT
do
create fenetre.make
create produit.make
create mediateur.make (fenetre, produit)
fenetre.bouton_clique
end
end
On voit que dans ce cas, nous pourrions facilement mettre les classes « Fenetre » et « MediateurFenetre » dans un « package », « dépôt », « librairie », etc. et faire de même avec « Produit » et « MediateurProduit ». Ils sont donc rendus indépendants. Lorsque l’utilisateur veut utiliser la classe « Produit » ou « Fenetre », il doit se créer une classe médiatrice qui hérite (implémente dans le cas de java) du médiateur respectif (« MediateurProduit » ou « MediateurFenetre »).
Mémento
Un mémento (ou « memento« ) permet de garder l’état d’un objet pour pouvoir retourner à cet état dans le futur. Ce type de mécanique est généralement utile lorsque le logiciel nécessite la fonctionnalité de « défaire » et de « refaire » (« undo » et « redo »).
Le principe du Mémento est le suivant:
L’objet à sauvegarder contient des attributs représentants l’état de l’objet à un instant donné (par exemple, ici, c’est le texte et le curseur). Ensuite, on permet de créer des objets Mémento contenant une copie de l’état à partir de l’objet qu’on veut permettre de défaire et refaire les modifications (ici, la boite texte). Un autre objet Gardien permet de sauvegarder et de retourner les différents Mémentos. Finalement, l’objet qu’on veut permettre de défaire et refaire les modifications doit également avoir une méthode pour remettre son état à l’état d’un mémento.
Java:
public class BoiteTexte {
private String texte;
private int curseur;
public BoiteTexte() {
setTexte("");
setCurseur(0);
}
public void setTexte(String aTexte) {
texte = aTexte;
}
public String getTexte() {
return texte;
}
public void setCurseur(int aCurseur) {
curseur = aCurseur;
}
public int getCurseur() {
return curseur;
}
public TexteMemento creerMemento() {
return new TexteMemento(new String(texte), curseur);
}
public void setMemento(TexteMemento aMemento) {
setTexte(new String(aMemento.getTexte()));
setCurseur(aMemento.getCurseur());
}
}
public class TexteMemento {
private String texte;
private int curseur;
public String getTexte() {
return texte;
}
public int getCurseur() {
return curseur;
}
public TexteMemento(String aTexte, int aCurseur) {
texte = aTexte;
curseur = aCurseur;
}
}
import java.util.List;
import java.util.LinkedList;
public class Gardien {
private List<TexteMemento> etats;
public List<TexteMemento> getEtats() {
return etats;
}
private int index;
public int getIndex() {
return index;
}
public Gardien() {
etats = new LinkedList<TexteMemento>();
index = -1;
}
public void ajouterMemento(TexteMemento aMemento) {
if (index < etats.size() - 1){
etats = etats.subList(0, index + 1);
}
etats.add(aMemento);
index = index + 1;
}
public TexteMemento getMemento() {
TexteMemento lMemento = null;
if (index < etats.size() && index > -1) {
lMemento = etats.get(index);
} else {
throw new IndexOutOfBoundsException("Index invalid");
}
return etats.get(index);
}
public void precedent() {
if (index > 0) {
index = index - 1;
} else {
throw new IndexOutOfBoundsException("Index invalid");
}
}
public void suivant() {
if (index < etats.size()) {
index = index + 1;
} else {
throw new IndexOutOfBoundsException("Index invalid");
}
}
}
public class Programme {
static public void main(String[] args) {
BoiteTexte boiteTexte = new BoiteTexte();
Gardien gardien = new Gardien();
boiteTexte.setTexte("État 1");
boiteTexte.setCurseur(1);
gardien.ajouterMemento(boiteTexte.creerMemento());
boiteTexte.setTexte("État 2");
boiteTexte.setCurseur(2);
gardien.ajouterMemento(boiteTexte.creerMemento());
boiteTexte.setTexte("État 3");
boiteTexte.setCurseur(3);
gardien.ajouterMemento(boiteTexte.creerMemento());
gardien.precedent();
boiteTexte.setTexte("État 4");
boiteTexte.setCurseur(4);
gardien.ajouterMemento(boiteTexte.creerMemento());
boiteTexte.setTexte("État 5");
boiteTexte.setCurseur(5);
gardien.ajouterMemento(boiteTexte.creerMemento());
gardien.precedent();
boiteTexte.setMemento(gardien.getMemento());
System.out.println("Texte: " + boiteTexte.getTexte() +
" Curseur: " + boiteTexte.getCurseur());
}
}
Eiffel:
class
BOITE_TEXTE
create
make
feature {NONE} --Initialisation
make
do
texte := ""
curseur := 0
end
feature -- Accès
creer_memento:TEXTE_MEMENTO
do
create Result.make(texte.twin, curseur)
end
set_memento(a_memento:TEXTE_MEMENTO)
do
set_texte(a_memento.texte)
set_curseur(a_memento.curseur)
end
texte:STRING assign set_texte
set_texte(a_texte:STRING)
do
texte := a_texte
end
curseur:INTEGER assign set_curseur
set_curseur(a_curseur:INTEGER)
do
curseur := a_curseur
end
end
class
TEXTE_MEMENTO
create {BOITE_TEXTE}
make
feature {NONE} --Initialisation
make(a_texte:STRING; a_curseur:INTEGER)
do
texte := a_texte
curseur := a_curseur
end
feature {BOITE_TEXTE} -- Implementation
texte:STRING
curseur:INTEGER
end
class
GARDIEN
create
make
feature {NONE} -- Initialisation
make
do
create {LINKED_LIST[TEXTE_MEMENTO]}etats.make
index := 0
end
feature -- Accès
etats:LIST[TEXTE_MEMENTO]
index:INTEGER
ajouter_memento(a_memento:TEXTE_MEMENTO)
do
from
until
index >= etats.count
loop
etats.finish
etats.remove
end
etats.extend (a_memento)
index := index + 1
end
memento:TEXTE_MEMENTO
require
Index_Valid: index >= 1 and index <= etats.count
do
Result := etats.at (index)
end
precedent
require
Index_Valid: index > 1
do
index := index - 1
end
suivant
require
Index_Valid: index < etats.count
do
index := index + 1
end
end
class
APPLICATION
create
make
feature {NONE} -- Initialization
make
-- Initialisation de `Current'
local
l_boite_texte:BOITE_TEXTE
l_gardien:GARDIEN
do
create l_boite_texte.make
create l_gardien.make
l_boite_texte.set_texte ("Etat 1")
l_boite_texte.set_curseur (1)
l_gardien.ajouter_memento (l_boite_texte.creer_memento)
l_boite_texte.set_texte ("Etat 2")
l_boite_texte.set_curseur (2)
l_gardien.ajouter_memento (l_boite_texte.creer_memento)
l_boite_texte.set_texte ("Etat 3")
l_boite_texte.set_curseur (3)
l_gardien.ajouter_memento (l_boite_texte.creer_memento)
l_gardien.precedent
l_boite_texte.set_texte ("Etat 4")
l_boite_texte.set_curseur (4)
l_gardien.ajouter_memento (l_boite_texte.creer_memento)
l_boite_texte.set_texte ("Etat 5")
l_boite_texte.set_curseur (5)
l_gardien.ajouter_memento (l_boite_texte.creer_memento)
l_gardien.precedent
l_boite_texte.set_memento (l_gardien.memento)
print("Texte: " + l_boite_texte.texte + " Curseur: " +
l_boite_texte.curseur.out + "%N")
end
end
Dans les exemples précédents, le résultat est:
Texte: État 4 Curseur: 4
Observeur
Le patron observeur (ou « observer« ) permet à plusieurs objets de type « observeur » d’observer un objet de type « observable« . Dès que l’état de l’objet observable change, les objets de type « observeur » en sont immédiatement informés. De plus, il n’est pas nécessaire que les objets de type « observable » aient de dépendance envers les objets de type « observeur ». En d’autre mots, l’objet de type « observable » n’a pas à « connaitre » les « observeur » pour que le patron fonctionne. Par exemple:
Java:
import java.util.List;
import java.util.LinkedList;
public abstract class Observable {
private List<Observeur> observeurs;
public Observable() {
observeurs = new LinkedList<Observeur>();
}
public void attache(Observeur aObserveur) {
observeurs.add(aObserveur);
}
public void detache(Observeur aObserveur) {
observeurs.remove(aObserveur);
}
protected void informe() {
for(Observeur laObserveur : observeurs) {
laObserveur.actualisation(this);
}
}
}
public interface Observeur {
public void actualisation(Observable observable);
}
public class Produit extends Observable {
public Produit() {
super();
nom = "";
}
private String nom;
public String getNom() {
return nom;
}
public void setNom(String aNom) {
nom = aNom;
informe();
}
private int prix;
public int getPrix() {
return prix;
}
public void setPrix(int aPrix) {
prix = aPrix;
informe();
}
}
public class AfficheurProduit implements Observeur {
public AfficheurProduit() {
texte = "";
}
private String texte;
public String getTexte() {
return texte;
}
public void actualisation(Observable aObservable) {
Produit lProduit = (Produit)aObservable;
texte = "Le produit " + lProduit.getNom() + " au cout de " + lProduit.getPrix();
}
}
public class Programme {
static public void main(String[] args) {
AfficheurProduit afficheur = new AfficheurProduit();
Produit produit = new Produit();
produit.attache(afficheur);
produit.setNom("Ordinateur");
produit.setPrix(200);
System.out.println(afficheur.getTexte());
}
}
Il est important de noter que la mécanique Observeur est déjà dans les librairies Java. Par contre, considérant que Java ne gère pas l’héritage multiple, il est parfois nécessaire de coder la mécanique de l’observable à dans la classe qui doit être observable. Voir: https://docs.oracle.com/javase/7/docs/api/java/util/Observer.html
Eiffel:
deferred class
OBSERVABLE
feature {NONE} -- Initialisation
make
do
create {LINKED_LIST[OBSERVEUR]} observeurs.make
end
feature -- Accès
attache(a_observeur:OBSERVEUR)
do
observeurs.extend (a_observeur)
end
detache(a_observeur:OBSERVEUR)
do
observeurs.prune_all (a_observeur)
end
informe
do
observeurs.do_all (
agent {OBSERVEUR}.actualisation(Current))
end
feature {NONE} -- Implémentaition
observeurs:LIST[OBSERVEUR]
end
deferred class
OBSERVEUR
feature {OBSERVABLE} -- Implémentation
actualisation(a_observable:OBSERVABLE)
deferred
end
end
Grâce à la mécanique de classe ami d’Eiffel. On voit qu’on peut retreindre les appels de la méthode « actualisation » aux classes « OBSERVABLE » seulement. Celà permet d’éviter que cette méthode soit utilisé à l’extérieur du contexte du patron « Observeur ».
class
PRODUIT
inherit
OBSERVABLE
rename
make as make_observable
end
create
make
feature {NONE} -- Initialisation
make
do
make_observable
nom := ""
end
feature -- Accès
nom:STRING assign set_nom
set_nom(a_nom:STRING)
do
nom := a_nom
informe
end
prix:INTEGER assign set_prix
set_prix(a_prix:INTEGER)
do
prix := a_prix
informe
end
end
class
AFFICHEUR_PRODUIT
inherit
OBSERVEUR
create
make
feature {NONE} -- Initialisation
make
do
texte := ""
end
feature -- Accès
texte:STRING
feature {PRODUIT} -- Implémentation
actualisation(a_produit:PRODUIT)
do
texte := "Le produit " + a_produit.nom + " au prix de " + a_produit.prix.out
end
end
Ici, on peut voir que lors d’une redéfinition en Eiffel, on peut modifier le type d’un argument, en autant que le nouvel argument soit de type descendant du type d’origine.
class
APPLICATION
create
make
feature {NONE} -- Initialisation
make
-- Exécute l'application.
local
l_produit:PRODUIT
l_afficheur:AFFICHEUR_PRODUIT
do
create l_produit.make
create l_afficheur.make
l_produit.attache (l_afficheur)
l_produit.nom := "Ordinateur"
l_produit.prix := 200
print(l_afficheur.texte + "%N")
end
end
Visiteur
Le patron visiteur (ou « visitor« )permet d’ajouter un algorithme qui s’applique à une classe en particulier, mais sans avoir à modifier les méthodes de la classe. Il est généralement préférable de mettre la méthode dans la classe en tant que telle; mais il peut arriver que ce type de patron soit avantageux. Par exemple, pour garder la classe simple d’utilisation; ou bien dans le cas ou il est impossible de mettre une méthode dans la classe (par exemple, dans une interface en langage objet avec héritage simple seulement).
Java:
public interface ElementComptable {
public void accepte(ElementComptableVisiteur visiteur);
}
public class Produit implements ElementComptable {
private String nom;
private int prix;
public Produit() {
nom = "";
}
public void setNom(String aNom) {
nom = aNom;
}
public String getNom() {
return nom;
}
public void setPrix(int aPrix) {
prix = aPrix;
}
public int getPrix() {
return prix;
}
public void accepte(ElementComptableVisiteur visiteur) {
visiteur.visiteProduit(this);
}
}
public class Client implements ElementComptable {
private String nom;
private String numeroTelephone;
public Client() {
nom = "";
numeroTelephone = "";
}
public void setNom(String aNom) {
nom = aNom;
}
public String getNom() {
return nom;
}
public void setNumeroTelephone(String aNumeroTelephone) {
numeroTelephone = aNumeroTelephone;
}
public String getNumeroTelephone() {
return numeroTelephone;
}
public void accepte(ElementComptableVisiteur visiteur) {
visiteur.visiteClient(this);
}
}
public class Facture implements ElementComptable {
private int total;
private int numero;
public void setTotal(int aTotal) {
total = aTotal;
}
public int getTotal() {
return total;
}
public void setNumero(int aNumero) {
numero = aNumero;
}
public int getNumero() {
return numero;
}
public void accepte(ElementComptableVisiteur visiteur) {
visiteur.visiteFacture(this);
}
}
public interface ElementComptableVisiteur {
public void visiteProduit(Produit aProduit);
public void visiteClient(Client aClient);
public void visiteFacture(Facture aFacture);
}
public class Afficheur implements ElementComptableVisiteur {
public void visiteProduit(Produit aProduit) {
System.out.println("Le produit " + aProduit.getNom() +
" au prix de " + aProduit.getPrix() + "$");
}
public void visiteClient(Client aClient) {
System.out.println("Le client appelé " + aClient.getNom() +
" qui peut être rejoint au " + aClient.getNumeroTelephone());
}
public void visiteFacture(Facture aFacture) {
System.out.println("La facture numéro " + aFacture.getNumero() +
" totalise " + aFacture.getTotal() + "$");
}
}
public class Programme {
static public void main(String[] args) {
Produit produit = new Produit();
Client client = new Client();
Facture facture = new Facture();
Afficheur afficheur = new Afficheur();
produit.setNom("Ordinateur");
produit.setPrix(200);
client.setNom("Louis M");
client.setNumeroTelephone("555-123-4567");
facture.setNumero(1);
facture.setTotal(1000);
produit.accepte(afficheur);
client.accepte(afficheur);
facture.accepte(afficheur);
}
}
Eiffel:
deferred class
ELEMENT_COMPTABLE
feature -- Accès
accepte(a_visiteur:ELEMENT_COMPTABLE_VISITEUR)
deferred
end
end
class
PRODUIT
inherit
ELEMENT_COMPTABLE
create
make
feature {NONE} -- Initialisation
make
do
nom := ""
end
feature -- Accès
nom:STRING assign set_nom
set_nom(a_nom:STRING)
do
nom := a_nom
end
prix:INTEGER assign set_prix
set_prix(a_prix:INTEGER)
do
prix := a_prix
end
accepte(a_visiteur:ELEMENT_COMPTABLE_VISITEUR)
do
a_visiteur.visite_produit (Current)
end
end
class
CLIENT
inherit
ELEMENT_COMPTABLE
create
make
feature {NONE} -- Initialisation
make
do
nom := ""
numero_telephone := ""
end
feature -- Accès
nom:STRING assign set_nom
set_nom(a_nom:STRING)
do
nom := a_nom
end
numero_telephone:STRING assign set_numero_telephone
set_numero_telephone(a_numero_telephone:STRING)
do
numero_telephone := a_numero_telephone
end
accepte(a_visiteur:ELEMENT_COMPTABLE_VISITEUR)
do
a_visiteur.visite_client (Current)
end
end
class
FACTURE
inherit
ELEMENT_COMPTABLE
feature -- Accès
numero:INTEGER assign set_numero
set_numero(a_numero:INTEGER)
do
numero := a_numero
end
total:INTEGER assign set_total
set_total(a_total:INTEGER)
do
total := a_total
end
accepte(a_visiteur:ELEMENT_COMPTABLE_VISITEUR)
do
a_visiteur.visite_facture (Current)
end
end
deferred class
ELEMENT_COMPTABLE_VISITEUR
feature -- Accès
visite_produit(a_produit:PRODUIT)
deferred
end
visite_client(a_client:CLIENT)
deferred
end
visite_facture(a_facture:FACTURE)
deferred
end
end
class
AFFICHEUR
inherit
ELEMENT_COMPTABLE_VISITEUR
feature -- Accès
visite_produit(a_produit:PRODUIT)
do
print("Le produit " + a_produit.nom +
" au prix de " + a_produit.prix.out + "$%N")
end
visite_client(a_client:CLIENT)
do
print("Le client appelé " + a_client.nom +
" qui peut être rejoint au " + a_client.numero_telephone + "%N")
end
visite_facture(a_facture:FACTURE)
do
print("La facture numéro " + a_facture.numero.out +
" totalise " + a_facture.total.out + "$%N")
end
end
class
APPLICATION
create
make
feature {NONE} -- Initialisation
make
-- Exécute l'application.
local
produit:PRODUIT
client:CLIENT
facture:FACTURE
afficheur:AFFICHEUR
do
create produit.make
create client.make
create facture
create afficheur
produit.nom := "Ordinateur"
produit.prix := 200
client.nom := "Louis M"
client.numero_telephone := "555-123-4567"
facture.numero := 1
facture.total := 1000
produit.accepte (afficheur)
client.accepte (afficheur)
facture.accepte (afficheur)
end
end
Les codes précédents donnent le résultat:
Le produit Ordinateur au prix de 200$ Le client appelé Louis M qui peut être rejoint au 555-123-4567 La facture numéro 1 totalise 1000$
Auteur: Louis Marchand
Sauf pour les sections spécifiées autrement, ce travail est sous licence Creative Commons Attribution 4.0 International.