Lagrida

Les patrons de conception (design patterns)

site lagrida Informatique Informatique Théorique Les patrons de conception (design patterns)
Un patron de conception (design pattern) est une solution générique d'implémentation répondant à un problème spécifique et cette solution doit avoir été validée sur plusieurs projets.
Un patron de conception donc est la meilleure solution connue pour un problème de conception récurrent.
Les patrons de conception ont été formellement reconnus en 1994 à la suite de la parution du livre Design Patterns: Elements of Reusable Software, co-écrit par quatre auteurs : Gamma, Helm, Johnson et Vlissides (Gang of Four - GoF).

Introduction

Il existe trois familles de patrons de conception selon leur utilisation :
  • Créateurs (creational): ils définissent comment faire l'instanciation et la configuration des classes et des objets.
  • Structuraux (structural): ils définissent comment organiser les classes d'un programme dans une structure plus large (séparant l'interface de l'implémentation).
  • Comportementaux (behavioral): ils définissent comment organiser les objets pour que ceux-ci collaborent (distribution des responsabilités) et expliquent le fonctionnement des algorithmes impliqués.
Design Patterns
Creational:Singleton, Factory, Abstract Factory, Builder, Prototype, ...
Structural:Adapter, Composite, Decorater, Facade, Proxy, ...
Behavioral:Observer, Strategy, State, Command, Iterator, ...

Singleton pattern

Présentation


Catégorie: creational
Objective:
Garantir q'une classe aura une seule instance et fournir un accès global a cette instance.
Structure:
Singleton pattern
Singleton pattern

Implémentation en Java

Exemple: Implémenter une classe Connection en utilisant le design pattern Singleton avec un conteur pour compter le nombre d'instanciations des objets de type Connection. Créer les objets de type Connection dans une classe Client.
La classe Connection:
CODE:
public class Connection {
	private static Connection instance;
	private static int counter = 0;
	// La visibilité du constructeur est privé pour intérdire
	// une instanciation directe en dehors la classe Connection
	private Connection() {
		counter++;
	}
	public static Connection getInstance() {
		if(instance == null) {
			instance = new Connection();
		}
		return instance;
	}
	public void description() {
		System.out.println("Connection........");
		System.out.println("Nombre d'objets de type Connection instanciés : "+counter);
	}
}

La classe Client:
CODE:
public class Client {
	public static void main(String[] args) {
		Connection cnx1 = Connection.getInstance();
		Connection cnx2 = Connection.getInstance();
		
		cnx1.description();
		cnx2.description();
	}
}

Résultat:
CODE:
Connection........
Nombre d'objets de type Connection instanciés : 1
Connection........
Nombre d'objets de type Connection instanciés : 1

Donc un seul Objet de type Connection qui est instancié.

Problème avec le multithreading

