Les design patterns de structuration

Dans cette séance, on va s’intéresser à des design patterns permettant de structurer des objets complexes :

Le design pattern Decorator (Décorateur)

La problématique :

Notre jeu est un vrai succès et de nombreux joueurs demandent un peu de diversité dans la niveau des ennemis. Pour l’instant, tous les Gobelins ont les mêmes caractéristiques. Ils aimeraient bien que certains d’entre eux soient des mini boss, d’autres des boss, etc. Il est facile de répondre aux attentes des joueurs en utilisant l’héritage :

Extension de la hiérarchie de classes des Gobelins

L’inconvénient de cette méthode, c’est que l’on rajoute 3 nouvelles classes pour les Gobelins mais on doit faire la même chose avec les Orcs, les Elfs, les Dragons, les Nains, etc. Cela fait beaucoup de nouvelles classes à créer.

Pire encore, après avoir créé ces classes, les joueurs ont vu le film « L’incroyable Hulk » et demandent à ce que les ennemis puissent muter pendant une courte période de temps. Pendant cette période, leur force est décuplée et ils deviennent tout vert. On doit donc modifier notre hiérarchie de classe :

Extension de la hiérarchie de classes des Gobelins

On sent bien que cette stratégie n’est pas la bonne : on doit en effet créer trop de sous-classes. Le problème, ici, c’est que l’héritage est une forme trop « statique » d’extension des classes. Le design pattern Decorator (Décorateur) permet de pallier cela.

Définition

La problématique générale que le design pattern Decorator essaie de résoudre est l’ajout dynamique de nouveaux comportements à des objets sans avoir à créer une hiérarchie de classe démente.

La solution apportée par Decorator :

La solution apportée par le design pattern Decorator pour l’ensemble des Ennemis peut être décrit grâce au diagramme UML suivant :

Extension de la hiérarchie de classes des Ennemis

L’idée consiste à créer une classe abstraite EnnemiDecorator qui, à la fois :

Ainsi, tout EnnemiDecorator est un Ennemi. Comme il contient une instance d'Ennemi, il peut déléguer à cette instance les appels aux méthodes incluses dans l’interface Ennemi. De plus, comme on hérite du décorateur, on peut redéfinir ces méthodes comme on le souhaite. Notez qu’en plus l'Ennemi inclus par composition peut lui-même être un EnnemiDecorator, ce qui permet de créer facilement, par exemple, des boss Gobelins hulkisés.

Pour être plus clair, voici un exemple en Java :

Ennemi.java
package fr.polytech.aco.jeu.ennemi;

public interface Ennemi {
  public String attaquer();
}
Gobelin.java
package fr.polytech.aco.jeu.ennemi;

public class Gobelin implements Ennemi {
  public String attaquer() {
    return "attaque d'un Gobelin";
  }
}
Orc.java
package fr.polytech.aco.jeu.ennemi;

public class Orc implements Ennemi {
  public String attaquer() {
    return "attaque d'un Orc";
  }
}

Pour l’instant, on a créé les classes d'Ennemis classiques. Maintenant, passons au décorateur. Celui-ci contient par composition un Ennemi et on peut voir qu’il délègue l’exécution de la méthode attaquer() à cette instance d'Ennemi.

EnnemiDecorator.java
package fr.polytech.aco.jeu.ennemi;

public abstract class EnnemiDecorator implements Ennemi {
  protected Ennemi ennemi;

  public EnnemiDecorator(Ennemi ennemi) {
    this.ennemi = ennemi;
  }

  public String attaquer() {
    return this.ennemi.attaquer();
  }
}

Les codes de Boss et Hulkise ci-dessous montrent l’idée clef des enfants du décorateur : ils redéfinissent la méthode attaquer() comme ils le souhaitent.

Boss.java
package fr.polytech.aco.jeu.ennemi;

public class Boss extends EnnemiDecorator {
  public Boss(Ennemi ennemi) {
    super(ennemi);
  }

  public String attaquer() {
    return this.ennemi.attaquer() + " Boss";
  }
}
Hulkise.java
package fr.polytech.aco.jeu.ennemi;

public class Hulkise extends EnnemiDecorator {
  public Hulkise(Ennemi ennemi) {
    super(ennemi);
  }

  public String attaquer() {
    return this.ennemi.attaquer() + " hulkisé";
  }
}

Enfin, le main() montre comment créer et utiliser des ennemis de différents types.

EtatJeu.java
package fr.polytech.aco.jeu;

import fr.polytech.aco.jeu.ennemi.*;

