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

schéma de Singleton

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

schéma de Builder pattern

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

schéma de Factory Method Pattern

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

schéma de Structural pattern

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

schéma de Bridge

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

schéma de composite

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

Schéma de Behavioral pattern

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

schéma de State

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

schéma de Strategy

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 !