L'implémentation précendante de la classe Connection peut ne pas marcher si on travaille avec le multithreading.
Exemple:
La classe ThreadConnection:
CODE:
public class ThreadConnection {
	private static ThreadConnection instance;
	private static int counter = 0;
	private ThreadConnection() {
		counter++;
	}
	public static ThreadConnection getInstance() {
		if(instance == null) {
			//Thread.currentThread();
			try {
				Thread.sleep(500);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			instance = new ThreadConnection();
		}
		return instance;
	}
	public void description() {
		System.out.println("Connection........");
		System.out.println("Nombre d'objets de type Connection instanciés : "+counter);
	}
}

La classe ImpThread:
CODE:
public class ImpThread extends Thread{
	@Override
	public void run() {
		ThreadConnection.getInstance().description();
	}
}

La classe Client:
CODE:
public class Client {
	public static void main(String[] args) {
		ImpThread t1 = new ImpThread();
		ImpThread t2 = new ImpThread();
		ImpThread t3 = new ImpThread();
		
		t1.start();
		t2.start();
		t3.start();
	}
}

Résultat:
CODE:
Connection........
Nombre d'objets de type Connection instanciés : 1
Connection........
Nombre d'objets de type Connection instanciés : 2
Connection........
Nombre d'objets de type Connection instanciés : 3

Explication:
Les Thread t1 et t2 et t3 s'éxecutent en même temps, donc quand le premier thread entre dans le bloc if(instance == null) { ... } il va attendre 500ms avant qu'il arrive à instanciation de instance. L'autre thread va entrer aussi dans le bloc if(instance == null) { ... } puisque la valeur de instance est null et par la suite instancier 3 différent objets.
Pour gérer l'accès concurrent il faut ajouter le mot clé synchronized à la fonction getInstance() dans la classe ThreadConnection pour s'assurer qu'un seul thread peut utiliser cette fonction à la fois:
CODE:
public class ThreadConnection {
	private static ThreadConnection instance;
	private static int counter = 0;
	private ThreadConnection() {
		counter++;
	}
	public static synchronized ThreadConnection getInstance() {
		if(instance == null) {
			//Thread.currentThread();
			try {
				Thread.sleep(500);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			instance = new ThreadConnection();
		}
		return instance;
	}
	public void description() {
		System.out.println("Connection........");
		System.out.println("Nombre d'objets de type Connection instanciés : "+counter);
	}
}

Résultat:
CODE:
Connection........
Connection........
Connection........
Nombre d'objets de type Connection instanciés : 1
Nombre d'objets de type Connection instanciés : 1
Nombre d'objets de type Connection instanciés : 1

Normalement on a besoin seulement de garentir que l'accès au bloc if(instance == null) { ... } se réalisera par un seul thread à la fois. Donc pour ne pas ralentir l'accès à la fonction getInstance() toute entière il vaut mieux gérer les concurrences à l'accès juste pour le bloc if(instance == null) { ... } :
CODE:
public class ThreadConnection {
	private static ThreadConnection instance;
	private static int counter = 0;
	private ThreadConnection() {
		counter++;
	}
	public static ThreadConnection getInstance() {
		// Ce n'est pas une redondance ;)
		if(instance == null) {
			synchronized(ThreadConnection.class) {
				if(instance == null) {
					//Thread.currentThread();
					try {
						Thread.sleep(500);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
					instance = new ThreadConnection();
				}
			}
		}
		return instance;
	}
	public void description() {
		System.out.println("Connection........");
		System.out.println("Nombre d'objets de type Connection instanciés : "+counter);
	}
}

Adapter pattern

Présentation


Catégorie: Structural
Objective:
Convertir l'interface d'une classe en une autre interface que le client attend.
Structure:
Adapter
Adapter pattern

Implémentation en Java

Exemple:

Adapter
adapter

L'interface Vehicule
CODE:
public interface Vehicule {
	public void accelerate();
	public void sound();
}

La classe Car
CODE:
public class Car implements Vehicule{
	@Override
	public void accelerate() {
		System.out.println("Speeeed");
	}
	@Override
	public void sound() {
		System.out.println("Bip Bip");
	}
}

La classe Bycicle
CODE:
public class Bycicle {
	public void pedal() {
		System.out.println("pedal");
	}
	public void horn() {
		System.out.println("rin rin");
	}
}

La classe BycicleAdapter
CODE:
public class BycicleAdapter implements Vehicule{
	private Bycicle bycicle;
	