public class EtatJeu {
    public static void main(String[] args) {
        // on crée un ennemi standard
        Ennemi ennemi1 = new Gobelin();
        // on crée un Boss Orc
        Ennemi ennemi2 = new Boss(new Orc());
        // on crée un Boss Gobelin qui est, en plus, hulkisé
        Ennemi ennemi3 = new Hulkise(new Boss(new Gobelin()));

        System.out.println(ennemi1.attaquer());
        System.out.println(ennemi2.attaquer());
        System.out.println(ennemi3.attaquer());
    }
}

Le résultat du programme :

attaque d'un Gobelin
attaque d'un Orc Boss
attaque d'un Gobelin Boss hulkisé

Avantages et inconvénients :

Avantages :

Inconvénients :

Le design pattern Composite

La problématique :

Notre jeu de rôle prévoit que l’on rencontre et combatte individuellement des Ennemis. Et si on le corsait un peu en prévoyant que certains Ennemis sont en bandes. On pourrait rajouter une nouvelle classe BandeEnnemis qui contient une liste d”Ennemis et faire en sorte que ÉtatJeu contienne un nouvel attribut bandes qui contient les bandes d'Ennemis. Mais cela va complexifier ÉtatJeu, ce qui n’est peut-être pas nécessaire car, dans une bande, les Ennemis se déplacent tous de la même manière et, quand le joueur combat la bande, il combat tous les Ennemis qu’elle contient. Donc, finalement, une BandeEnnemis se comporte comme un Ennemi solitaire. Peut-être pourrait-on unifier les BandeEnnemis et les Ennemis. C’est l’objectif du design pattern Composite.

Définition

L’objectif du design pattern Composite est de créer des objets complexes contenant d’autres objets (en fait une arborescence d’objets) et d’offrir une interface uniforme pour l’ensemble de ces objets.

La solution apportée par Composite :

La solution apportée est la même que celle du Décorateur. Essentiellement, c’est la sémantique qui diffère : dans un décorateur, on souhaite rajouter des fonctionnalités dynamiquement en cours d’exécution du programme; dans un objet composite, on souhaite maintenir ensemble un groupe d’objets dont le comportement est similaire. Pour notre problème de bandes d'Ennemis, voilà le diagramme UML correspondant, où BandeEnnemis est l’objet composite :

Un objet composite d'Ennemis

En termes Java, on aurait un code similaire à celui ci-dessous, l’objet Composite étant la classe BandeEnnemis:

Ennemi.java
package fr.polytech.aco.jeu.ennemi;

public interface Ennemi {
  public String attaquer();
}
Gobelin.java
package fr.polytech.aco.jeu.ennemi;

public class Gobelin implements Ennemi {
  public String attaquer() {
    return "attaque d'un Gobelin";
  }
}
Orc.java
package fr.polytech.aco.jeu.ennemi;

public class Orc implements Ennemi {
  public String attaquer() {
    return "attaque d'un Orc";
  }
}
BandeEnnemis.java
package fr.polytech.aco.jeu.ennemi;

import fr.polytech.aco.MyArray;

public class BandeEnnemis implements Ennemi {
  private MyArray<Ennemi> ennemis;

  public BandeEnnemis() {
    this.ennemis = new MyArray<>(2);
  }

  public BandeEnnemis(Ennemi[] ennemis) {
    this.ennemis = new MyArray<>(ennemis);
  }

  public void addEnnemi(Ennemi ennemi) {
    this.ennemis.pushBack(ennemi);
  }

  public String attaquer() {
    String res = "";
    for (var ennemi : this.ennemis)
      res += ennemi.attaquer() + " ; ";
    res = res.substring(0, res.length() - 3);
    return res;
  }
}
ÉtatJeu.java
import fr.polytech.aco.jeu.ennemi.*;

public class EtatJeu {
  public static void main(String[] args) {
    // une bande avec un Orc et un Gobelin
    BandeEnnemis bande1 = new BandeEnnemis();
    bande1.addEnnemi(new Orc());
    bande1.addEnnemi(new Gobelin());
    System.out.println(bande1.attaquer());

    // une bande contenant la bande précédente + un Gobelin
    BandeEnnemis bande2 = new BandeEnnemis();
    bande2.addEnnemi(new Gobelin());
    bande2.addEnnemi(bande1);
    System.out.println(bande2.attaquer());
  }
}

Le résultat du programme :

attaque d'un Orc ; attaque d'un Gobelin
attaque d'un Gobelin ; attaque d'un Orc ; attaque d'un Gobelin

Avantages et inconvénients :

Avantages :

Inconvénients :

Exercice 1 : La structuration de ma petite entreprise   

Quels patrons pensez-vous pouvoir utiliser dans l’application de votre petite entreprise de restauration ? Modifiez en conséquence son diagramme de classes.

 
© C.G. 2007 - 2024