Comprendre les différents design patterns de construction

Jean-Michel Ormes

Comprendre les différents design patterns de construction fait partie d'une suite d'articles que j'ai écrits pour expliquer comment implémenter les 23 modèles de conception les plus connus. Dans cet article, nous allons nous concentrer sur le fonctionnement des design patterns liés à la construction d'objets, les deux autres familles feront le sujet d'un autre article. N'hésitez pas à laisser votre avis sur le contenu de l'article directement via le forum : Commentez

- Titre : Comprendre les différents design patterns de construction

- Auteur : Jean-Michel Ormes

- Parution : 4 avril 2011

- Mise à jour : 4 avril 2011

- Licence : Copyright ® 2010 Jean-Michel Ormes. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- 1. Introduction

- 2. Problématique générale

- 3. Les modèles de construction

- 3-1. Fabrique abstraite (Abstract Factory)

- 3-1-1. Définition

- 3-1-2. Diagramme de classe

- 3-1-3. Implémentation en C#

- 3-2. Monteur (Builder)

- 3-2-1. Définition

- 3-2-2. Diagramme de classe

- 3-2-3. Implémentation en C#

- 3-3. Fabrique (Factory Method)

- 3-3-1. Définition

- 3-3-2. Diagramme de classe

- 3-3-3. Implémentation en C#

- 3-4. Prototype (Prototype)

- 3-4-1. Définition

- 3-4-2. Diagramme de classe

- 3-4-3. Implémentation en C#

- 3-5. Singleton (Singleton)

- 3-5-1. Définition

- 3-5-2. Diagramme de classe

- 3-5-3. Implémentation en C#

- 4. Récapitulatif

- 5. Liens

- 6. Remerciements

1 - Introduction

Qu'est-ce qu'un design pattern (ou patron de conception) ? Il s'agit tout simplement d'un schéma qui forme une solution à un problème connu ou récurrent. Ce sont des solutions connues et dont la conception est due à l'expérience de programmeurs. Le concept de design patterns est né des travaux de quatre personnes (Erich Gamma, Richard Helm, Ralph Johnson, et John Vlissides plus communément appelés « Gang of Four ») dans leur ouvrage « Design Patterns : Elements of Reusable Object-Oriented Software ». De façon générale, on utilise un design pattern pour diminuer le temps nécessaire au développement d'une application et pour augmenter la qualité du résultat attendu à un traitement donné.

2 - Problématique générale

Afin de mieux illustrer la mise en œuvre des modèles de conception, nous allons prendre un exemple d'étude de cas. Vous êtes à la tête d'un géant constructeur d'ordinateurs et de pièces détachées. Il vous faut imaginer tout le processus, allant de la création des ordinateurs ou des pièces jusqu'à leur livraison.

3 - Les modèles de construction

Les patterns de construction déterminent comment faire l'instanciation et la configuration des classes et des objets. Ces patterns permettent également d'encapsuler les classes concrètes et de rendre indépendant la façon dont les objets sont créés.

3-1 - Fabrique abstraite (Abstract Factory)

3-1-1 - Définition

Le pattern Abstract Factory permet, à partir d'une interface, de créer une famille d'objets sans pour autant spécifier de classes concrètes. C'est de ce pattern dont nous aurons besoin pour créer nos différentes pièces pour ordinateurs fixes ou ordinateurs portables. Pour chaque type d'ordinateur ou de pièce, nous disposons d'une classe abstraite et de n sous-classes, qui définiront chacune un modèle spécifique. Un client qui passe une commande personnalisée en choisissant lui-même ses pièces est un exemple concret d'utilisation de ce pattern. Ça veut dire que chaque composant est une classe abstraite, et que chaque version du composant est une sous-classe.

3-1-2 - Diagramme de classe

Les diagrammes de classe suivants permettent de mieux comprendre comment fonctionne le pattern Abstract Factory :

Nous avons créé deux classes abstraites CarteMere et CarteGraphique qui contiennent chacune deux sous-classes concrètes (CarteMereFixe, CarteMerePortable, CarteGraphiqueFixe et CarteGraphiquePortable). Par souci de simplicité, je n'ai pas créé les autres classes d'objets appartenant à un ordinateur. Nous avons également créé une interface Ordinateur qui contient la signature des méthodes permettant de créer les différentes pièces pour un ordinateur fixe ou portable. Deux classes OrdinateurFixe et OrdinateurPortable implémentent cette interface.