	public BycicleAdapter(Bycicle bycicle) {
		this.bycicle = bycicle;
	}
	@Override
	public void accelerate() {
		bycicle.pedal();
	}
	@Override
	public void sound() {
		bycicle.horn();
	}
}

La classe Client
CODE:
public class Client {
	public static void main(String[] args) {
		Vehicule car = new Car();
		car.accelerate();
		car.sound();
		
		Bycicle b = new Bycicle();
		Vehicule bycicle= new BycicleAdapter(b);
		bycicle.accelerate();
		bycicle.sound();
	}
}

Explication: On a adapté la classe Bycicle par la classe BycicleAdapter de telle sorte le Client travaille avec l'interface Vehicule et par la suite les méthodes qu'il expecte (accelerate() et sound()).

Composite pattern

Présentation


Catégorie: structural
Objective:
Composer plusieurs objets dans une structure d'arbre pour représenter une hierarchie en traitant les objets feuilles et composés de la meme façon.
Structure:
Composite pattern
Composite pattern

Implémentation en Java

Exemple: Donnez une solution pour un système qui a des dossiers, et chaque dossier peut contenir des fichiers ou des dossiers.
Exemple de Composite pattern
Exemple de Composite pattern

La classe Component
CODE:
public abstract class Component {
	private String type;
	private String name;
	public void add(Component component) {
	}
	public void remove(Component component) {
	}
	public Component getChild(int k) {
		return null;
	}
	public String getType() {
		return type;
	}
	public void setType(String type) {
		this.type = type;
	}
	public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	}
	public abstract void description();
}

La classe PdfFile
CODE:
public class PdfFile extends Component{
	public PdfFile(String name){
		super.setType("pdf");
		super.setName(name);
	}
	@Override
	public void description() {
		System.out.println("type : "+super.getType()+", name : "+super.getName());
	}
}

La classe TxtFile
CODE:
public class TxtFile extends Component{
	public TxtFile(String name) {
		super.setType("txt");
		super.setName(name);
	}
	@Override
	public void description() {
		System.out.println("type : "+super.getType()+", name : "+super.getName());
	}
}

La classe Folder
CODE:
public class Folder extends Component{
	private List<Component> components;
	public Folder(String name) {
		super.setType("folder");
		super.setName(name);
		this.components = new ArrayList<>();
	}
	@Override
	public void add(Component component) {
		this.components.add(component);
	}
	@Override
	public void remove(Component component) {
		this.components.remove(component);
	}
	@Override
	public Component getChild(int k) {
		return this.components.get(k);
	}
	@Override
	public void description() {
		System.out.println("type : "+super.getType()+", name : "+super.getName());
		// Afficher les noms de tous les composants du dossier
		System.out.print("Les composants : ");
		for(Component c: components) {
			System.out.print(c.getName()+", ");
		}
		System.out.println();
	}
}

La classe Client
CODE:
public class Client {
	public static void main(String[] args) {
		Folder folder1 = new Folder("folder1");
		
		folder1.add(new TxtFile("txtFile1"));
		folder1.add(new TxtFile("txtFile2"));
		folder1.add(new PdfFile("pdfFile1"));
		
		folder1.getChild(0).description();
		System.out.println("-----------------------------");
		folder1.description();
		System.out.println("-----------------------------");
		Folder folder2 = new Folder("folder2");
		folder2.add(new PdfFile("pdfFile2"));
		folder1.description();
		folder1.add(folder2);
		System.out.println("-----------------------------");
		folder1.getChild(3).add(new TxtFile("txtFile3"));
		folder2.description();
	}
}

Résultat:
CODE:
type : txt, name : txtFile1
-----------------------------
type : folder, name : folder1
Les composants : txtFile1, txtFile2, pdfFile1, 
-----------------------------
type : folder, name : folder1
Les composants : txtFile1, txtFile2, pdfFile1, 
-----------------------------
type : folder, name : folder2
Les composants : pdfFile2, txtFile3,

Strategy pattern

Présentation


Catégorie: behavioral
Objective:
Définir une famille d'algorithmes, et *encapsuler chacun et les rendre **interchangeables tout en assurant que chaque algorithme puisse évoluer indépendamment des clients qui l'utilisent.
* Encapsuler un lgorithme c'est de le mettre dans une classe (une seule méthode qui implémente cet algorithme).
** Rendre les algorithmes interchangeables c'est pouvoir changer l'algorithme au cours d'execution.
La fin du définition ségnifie qu'on puisse toujours ajouter des algorithmes indépendamment des clients.
Structure:
Strategy pattern
Strategy pattern

Implémentation en Java

Exemple: On considère les algorithmes de tri suivant : tri par insertion, Tri paŕ selection, Le tri rapide.
Pour trier un tableau, on passe par plusieurs algorithmes de tri :
Exemple de Strategy pattern
Exemple de Strategy pattern

L'interface SortStrategy
CODE:
public interface SortStrategy {
	public void algorithm();
}

La classe InsertionSort
CODE:
public class InsertionSort implements SortStrategy{
	@Override
	public void algorithm() {
		System.out.println("Insertion Sort");
	}
}

La classe SelectionSort
CODE:
public class SelectionSort implements SortStrategy{
	@Override
	public void algorithm() {
		System.out.println("Selection Sort");
	}
}

La classe Quicksort
CODE:
public class Quicksort implements SortStrategy{
	@Override
	public void algorithm() {
		System.out.println("Quick Sort");
	}
}

La classe Context
CODE:
public class Context {
	private SortStrategy sortStrategy;
	public Context(SortStrategy sortStrategy) {
		this.sortStrategy = sortStrategy;
	}
	public void setSortStrategy(SortStrategy sortStrategy) {
		this.sortStrategy = sortStrategy;
	}
	public void operation() {
		System.out.println("------ Starting Algorithm:");
		this.sortStrategy.algorithm();
		System.out.println("------ Ending Algorithm\n");
	}
}

La classe Client
CODE:
public class Client {
	public static void main(String[] args) {
		SortStrategy sortStrategy = new InsertionSort();
		Context context = new Context(sortStrategy);
		context.operation();
		
		sortStrategy = new SelectionSort();
		context.setSortStrategy(sortStrategy);
		context.operation();
		
		sortStrategy = new Quicksort();
		context.setSortStrategy(sortStrategy);
		context.operation();
	}
}

Résultat:
CODE:
------ Starting Algorithm:
Insertion Sort
------ Ending Algorithm

------ Starting Algorithm:
Selection Sort
------ Ending Algorithm

------ Starting Algorithm:
Quick Sort
------ Ending Algorithm

Observer pattern

Présentation


Catégorie: behavioral
Objective:
Il définit une relation one-to-many entre les objets de telle sorte le changement d'état du one-objet entraine une notification et une modification pour les objets en relation avec cet objet.
Structure:
Observer pattern
Observer pattern

Implémentation en Java

Exemple: Donnez une solution pour une classe de devise. Si une devise est changée on doit notifier les personnes inscrient dans cette devise.
Exemple d'Observer pattern
Exemple d'Observer pattern

L'interface Subject
CODE:
public interface Subject {
	void subscribe(Observer observer);
	void remove(Observer observer);
	void notifyAllObservers();
}

L'interface Observer
CODE:
public interface Observer {
	public void update(Currency currency);
}

La classe Person
CODE:
public class Person implements Observer{
	private String name;
	public Person(String name) {
		this.name = name;
	}
	public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	}
	@Override
	public void update(Currency currency) {
		System.out.println("Notification to "+this.name+": currency "+currency.getName()+" points is changed to "+currency.getPoints());
	}
}

La classe Currency
CODE:
public class Currency {
	private String name;
	private int points;
	public Currency(String name, int points) {
		super();
		this.name = name;
		this.points = points;
	}
	public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	}
	public int getPoints() {
		return points;
	}
	public void setPoints(int points) {
		this.points = points;
	}
}

