1 – Introduction
En premier lieu, les design patterns sont des modèles de conception répondant à des problématiques spécifiques dans la programmation orientée objet. Ils permettent aussi d’apporter des solutions efficaces, éprouvées par des développeurs experts dans le domaine et appliquées à des problèmes récurrents. Pourquoi réfléchir de zéro à un problème à chaque fois alors qu’il y a une solution existante à ce dernier ? De plus, ils facilitent la lecture du code par un autre développeur.
L’ouvrage qui a permis leur démocratisation est Design Patterns : Elements of reusable software, co-écrit par le Gang Of Four, composé des auteurs Gamma, Helm, Johnson et Vlissides. En effet, dans cet ouvrage, ils décrivent plus d’une vingtaine de design patterns qui sont classés sur trois catégories :
- D’abord, les modèles de création “Creational design patterns”
- Ensuite, les modèles de structuration “Structural design patterns”
- Enfin, les modèles de comportement “Behavioral design patterns”
Cet article va donc aborder une sélection de design patterns les plus connus dans chacune de ces catégories pour mieux les appréhender.
2 – Design patterns : les modèles de création « Creational patterns »
Les modèles de création, qui concernent l’instanciation et
la configuration des classes et des objets, font appelles à deux concepts de la
POO qui sont l’héritage et la délégation.
2.1 Singleton pattern
2.1.1 Problématique
Très souvent cité en entretien, le singleton pattern est un des modèles de design patterns le plus connu. Il répond en effet au besoin de n’avoir qu’une seule instance d’une classe et que cette dernière soit accessible dans toute l’application.
2.1.2 Exemple
Plus précisement, exemple basique du Singleton avec l’instance statique et la méthode statique pour la retourner.
2.1.2.1 Schéma
2.1.2.2 Code
public class Singleton { private static Singleton instance; private Singleton() { } public static Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; } }
2.2 Builder pattern
2.2.1 Problématique
La construction d’une classe contenant plusieurs champs peut en effet être lourde à implémenter, surtout lorsque l’alimentation d’un certain champ dépend d’une logique métier complexe. Le Builder pattern consiste à déplacer cette logique de construction hors de la classe concernée afin de l’alléger et pour rendre plus modulable la construction de l’objet.
2.2.2 Exemple
Un produit avec des attributs qui est construit par une
classe Builder.
2.2.2.1 Schéma
2.2.2.2 Code
public class Product { private final String color; private final float price; public static class ProductBuilder { private String color; private float price; public ProductBuilder couleur(String color) { this.color = color; return this; } public ProductBuilder prix(float price) { this.price = price; return this; } public Product build() { return new Product(this); } } private Product(ProductBuilder productBuilder) { this.color = productBuilder.color; this.price = productBuilder.price; } }
2.3 Factory Method Pattern
2.3.1 Problématique
Dans les design patterns, le factory method pattern permet de créer des objets d’une même famille sans avoir à spécifier leur classe. Ce rôle est délégué à la Factory qui saura, à partir de certains paramètres, créer les objets de la bonne classe sans exposer la logique de leur création. De fait, c’est un pattern souvent utilisé dans les Frameworks et les librairies qui fournissent le contrat d’utilisation aux applications clientes.
2.3.2 Exemple
Deux produits (TV, Radio) qui héritent d’une interface commune et qui sont construits par une classe Factory selon le type de produit.
2.3.2.1 Schéma
2.3.2.2 Code
public class FactoryPattern { public interface IProduct { void cost(); void price(); } public class TV implements IProduct { @Override public void cost() { System.out.println("The TV will cost 100$ to produce"); } @Override public void price() { System.out.println("The TV will be sold at 250$"); } } public class Radio implements IProduct { @Override public void cost() { System.out.println("The Radio will cost 50$ to produce"); } @Override public void price() { System.out.println("The Radio will be sold at 110$"); } } public class ProductFactory { public ProductFactory() { } IProduct produce(String type) { IProduct product = null; if ("TV".equals(type)) { product = new TV(); } else if ("Radio".equals(type)) { product = new Radio(); } return product; } } }
3 – Design patterns : les modèles de structuration “Structural patterns”
3.1 Adapter
3.1.1 Problématique
Ce pattern permet, pour un client qui ne pourrait pas appeler directement les fonctionnalités d’un programme, d’utiliser une interface adaptée à ce dernier.
3.1.2 Exemple
Un adaptateur HDMI vers VGA où la classe « Adapter » implémente cette adaptation.
3.1.2.1 Schéma
3.1.2.2 Code
public class AdapterPattern { public class HDMI { void getConnectorType() { System.out.println("HDMI Connector"); } } public class VGA { public void getConnectorType() { System.out.println("VGA Connector"); } } public class VgaAdapter extends VGA { private HDMI hdmi; public VgaAdapter(HDMI hdmi) { this.hdmi = hdmi; } public void getConnectorType() { hdmi.getConnectorType(); } } }
3.2 Bridge
3.2.1 Problématique
Le pattern du bridge permet de séparer la modélisation d’un problème à résoudre de son implémentation. Le problème est ainsi modélisé par une classe abstraite et une ou plusieurs classes représentent les implémentations possibles de cette problématique. Les implémentations peuvent donc évoluer et être changées en fonction des besoins sans avoir à modifier la modélisation du problème à résoudre.
3.2.2 Exemple
Un opérateur qui peut effectuer toutes les opérations qui implémentent l’interface « IOperation » en appelant la méthode « doOperation »
3.2.2.1 Schéma
3.2.2.2 Code
public class BridgePattern { interface IOperation { void doOperation(); } class OperationA implements IOperation { @Override public void doOperation() { System.out.println("Doing operation A"); } } class OperationB implements IOperation { @Override public void doOperation() { System.out.println("Doing operation B"); } } abstract class _Operator { protected IOperation operation; public _Operator(IOperation operation) { this.operation = operation; } abstract void operate(); } class Operator extends _Operator { public Operator(IOperation operation) { super(operation); } @Override void operate() { operation.doOperation(); } } }
3.3 Composite
3.3.1 Problématique
Le pattern du composite représente les objets de manière hiérarchisée sous forme d’une structure d’arbre et ces objets peuvent eux-mêmes être composés par d’autres objets afin qu’ils puissent être traités de manière uniforme.
3.3.2 Exemple
Un employé qui peut gérer zéro ou plusieurs employées.
3.3.2.1 Schéma
3.3.2.2 Code
public class CompositePattern { interface IEmployee { int managedEmployeesCount(); void add(IEmployee enmployee); void remove(IEmployee employee); void showInfo(); } class Employee implements IEmployee { String firstName; String lastName; List<IEmployee> employees = new ArrayList<>(); @Override public int managedEmployeesCount() { return employees.size(); } @Override public void add(IEmployee employé) { employees.add(employé); } @Override public void remove(IEmployee employé) { employees.remove(employé); } @Override public void showInfo() { System.out.println("Employee{" + "firstName='" + firstName + '\'' + ", lastName='" + lastName + '\'' + ", number of managed employees=" + employees.size() + '}'); Iterator<IEmployee> employeeIterator = employees.iterator(); while (employeeIterator.hasNext()) { employeeIterator.next().showInfo(); } } } }
4 – Design patterns : les modèles de comportement “Behavioral patterns”
4.1 Observer
4.1.1 Problématique
Le pattern Observer répond au besoin des clients de suivre le changement d’état d’un objet afin de se mettre à jour. En effet dans ce pattern, un «Subject » est observé par des « Observers » qui s’enregistrent auprès de lui et qui seront notifiés de toutes les modifications.
4.1.2 Exemple
Un article « Subject » qui est suivi par plusieurs lecteurs « Observers » où lorsqu’il est modifié, les lecteurs seront alors notifiés.
4.1.2.1 Schéma
4.1.2.2 Code
public class ObserverPattern { interface Subject { void register(Observer observer); void notifyObservers(); void unregister(Observer observer); } interface Observer { void update(); void setSubject(Subject subject); } class Article implements Subject { List<Observer> observers = new ArrayList<>(); boolean isStateChanged; @Override public void register(Observer observer) { observers.add(observer); } @Override public void unregister(Observer observer) { observers.remove(observer); } @Override public void notifyObservers() { if (isStateChanged) { for (Observer observer : observers) { observer.update(); } } } public void update() { isStateChanged = true; notifyObservers(); } } }
4.2 State
4.2.1 Problématique
Là-dessus, l’’état d’un objet est déterminé par le changement de valeurs de ces attributs. Le State pattern permet donc à l’objet de changer son comportement quand un changement survient sur son statut interne.
4.2.2 Exemple
Un moteur qui peut être démarré ou à l’arrêt.
4.2.2.1 Schéma
4.2.2.2 Code
public class StatePattern { interface IMotorState { void getState(); } class Started implements IMotorState { @Override public void getState() { System.out.println("Motor started"); } } class Stopped implements IMotorState { @Override public void getState() { System.out.println("Motor stopped"); } } class Motor implements IMotorState { IMotorState state; public Motor(IMotorState state) { this.state = state; } public void setState(IMotorState state) { this.state = state; } @Override public void getState() { state.getState(); } } }
4.3 Strategy
4.3.1 Problématique
Un objet client a en effet besoin de choisir dynamiquement un algorithme adapté à la problématique qu’il traite. Alors le strategy pattern permet d’encapsuler chaque algorithme dans une classe d’implémentation et de pouvoir utiliser la plus adaptée pour résoudre un problème.
4.3.2 Exemple
Un exemple de jeu où il y a plusieurs stratégies notamment celle d’attaque ou de défense.
4.3.2.1 Schéma
4.3.2.2 Code
public class StrategyPattern { interface Strategy { void apply(); } class Attack implements Strategy { @Override public void apply() { System.out.println("Attack !!!"); } } class Defend implements Strategy { @Override public void apply() { System.out.println("Defend !!!"); } } class GameContext { Strategy strategy; public GameContext(Strategy strategy) { this.strategy = strategy; } public void setStrategy(Strategy strategy) { this.strategy = strategy; } void applyStrategy() { strategy.apply(); } } }
Pour conclure sur le sujet des design patterns, il n’y a donc plus que le pattern du Singleton à citer dans un prochain entretien. Essentiellement, la manière la plus efficace pour les retenir c’est la pratique dès que l’occasion se présente, n’hésitez pas à les utiliser !
Poursuivez votre lecture sur nos autres articles autour de Java !