Aller au contenu

1. Design pattern : c’est quoi ?

En développement informatique, les Design Patterns sont des solutions standards à des problèmes fréquemment rencontrés. Ils permettent aux développeurs et développeurses de résoudre rapidement les problèmes courants sans avoir à réinventer la roue. Ils garantissent un code propre, facile à maintenir et permettent d’accélérer le développement !

Il existe différents types de Design Patterns, en voici quelques uns.

En cours de préparation… des exemples simples et variés pour aborder tous les design patterns importants… merci de patienter ;)

1.1 Création

Pour créer des objets et des classes, en gérant les dépendances entre eux :

Singleton

Exemple :

public class Singleton {
   private static Singleton instance = null;
   private Singleton() {}
   
   public static Singleton getInstance() {
      if (instance == null) {
         instance = new Singleton();
      }
      return instance;
   }
}

Prototype

public class PrototypeDemo {
   public static void main(String[] args) {
      ShapeCache.loadCache();
 
      Shape clonedShape = (Shape) ShapeCache.getShape("1");
      System.out.println("Shape : " + clonedShape.getType());
 
      Shape clonedShape2 = (Shape) ShapeCache.getShape("2");
      System.out.println("Shape : " + clonedShape2.getType());
 
      Shape clonedShape3 = (Shape) ShapeCache.getShape("3");
      System.out.println("Shape : " + clonedShape3.getType());
   }
}
abstract class Shape implements Cloneable {
   private String id;
   protected String type;
 
   abstract void draw();
 
   public String getType() {
      return type;
   }
 
   public String getId() {
      return id;
   }
 
   public void setId(String id) {
      this.id = id;
   }
 
   public Object clone() {
      Object clone = null;
      try {
         clone = super.clone();
      } catch (CloneNotSupportedException e) {
         e.printStackTrace();
      }
      return clone;
   }
}
class Rectangle extends Shape {
   public Rectangle() {
      type = "Rectangle";
   }
 
   @Override
   public void draw() {
      System.out.println("Inside Rectangle::draw() method.");
   }
}
class Circle extends Shape {
   public Circle() {
      type = "Circle";
   }
 
   @Override
   public void draw() {
      System.out.println("Inside Circle::draw() method.");
   }
}
class Square extends Shape {
   public Square() {
      type = "Square";
   }
 
   @Override
   public void draw() {
      System.out.println("Inside Square::draw() method.");
   }
}
class ShapeCache {
   private static Map<String, Shape> shapeMap = new HashMap<String, Shape>();
 
   public static Shape getShape(String shapeId) {
      Shape cachedShape = shapeMap.get(shapeId);
      return (Shape) cachedShape.clone();
   }
 
   public static void loadCache() {
      Circle circle = new Circle();
      circle.setId("1");
      shapeMap.put(circle.getId(), circle);
 
      Square square = new Square();
      square.setId("2");
      shapeMap.put(square.getId(), square);
 
      Rectangle rectangle = new Rectangle();
      rectangle.setId("3");
      shapeMap.put(rectangle.getId(), rectangle);
   }
}

1.2 Structure

Pour structurer le code et améliorer l’architecture du logiciel :

Facade

public class ComputerFacade {
   private CPU processor;
   private Memory ram;
   private HardDrive hd;
   
   public ComputerFacade() {
      this.processor = new CPU();
      this.ram = new Memory();
      this.hd = new HardDrive();
   }
   
   public void start() {
      processor.freeze();
      ram.load(123, hd.read(456, 789));
      processor.jump(123);
      processor.execute();
   }
}

Decorator

public class DecoratorDemo {
   public static void main(String[] args) {
      Shape circle = new Circle();
      Shape redCircle = new RedShapeDecorator(new Circle());
      Shape redRectangle = new RedShapeDecorator(new Rectangle());
      System.out.println("Circle with normal border");
      circle.draw();
      System.out.println("\nCircle of red border");
      redCircle.draw();
      System.out.println("\nRectangle of red border");
      redRectangle.draw();
   }
}

interface Shape {
   void draw();
}

class Circle implements Shape {
   @Override
   public void draw() {
      System.out.println("Shape: Circle");
   }
}

class Rectangle implements Shape {
   @Override
   public void draw() {
      System.out.println("Shape: Rectangle");
   }
}

abstract class ShapeDecorator implements Shape {
   protected Shape decoratedShape;
 
   public ShapeDecorator(Shape decoratedShape){
      this.decoratedShape = decoratedShape;
   }
 
   public void draw(){
      decoratedShape.draw();
   }
}

class RedShapeDecorator extends ShapeDecorator {
   public RedShapeDecorator(Shape decoratedShape) {
      super(decoratedShape);
   }
 
   @Override
   public void draw() {
      decoratedShape.draw();     
      setRedBorder(decoratedShape);
   }
 
   private void setRedBorder(Shape decoratedShape){
      System.out.println("Border Color: Red");
   }
}

Flyweight

import java.util.HashMap;