La classe CurrencySub
CODE:
public class CurrencySub implements Subject{
	private Currency currency;
	private List<Observer> observers;
	public CurrencySub(Currency currency) {
		this.currency = currency;
		this.observers = new ArrayList<>();
	}
	public Currency getCurrency() {
		return currency;
	}
	public void setCurrency(Currency currency) {
		this.currency = currency;
		notifyAllObservers();
	}
	@Override
	public void subscribe(Observer observer) {
		this.observers.add(observer);
	}
	@Override
	public void remove(Observer observer) {
		this.observers.remove(observer);
	}
	@Override
	public void notifyAllObservers() {
		for(Observer observer: observers) {
			observer.update(this.currency);
		}
	}
}

La classe Client
CODE:
public class Client {
	public static void main(String[] args) {
		Currency dollards = new Currency("dollards", 1254);
		CurrencySub currency = new CurrencySub(dollards);
		
		Person yassine = new Person("yassine");
		Person sami = new Person("sami");
		
		currency.subscribe(yassine);
		currency.subscribe(sami);
		
		dollards.setPoints(1255);
		currency.setCurrency(dollards);
	}
}

Résultat:
CODE:
Notification to yassine: currency dollards points is changed to 1255
Notification to sami: currency dollards points is changed to 1255

Observable et Observer du package java.util

Si on utilise le langage Java, on peut utiliser directement la classe Observable et l'interface Observer du package java.util pour implémenter le design pattern Observer.
Si on reprend le meme exemple précedant avec la classe Observable et l'interface Observer:
La classe Person
CODE:
public class Person implements Observer {
	private String name;
	public Person(String name) {
		this.name = name;
	}
	public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	}
	@Override
	public void update(Observable o, Object c) {
		Currency Currency = (Currency) c;
		System.out.println("Notification to "+this.name+": currency "+Currency.getName()+" points is changed to "+Currency.getPoints());
	}
}

La classe Currency
CODE:
public class Currency {
	private String name;
	private int points;
	public Currency(String name, int points) {
		super();
		this.name = name;
		this.points = points;
	}
	public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	}
	public int getPoints() {
		return points;
	}
	public void setPoints(int points) {
		this.points = points;
	}
}

La classe CurrencySub
CODE:
public class CurrencySub extends Observable{
	private Currency currency;
	public CurrencySub(Currency currency) {
		this.currency = currency;
	}
	public Currency getCurrency() {
		return currency;
	}
	public void setCurrency(Currency currency) {
		this.currency = currency;
		setChanged();
		notifyObservers(currency);
	}
}

La classe Client
CODE:
public class Client {
	public static void main(String[] args) {
		Currency dollards = new Currency("dollards", 1254);
		CurrencySub currency = new CurrencySub(dollards);
		
		Person yassine = new Person("yassine");
		Person sami = new Person("sami");
		
		currency.addObserver(yassine);
		currency.addObserver(sami);
		
		dollards.setPoints(1253);
		currency.setCurrency(dollards);
	}
}