3-1-3 - Implémentation en C#

Voici un exemple de code simplifié d'utilisation du pattern Abstract Factory. Tout d'abord les classes abstraites ainsi que leurs sous-classes concrètes :

CarteMere.cs

public abstract class CarteMere
{
protected String modele;
protected String chipset;

public CarteMere(String modele, String chipset)
{
this.modele = modele;
this.chipset = chipset;
}

public abstract void afficheDetail();
}

CarteMereFixe.cs

public class CarteMereFixe : CarteMere
{
protected String modele;
protected String chipset;

public CarteMereFixe(String modele, String chipset)
: base(modele, chipset)
{
this.modele = modele;
this.chipset = chipset;
}

public override void afficheDetail()
{
Console.WriteLine("Carte mere pour fixe cree : " + modele +
" , chipset integre :" + chipset);
}
}

CarteMerePortable.cs

public class CarteMerePortable : CarteMere
{
protected String modele;
protected String chipset;

public CarteMerePortable(String modele, String chipset)
: base(modele, chipset)
{
this.modele = modele;
this.chipset = chipset;
}

public override void afficheDetail()
{
Console.WriteLine("Carte mere pour portable cree : " + modele +
" , chipset integre :" + chipset);
}
}

CarteGraphique.cs

public abstract class CarteGraphique
{
protected String modele;
protected String memoire;
protected String chipset;

public CarteGraphique(String modele, String memoire,
String chipset)
{
this.modele = modele;
this.memoire = memoire;
this.chipset = chipset;
}

public abstract void afficheDetail();
}

CarteGraphiquePortable.cs

public class CarteGraphiquePortable : CarteGraphique
{
protected String modele;
protected String memoire;
protected String chipset;

public CarteGraphiquePortable(String modele, String memoire,
String chipset)
: base(modele, memoire, chipset)
{
this.modele = modele;
this.memoire = memoire;
this.chipset = chipset;
}

public override void afficheDetail()
{
Console.WriteLine("Carte graphique pour portable cree : " + modele +
" , " + memoire + " Mo de RAM, chipset integre :" + chipset);
}
}

CarteGraphiqueFixe.cs

public class CarteGraphiqueFixe : CarteGraphique
{
protected String modele;
protected String memoire;
protected String chipset;

public CarteGraphiqueFixe(String modele, String memoire,
String chipset)
: base(modele, memoire, chipset)
{
this.modele = modele;
this.memoire = memoire;
this.chipset = chipset;
}

public override void afficheDetail()
{
Console.WriteLine("Carte graphique pour fixe cree : " + modele +
" , " + memoire + " Mo de RAM, chipset integre :" + chipset);
}
}

Voici le code source de l'interface Ordinateur ainsi que ses deux classes d'implémentation OrdinateurFixe et OrdinateurPortable :

Ordinateur.cs

public interface Ordinateur
{
CarteMere creeCarteMere(String modele, String chipset);

CarteGraphique creeCarteGraphique(String modele, String memoire,
String chipset);
}

OrdinateurFixe.cs

public class OrdinateurFixe : Ordinateur
{
public CarteMere creeCarteMere(string modele, string chipset)
{
return new CarteMereFixe(modele , chipset);
}

public CarteGraphique creeCarteGraphique(string modele, string memoire, string chipset)
{
return new CarteGraphiqueFixe(modele, memoire, chipset);
}
}

OrdinateurPortable.cs

public class OrdinateurPortable : Ordinateur
{
public CarteMere creeCarteMere(string modele, string chipset)
{
return new CarteMerePortable(modele, chipset);
}

public CarteGraphique creeCarteGraphique(string modele, string memoire, string chipset)
{
return new CarteGraphiquePortable(modele, memoire, chipset);
}
}

Voici un petit programme en mode console permettant de tester le pattern Abstract Factory :

Program.cs