public class FlyweightDemo {
   private static final String colors[] = 
      { "Red", "Green", "Blue", "White", "Black" };
   public static void main(String[] args) {
      for(int i=0; i < 20; ++i) {
         Circle circle = 
            (Circle)ShapeFactory.getCircle(getRandomColor());
         circle.setX(getRandomX());
         circle.setY(getRandomY());
         circle.setRadius(100);
         circle.draw();
      }
   }
   private static String getRandomColor() {
      return colors[(int)(Math.random()*colors.length)];
   }
   private static int getRandomX() {
      return (int)(Math.random()*100 );
   }
   private static int getRandomY() {
      return (int)(Math.random()*100);
   }
}
interface Shape {
   void draw();
}
class Circle implements Shape {
   private String color;
   private int x;
   private int y;
   private int radius;
 
   public Circle(String color){
      this.color = color;     
   }
 
   public void setX(int x) {
      this.x = x;
   }
 
   public void setY(int y) {

this.y = y;
}

public void setRadius(int radius) {
this.radius = radius;
}

@Override
public void draw() {
System.out.println("Circle: Draw() [Color : " + color + ", x : " + x + ", y :" + y + ", radius :" + radius);
}
}
```java

```java
class ShapeFactory {
private static final HashMap<String, Shape> circleMap = new HashMap<>();

public static Shape getCircle(String color) {
Circle circle = (Circle)circleMap.get(color);

  if(circle == null) {
     circle = new Circle(color);
     circleMap.put(color, circle);
     System.out.println("Creating circle of color : " + color);
  }
  return circle;

}
}

1.3 Comportement

Pour gérer les interactions entre les objets et les classes :

Observer

public class ObserverDemo {
   public static void main(String[] args) {
      Subject subject = new Subject();
 
      new BinaryObserver(subject);
      new OctalObserver(subject);
      new HexaObserver(subject);
 
      System.out.println("First state change: 15"); 
      subject.setState(15);
      System.out.println("Second state change: 10"); 
      subject.setState(10);
   }
}
class Subject {
   private List<Observer> observers = new ArrayList<Observer>();
   private int state;
 
   public int getState() {
      return state;
   }
 
   public void setState(int state) {
      this.state = state;
      notifyAllObservers();
   }
 
   public void attach(Observer observer) {
      observers.add(observer);
   }
 
   public void notifyAllObservers() {
      for (Observer observer : observers) {
         observer.update();
      }
   }
}
abstract class Observer {
protected Subject subject;
public abstract void update();
}

class BinaryObserver extends Observer {
public BinaryObserver(Subject subject) {
this.subject = subject;
this.subject.attach(this);
}

@Override
public void update() {
System.out.println("Binary String: " + Integer.toBinaryString(subject.getState()));
}
}

class OctalObserver extends Observer {
public OctalObserver(Subject subject) {
this.subject = subject;
this.subject.attach(this);
}

@Override
public void update() {
System.out.println("Octal String: " + Integer.toOctalString(subject.getState()));
}
}

class HexaObserver extends Observer {
public HexaObserver(Subject subject) {
this.subject = subject;
this.subject.attach(this);
}

@Override
public void update() {
System.out.println("Hex String: " + Integer.toHexString(subject.getState()).toUpperCase());
}
}

Mediator

public class MediatorDemo {
   public static void main(String[] args) {
      User robert = new User("Robert");
      User john = new User("John");
      robert.sendMessage("Hi! John!");
      john.sendMessage("Hello! Robert!");
}
}
class User {
private String name;

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public User(String name){
this.name = name;
}

public void sendMessage(String message){
ChatRoom.showMessage(this,message);
}
}
class ChatRoom {
public static void showMessage(User user, String message){
System.out.println(new Date().toString() + " [" + user.getName() + "] : " + message);
}
}

Dans cet exemple, la classe ChatRoom agit en tant que médiateur entre les utilisateurs. Les utilisateurs peuvent envoyer des messages à la salle de chat sans avoir besoin de connaître les détails de l’autre utilisateur. Cela aide à maintenir une séparation claire entre les différents composants de l’application.

Proxy

public class ProxyDemo {
public static void main(String[] args) {
Image image = new ProxyImage("test_10mb.jpg");
//image will be loaded from disk
image.display();
System.out.println("");
//image will not be loaded from disk
image.display();
}
}
interface Image {
void display();
}
class RealImage implements Image {

private String fileName;

public RealImage(String fileName){
this.fileName = fileName;
loadFromDisk(fileName);
}

@Override
public void display() {
System.out.println("Displaying " + fileName);
}

private void loadFromDisk(String fileName){
System.out.println("Loading " + fileName);
}
}
class ProxyImage implements Image{

private RealImage realImage;
private String fileName;

public ProxyImage(String fileName){
this.fileName = fileName;
}

@Override
public void display() {
if(realImage == null){
realImage = new RealImage(fileName);
}
realImage.display();
}
}

Multiton

Le design pattern Multiton est similaire au Singleton, mais il permet de maintenir plusieurs instances d’une même classe en mémoire. Cependant, contrairement au Singleton, chaque instance du Multiton est associée à une clé unique, ce qui permet de les identifier de manière distincte.