Résultat:
CODE:
Notification to sami: currency dollards points is changed to 1253
Notification to yassine: currency dollards points is changed to 1253

Decorator pattern

Présentation


Catégorie: structural
Objective:
Attacher dynamiquement de nouvelles responsabilités à un objet. Les décorateurs offrent une alternative assez souple à l'héritage pour composer de nouvelles fonctionnalités.
Structure:
Decorator pattern
Decorator pattern

Implémentation en Java

Exemple: Un restaurant propose un sandwich basic avec 20dhs, le client peut ajouter des complémentaires (fromages 5dhs, poulet 15dhs). Proposez une solution.
Decorator pattern
Decorator pattern

La classe Sandwich
CODE:
public abstract class Sandwich {
	public abstract int cost();
	public abstract String description();
}

La classe BasicSandwich
CODE:
public class BasicSandwich extends Sandwich{
	@Override
	public int cost() {
		return 20;
	}
	@Override
	public String description() {
		return "Sandwich";
	}
}

La classe SandwichDecorator
CODE:
public abstract class SandwichDecorator extends Sandwich{
	private Sandwich sandwich;
	public SandwichDecorator(Sandwich sandwich) {
		this.sandwich = sandwich;
	}
	public Sandwich getSandwich() {
		return sandwich;
	}
	public void setSandwich(Sandwich sandwich) {
		this.sandwich = sandwich;
	}
	@Override
	public int cost() {
		return this.sandwich.cost();
	}
	@Override
	public String description() {
		return this.sandwich.description();
	}
}

La classe ChickenDecorator
CODE:
public class ChickenDecorator extends SandwichDecorator{
	public ChickenDecorator(Sandwich sandwich) {
		super(sandwich);
	}
	@Override
	public int cost() {
		return super.getSandwich().cost()+15;
	}
	@Override
	public String description() {
		return super.getSandwich().description()+", Chiken";
	}
}

La classe CheeseDecorator
CODE:
public class CheeseDecorator extends SandwichDecorator{
	public CheeseDecorator(Sandwich sandwich) {
		super(sandwich);
	}
	@Override
	public int cost() {
		return super.getSandwich().cost()+5;
	}
	@Override
	public String description() {
		return super.getSandwich().description()+", Cheese";
	}
}

La classe Client
CODE:
public class Client {
	public static void main(String[] args) {
		Sandwich sandwich1 = new ChickenDecorator(new CheeseDecorator(new BasicSandwich()));
		Sandwich sandwich2 = new ChickenDecorator(new BasicSandwich());
		
		System.out.println("------------- Sandwich1 :");
		System.out.println("Ingredients : " + sandwich1.description());
		System.out.println("Cost : " + sandwich1.cost());
		
		System.out.println("\n------------- Sandwich2 :");
		System.out.println("Ingredients : " + sandwich2.description());
		System.out.println("Cost : " + sandwich2.cost());
	}
}

Résultat:
CODE:
------------- Sandwich1 :
Ingredients : Sandwich, Cheese, Chiken
Cost : 40

------------- Sandwich2 :
Ingredients : Sandwich, Chiken
Cost : 35

Proxy pattern

Présentation

Catégorie: structural
Objective:
Donner un substitut pour un autre objet afin de contrôler l'accès à ce dernier.
Structure:
Proxy pattern
Proxy pattern

Implémentation en Java

Exemple: On veut limiter l'accés à internet par interdir une liste de sites web. Trouvez une solution.
Exemple de proxy pattern
Exemple de Proxy pattern

L'interface ISP
CODE:
public interface ISP {
	public void serviceSites(String url);
}

La classe Internet
CODE:
public class Internet implements ISP{
	@Override
	public void serviceSites(String url) {
		System.out.println("http://"+url);
	}
}

La classe InternetProxy
CODE:
public class InternetProxy implements ISP{
	private Internet internet;
	private List<String> forbidenSites = Arrays.asList("facebook.com", "twitter.com");
	public InternetProxy(Internet internet) {
		this.internet = internet;
	}
	@Override
	public void serviceSites(String url){
		if(forbidenSites.contains(url)) {
			System.out.println("Site "+url+" is forbidden!");
		}else {
			internet.serviceSites(url);
		}
	}
}

La classe Client
CODE:
public class Client {
	public static void main(String[] args) {
		Internet internet = new Internet();
		ISP internetProxy = new InternetProxy(internet);
		internetProxy.serviceSites("facebook.com"); //Forbidden
		internetProxy.serviceSites("google.com"); //Allowed
	}
}

Résultat:
CODE:
Site facebook.com is forbidden!
http://google.com