static void Main(string[] args)
{
Ordinateur ordinateur;

// Stock de cartes meres et cartes graphiques
CarteGraphique[] carteGraphique = new CarteGraphique[10];
CarteMere[] carteMere = new CarteMere[10];

// Un client arrive et passe commande d'un Ordinateur
Console.WriteLine("Voulez-vous commander un " +
"ordinateur fixe (1) ou portable (2)?");
String choix = Console.ReadLine();

if (choix.Equals("1"))
{
ordinateur = new OrdinateurFixe();
}
else
{
ordinateur = new OrdinateurPortable();
}

carteGraphique[0] = ordinateur.creeCarteGraphique("modele1", "512", "chipset32");
carteMere[0] = ordinateur.creeCarteMere("modele1", "chipset32");

Console.WriteLine("Votre demande a  ete traitee.");
carteGraphique[0].afficheDetail();
carteMere[0].afficheDetail();
Console.ReadKey();
}

Et le résultat que l'on obtient :

3-2 - Monteur (Builder)

3-2-1 - Définition

Le pattern Monteur permet de créer des objets complexes à partir d'autres objets. Concrètement, c'est comme si on assemblait plusieurs objets pour les "monter" et n'en faire qu'un. Exemple : un menu. L'objet menu se compose d'autres objets (sandwich, frites, boisson, etc.). Dans notre cas, nous allons nous servir de ce pattern pour monter différentes tours de PC.

3-2-2 - Diagramme de classe

Le diagramme de classe suivant permet de mieux comprendre comment fonctionne le pattern Builder :

Le Buildeur est la classe abstraite qui contient le produit. Le Director est celui qui construit un objet utilisant la méthode de conception Buildeur. ConcreteBuilderA et ConcreteBuilderB fournissent une implémentation du Buildeur. Ce sont ces classes qui construisent et assemblent les différentes parties de nos tours. ProductA et ProductB correspondent à nos tours en cours de construction.

3-2-3 - Implémentation en C#

Voici un exemple de code d'utilisation du pattern Builder :

Tour.cs

public class Tour
{
private String alimentation = "";
private String carteMere = "";
private String boitier = "";
private String disqueDur = "";
private String memoireRAM = "";
private String carteGraphique = "";
private String processeur = "";
private String carteSon = "";

public void setAlimentation(String alimentation) { this.alimentation = alimentation; }
public void setCarteMere(String carteMere) { this.carteMere = carteMere; }
public void setBoitier(String boitier) { this.boitier = boitier; }
public void setDisqueDur(String disqueDur) { this.disqueDur = disqueDur; }
public void setMemoireRAM(String memoireRAM) { this.memoireRAM = memoireRAM; }
public void setCarteGraphique(String carteGraphique) { this.carteGraphique = carteGraphique; }
public void setProcesseur(String processeur) { this.processeur = processeur; }
public void setCarteSon(String carteSon) { this.carteSon = carteSon; }

public void Informations()
{
Console.WriteLine("Tour creee : " + alimentation + ", " +
carteMere + ", " + boitier + ", " + disqueDur + ", " +
memoireRAM + ", " + carteGraphique + ", " + processeur + ", " +
carteSon);
}
}

MonteurTour.cs

public abstract class MonteurTour
{
protected Tour tour;

public Tour getTour() { return tour; }
public void creerNouvelleTour() { tour = new Tour(); }

public abstract void monterAlimentation();
public abstract void monterCarteMere();
public abstract void monterBoitier();
public abstract void monterDisqueDur();
public abstract void monterMemoireRAM();
public abstract void monterCarteGraphique();
public abstract void monterProcesseur();
public abstract void monterCarteSon();
}

MonteurTour1.cs

class MonteurTour1 : MonteurTour
{
public override void monterAlimentation() {tour.setAlimentation("Alimentation 1");}

public override void monterCarteMere() {tour.setCarteMere("Carte mere ATX");}

public override void monterBoitier() {tour.setBoitier("Boitier moyen");}

public override void monterDisqueDur() {tour.setDisqueDur("DD 7200 tours");}

public override void monterMemoireRAM() {tour.setMemoireRAM("1024Mo");}

public override void monterCarteGraphique() {tour.setCarteGraphique("ATI Radeon");}

public override void monterProcesseur() { tour.setProcesseur("Intel Dual Core 2 3.16 GHz"); }

public override void monterCarteSon() { tour.setCarteSon("Creative Sound Blaster X-Fi Titanium"); }
}