import java.util.HashMap;

public class Multiton {
    private static HashMap<String, Multiton> instances = new HashMap<>();
    private int value;

    private Multiton(int value) {
        this.value = value;
    }

    public static Multiton getInstance(String key, int value) {
        if (!instances.containsKey(key)) {
            instances.put(key, new Multiton(value));
        }
        return instances.get(key);
    }

    public int getValue() {
        return value;
    }
}

Dans cet exemple, on défini une classe Multiton qui maintient une collection d’instances associées à des clés distinctes. Pour obtenir une instance du Multiton, nous appelons la méthode statique getInstance() (comme dans le design pattern Signleton) et on passe une clé en paramètre.

Si une instance associée à la clé n’existe pas encore, alors une nouvelle instance est créée et associée à la clé, sinon, l’instance existante est retournée.

L’utilisation du pattern Multiton peut être utile dans des cas où on a besoin de maintenir plusieurs instances d’une même classe en mémoire en ayant la possibilité de pouvoir identifier les instances de manière distincte.

Par exemple, vous pourriez utiliser le Multiton pour gérer différents pools de connexions à une base de données en associant chaque pool à une clé distincte.

Memento

Le design pattern Memento est un modèle de conception utilisé pour enregistrer et restaurer l’état d’un objet à un moment donné. Il s’agit d’un modèle de conception utile pour les opérations de undo/redo et les systèmes de sauvegarde/restauration d’état que l’on toruve souvent dans les éditeurs de textes permettant de revenir en arrière et d’annuler une opération.

import java.util.ArrayList;
import java.util.List;

public class Originator {
    private int state;

    public void setState(int state) {
        this.state = state;
    }

    public int getState() {
        return state;
    }

    public Memento saveStateToMemento() {
        return new Memento(state);
    }

    public void getStateFromMemento(Memento memento) {
        state = memento.getState();
    }
}
public class Memento {
    private int state;

    public Memento(int state) {
        this.state = state;
    }

    public int getState() {
        return state;
    }
}
public class CareTaker {
    private List<Memento> mementoList = new ArrayList<>();

    public void add(Memento state) {
        mementoList.add(state);
    }

    public Memento get(int index) {
        return mementoList.get(index);
    }
}

Dans cet exemple,on défini une classe Originator qui représente l’objet pour lequel on souhaite enregistrer l’état.

La classe Originator possède une méthode saveStateToMemento() qui retourne un objet Memento contenant l’état actuel de l’objet. La classe Memento est une classe interne qui enregistre l’état de l’objet. La classe CareTaker est utilisée pour stocker les objets Memento pour une utilisation ultérieure.

Utilisation : On créé une instance de la classe Originator, puis on modifie son état en utilisant la méthode setState(). Pour enregistrer l’état de l’objet, on créé un objet Memento en appelant la méthode saveStateToMemento() et en le stockant dans la classe CareTaker.

Pour restaurer l’état de l’objet, on appelle la méthode getStateFromMemento() en lui passant l’objet Memento que l’on veut restaurer !

Strategy

interface Strategy {
    int doOperation(int num1, int num2);
}

class AdditionStrategy implements Strategy {

    @Override
    public int doOperation(int num1, int num2) {
        return num1 + num2;
    }
}

class SubtractionStrategy implements Strategy {
    @Override
    public int doOperation(int num1, int num2) {
        return num1 - num2;
    }
}

class Context {
    private Strategy strategy;

    public Context(Strategy strategy) {
        this.strategy = strategy;
    }

    public int executeStrategy(int num1, int num2) {
        return strategy.doOperation(num1, num2);
    }
}

Le design pattern Strategy est un modèle de conception utilisé pour définir une famille d’algorithmes, d’encapsuler chacun d’entre eux et de les rendre interchangeables.

Le but est de permettre à un objet de changer de comportement à l’exécution sans changer son code.

Dans l’exemple ci-dessus, nous avons défini une interface Strategy qui définit une méthode doOperation() pour les algorithmes. Nous avons également défini deux classes concrètes AdditionStrategy et SubtractionStrategy qui implémentent l’interface Strategy et définissent les algorithmes pour addition et de soustraction.

La classe Context encapsule le choix de l’algorithme à utiliser et possède une référence à l’objet Strategy sélectionné.

La méthode executeStrategy() de la classe Context appelle la méthode doOperation() de l’objet Strategy concerné.

Ainsi, pour utiliser le pattern Strategy, nous créons une instance de la classe Context en lui passant un objet Strategy concret, comme AdditionStrategy ou SubtractionStrategy. Nous appelons ensuite la méthode executeStrategy() pour effectuer l’opération définie par l’objet Strategy.

De fait, vous pouvez changer d’algorithme en passant une autre implémentation de Strategy à la classe Context, plutôt pratique !

Les Design Pattern indispensables pour créer un framework complet et autonome

Ces design patterns ne sont que quelques-uns des nombreux que vous pouvez utiliser pour créer votre framework Java ou autre. Finalement, vous utilisez déjà de nombreux modèles de conception sans même vous en rendre compte !