MonteurTour2.cs

class MonteurTour2 : MonteurTour
{
public override void monterAlimentation() { tour.setAlimentation("Alimentation 2"); }

public override void monterCarteMere() { tour.setCarteMere("Carte mere DTX"); }

public override void monterBoitier() { tour.setBoitier("Boitier moyen"); }

public override void monterDisqueDur() { tour.setDisqueDur("DD 5400 tours"); }

public override void monterMemoireRAM() { tour.setMemoireRAM("2048Mo"); }

public override void monterCarteGraphique() { tour.setCarteGraphique("Nvidia Geforce GT 430"); }

public override void monterProcesseur() { tour.setProcesseur("Intel Dual Core 2 3.16 GHz"); }

public override void monterCarteSon() { tour.setCarteSon("Creative Sound Blaster X-Fi Titanium"); }
}

Technicien.cs

class Technicien
{
private MonteurTour monteurTour;

public void setMonteurTour(MonteurTour m) { monteurTour = m; }
public Tour getTour() { return monteurTour.getTour(); }

public void construireTour()
{
monteurTour.creerNouvelleTour();
monteurTour.monterAlimentation();
monteurTour.monterCarteMere();
monteurTour.monterBoitier();
monteurTour.monterDisqueDur();
monteurTour.monterMemoireRAM();
monteurTour.monterCarteGraphique();
monteurTour.monterProcesseur();
monteurTour.monterCarteSon();
}
}

Program.cs

class Program
{
static void Main(string[] args)
{
Technicien technicien = new Technicien();
MonteurTour monteurTour1 = new MonteurTour1();
MonteurTour monteurTour2 = new MonteurTour2();

technicien.setMonteurTour(monteurTour1);
technicien.construireTour();

Tour tour = technicien.getTour();

tour.Informations();

Console.ReadKey();
}
}

Et le résultat que l'on obtient :

3-3 - Fabrique (Factory Method)

3-3-1 - Définition

Le pattern Fabrique permet l'instanciation d'objets non définis dans une classe concrète à partir d'une méthode d'instanciation issue d'une classe abstraite. Exemple : La création d'un disque dur, notamment le contrôleur de disque. Il existe plusieurs types de contrôleurs de disque (SATA, IDE, SCSI, SAS). Le disque dur correspond donc à la classe abstraite et les contrôleurs aux sous-classes concrètes.

3-3-2 - Diagramme de classe

Le diagramme de classe suivant permet de mieux comprendre comment fonctionne le pattern Factory :

Le Créateur est une classe abstraite qui contient les méthodes qui vont manipuler le produit. Les Créateurs Concrets permettent de créer des produits concrets. Le Produit définit des méthodes qui permettent d'attribuer le nombre de tours, tester le bon fonctionnement du disque et de finaliser le disque. Dans notre exemple, Le Produit est la classe DisqueDur. Le Produit Concret est une classe qui correspond à un disque dur spécifique. Cette classe hérite de la classe Produit. Dans notre exemple, les produits concrets sont les classes DisqueDurATA et DisqueDurSCSI.

3-3-3 - Implémentation en C#

Voici un exemple de code d'utilisation du pattern Factory :

DisqueDur.cs

public abstract class DisqueDur
{
public abstract void setNbTours(String nombre);

public abstract void Tester();

public abstract void Finaliser();
}

FabriqueDisqueDur.cs

public abstract class FabriqueDisqueDur
{
public DisqueDur creerDisqueDur(String controleur)
{
DisqueDur disqueDur;

disqueDur = setControleur(controleur);
disqueDur.setNbTours("7200");
disqueDur.Tester();
disqueDur.Finaliser();

return disqueDur;
}

protected abstract DisqueDur setControleur(String typeControleur);
}

FabriqueDisqueDurATA.cs

public class FabriqueDisqueDurATA : FabriqueDisqueDur
{
override protected DisqueDur setControleur(String controleur)
{
DisqueDur disqueDurATA = null;
if (controleur == "ATA")
disqueDurATA = new DisqueDurATA();

return disqueDurATA;
}
}

FabriqueDisqueDurSCSI.cs

public class FabriqueDisqueDurSCSI : FabriqueDisqueDur
{
override protected DisqueDur setControleur(String controleur)
{
DisqueDur disqueDurSCSI = null;
if (controleur == "SCSI")
disqueDurSCSI = new DisqueDurSCSI();

return disqueDurSCSI;
}
}

DisqueDurSCSI.cs

public class DisqueDurSCSI: DisqueDur
{
public DisqueDurSCSI()
{
Console.WriteLine("Disque Dur SCSI cree");
}

public override void setNbTours(String nombre)
{
Console.WriteLine("Nb tours : " + nombre + " tours");
}

public override void Tester()
{
Console.WriteLine("Tests en cours..");
}

public override void Finaliser()
{
Console.WriteLine("Disque dur SCSI operationnel");
}
}

DisqueDurATA.cs

public class DisqueDurATA : DisqueDur
{
public DisqueDurATA()
{
Console.WriteLine("Disque Dur ATA cree");
}

public override void setNbTours(String nombre)
{
Console.WriteLine("Nb tours : " + nombre + " tours");
}

public override void Tester()
{
Console.WriteLine("Tests en cours..");
}

public override void Finaliser()
{
Console.WriteLine("Disque dur ATA operationnel");
}
}

Program.cs

class Program
{
static void Main(string[] args)
{
FabriqueDisqueDur disqueDurATA = new FabriqueDisqueDurATA();
disqueDurATA.creerDisqueDur("ATA");

FabriqueDisqueDur disqueDurSCSI = new FabriqueDisqueDurSCSI();
disqueDurSCSI.creerDisqueDur("SCSI");

Console.ReadKey();
}
}

Et le résultat que l'on obtient :

3-4 - Prototype (Prototype)

3-4-1 - Définition

Le pattern Prototype permet de créer de nouveaux objets à partir d'objets existants. Ces objets sont appelés prototypes et possèdent une capacité de duplication. Dans le cadre de notre super méga géant constructeur d'ordinateurs et de pièces détachées, ce design pattern permettrait de créer toute la panoplie de papiers (bon de commande, facture, notice d'utilisation, etc.).

3-4-2 - Diagramme de classe

Le diagramme de classe suivant permet de mieux comprendre comment fonctionne le pattern Prototype :

La classe Prototype est le squelette principal qui permet de créer les nouvelles copies. Les classes BonCommande, Facture et Notice sont les sous-classes spéficiques ayant leurs propres attributs. La méthode Clone() retourne une copie de l'objet.

3-4-3 - Implémentation en C#

Voici un exemple de code d'utilisation du pattern Prototype :

Prototype.cs

public abstract class Prototype
{
public abstract Prototype clone();

public abstract void Informations();
}

Notice.cs

public class Notice : Prototype
{
public override Prototype clone()
{
return (Prototype)this.MemberwiseClone();
}

public override void Informations()
{
Console.WriteLine("Notice d'utilisation generee");
}
}

Facture.cs

public class Facture : Prototype
{
String numFacture = "";
String client = "";

public Facture(String Client)
{
this.client = Client;
}

public override Prototype clone()
{
return (Prototype)this.MemberwiseClone();
}

public override void Informations()
{
Console.WriteLine("Facture nÂ°{0} generee pour {1}",numFacture,client);
}

public void SetNumFacture(String Facture)
{
this.numFacture = Facture;
}
}

BonCommande.cs

public class BonCommande : Prototype
{
String numCommande = "";
String client = "";

public BonCommande(String Client)
{
this.client = Client;
}

public override Prototype clone()
{
return (Prototype)this.MemberwiseClone();
}

public override void Informations()
{
Console.WriteLine("Bon de commande nÂ°{0} genere pour {1}", numCommande, client);
}

public void SetNumCommande(String numCommande)
{
this.numCommande = numCommande;
}
}

Main.cs

class Program
{
static void Main(string[] args)
{
String numFacture1 = "F.2011.001";
String numFacture2 = "F.2011.002";

String Client1 = "Jean-Michel";
String Client2 = "Philippe";

Facture f1 = new Facture(Client1);
f1.SetNumFacture(numFacture1);
Console.WriteLine("1ere sortie :  ");
f1.Informations();

Facture f2 = (Facture)f1.clone();
Console.WriteLine("2eme sortie :  ");
f2.Informations(); // Avant modification du numero de facture
f2.SetNumFacture(numFacture2);
Console.WriteLine("3eme sortie :  ");
f2.Informations(); // Apres modification

Console.ReadKey();
}
}

Et le résultat que l'on obtient :

Initialement, on crée une facture ayant pour numéro "F.2011.001", pour le client Jean-Michel. On clone ensuite cette facture puis on vérifie que le résultat est bien celui attendu, à savoir que le client est toujours Jean-Michel et que le numéro de facture est inchangé. On modifie ensuite le numéro de facture.

3-5 - Singleton (Singleton)

3-5-1 - Définition

Le pattern Singleton permet de s'assurer qu'une classe ne possède qu'une seule instance. Ce pattern sera utile par exemple quand on effectuera nos tests de démarrage sur un disque dur. En effet, celui-ci ne devra pas être démarré deux fois.

3-5-2 - Diagramme de classe

Le diagramme de classe suivant permet de mieux comprendre comment fonctionne le pattern Singleton :

Le plus important dans la classe Singleton est la méthode Instance(). Celle-ci est implémentée de façon à ne créer qu'une seule instance. Par ailleurs, pour vérifier qu'une instance est déjà créée, on utilise un attribut privé appelé _instance.

3-5-3 - Implémentation en C#

Voici un exemple de code d'utilisation du pattern Singleton :

TestDisqueDur.cs

public class TestDisqueDur
{
public DateTime dt;

private static TestDisqueDur _instance = null;

private TestDisqueDur() { }

public static TestDisqueDur Instance()
{
if (_instance == null)
{
_instance = new TestDisqueDur();
Console.WriteLine("Démarrage du disque....");
}

return _instance;
}

public void afficheDetail()
{
Console.WriteLine("Disque dur démarre.");
}
}

Voici un programme permettant de tester le pattern Singleton :

Program.cs

class Program
{
static void Main(string[] args)
{
//Instanciation du disque
TestDisqueDur disque = TestDisqueDur.Instance();
disque.afficheDetail();

AutreDisque();
Console.ReadKey();
}

public static void AutreDisque()
{
//On essaie d'instancier un nouveau disque
TestDisqueDur disque = TestDisqueDur.Instance();
disque.afficheDetail();
}
}

Et le résultat que l'on obtient :

Initialement, on crée un disque dur de test puis on appelle la méthode afficheDetail() de ce disque. Comme prévu, on a bien le démarrage qui s'effectue. On appelle ensuite la méthode AutreDisque(). Dans cette même méthode, on essaye d'instancier un nouveau disque dur de test et on appelle la méthode afficheDetail(). Vu que l'attribut _instance n'est pas null, la méthode Instance() ne créera pas de nouveau disque dur de test et renverra l'instance déjà créée.

4 - Récapitulatif

Nous venons de passer en revue les différents patterns de construction. Voici un récapitulatif des cas où il faut implémenter ces patterns :

- On utilise le pattern Fabrique Abstraite :

- Quand on doit créer une famille d'objets qui se ressemblent.

- Quand les objets d'une famille peuvent évoluer.

- On utilise le pattern Monteur :

- Quand on doit créer un objet complexe qui nécessite plusieurs étapes.

- On utilise le pattern Fabrique :

- Quand on doit créer un objet concret en fonction d'un paramètre donné.

- Quand on a besoin de faire évoluer ou ajouter des classes concretes sans modifier la classe abstraite.

- On utilise le pattern Prototype :

- Quand on doit créer de nouveaux objets à partir d'objets existants soit par copie, soit par clonage.

- Quand la création d'un objet est plus coûteuse que de le copier.

- On utilise le pattern Singleton :

- Quand un objet ne doit posséder qu'une seule et unique instance sous peine de générer une erreur.

- Quand un objet créé deux fois est susceptible de générer une erreur.

5 - Liens

Je vous invite à lire également ces liens :

Le pattern adaptateur par Florian CASABLANCA
Le pattern Singleton par Ronald VASSEUR
Les critiques de livres

6 - Remerciements

Je tiens à remercier ClaudeLELOUP pour sa relecture orthographique ainsi que Philippe Vialatte et 3DArchi pour leurs remarques techniques.