Patrons de conception

1. Double Dispatch

Considérez les classes Graphic, Shape et Display, dont le code est listé ci-dessous :

Listing 1. Graphic.java
package fr.unantes.sce.patterns.dispatch;

public class Graphic {
    @Override
    public String toString() {
        return "Graphic";
    }
}
Listing 2. Shape.java
package fr.unantes.sce.patterns.dispatch;

public class Shape extends Graphic {
    @Override
    public String toString() {
        return "Shape";
    }
}
Listing 3. Display.java
package fr.unantes.sce.patterns.dispatch;

public class Display {

    void display(Graphic graphic) {
        System.out.println("Displaying a Graphic");
    }

    void display(Shape shape) {
        System.out.println("Displaying a Shape");
    }

    public static void main(String[] args) {
        Graphic ga = new Graphic();
        Graphic gb = new Shape();
        Shape s = new Shape();
        Display d = new Display();

        d.display(ga);
        d.display(gb);
        d.display(s);
    }
}

Qu’est-ce qui sera affiché lorsque la méthode Display::main() sera exécutée ? Pourquoi ?

L’expédition multiple (Multiple Dispatch) est une caractéristique propre à certains langages de programmation, tels que C#, Groovy ou Julia. Elle permet de déterminer la méthode à invoquer, en fonction du type dynamique (à l’exécution) de l’objet récepteur et de ceux des arguments.

La double expédition est une forme spéciale de multiple expédition, qui détermine la méthode à invoquer en fonction des types dynamiques du récepteur et d’un argument. Cependant, la plupart des langages à objets typés, tels que Java et C++, déterminent la méthode à invoquer en fonction du type dynamique du récepteur et des types statiques des arguments.

Supposons que vous deviez gérer des portefeuilles contenant de l’argent dans différentes devises : Euro (€), Dollar ($), Livre (£), etc. Ajouter de l’argent à un portefeuille est un problème, car vous ne pouvez additionner que de l’argent d’une même devise.

Lorsque vous additionnez de l’argent provenant de différentes devises, vous devez créer un porte-monnaie d’argent contenant les deux montants. Par exemple, 15€ + 5€ = 20€, mais 15€ + 5£ = {15€, 5£}.

Vous pouvez également additionner de l’argent à un porte-monnaie et additionner deux porte-monnaies. Par exemple, 15€ + {15€, 5£} = {30€, 5£} et {5€, 5£} + {10€, 10£} = {15€, 15£}.

En résumé, il y a 3 additions différentes :

  1. Argent + Argent

  2. Argent + Porte-monnaie (ou vice-versa)

  3. Porte-monnaie + Porte-monnaie

Par conséquent, la complexité de la mise en œuvre concerne la gestion des différentes combinaisons possibles d’argent et de porte-monnaie.

Une manière élégante de résoudre ce problème consiste à utiliser le patron de conception Double Dispatch. L’idée derrière ce patron est d’utiliser un appel supplémentaire pour découvrir le type dynamique de l’argument. Par exemple, considérons l’interface Money et ses implémentations, MoneyBag et SingleMoney :

Listing 4. L’interface Money
interface Money {
    Money add(Money amount);
}

class MoneyBag implements Money {
    public Money add(Money amount) {
    //...
    }
}

Les implémentations de la méthode add() n’ont pas accès au type dynamique du paramètre amount, qui peut être soit SingleMoney soit MoneyBag. Une manière simple de trouver le type dynamique de l’argument est d’utiliser l’opérateur instanceof, mais cette solution est rarement une bonne idée.

Le patron Double Dispatch à la rescousse : Une autre solution consiste à utiliser l’argument comme récepteur d’un nouvel appel de méthode et laisser le polymorphisme faire sa magie :

class MoneyBag implements Money {}
    public Money add(Money amount) {
        amount.addMoneyBag(this);
    }
}

La solution est simple, puisque nous connaissons le type dynamique de l’objet courant (this), MoneyBag. Nous utilisons un deuxième appel de méthode, cette fois en utilisant l’argument amount comme récepteur de l’appel.

Quelle implémentation de la méthode addMoneyBag() sera invoquée, SingleMoney::addMoneyBag() ou MoneyBag::addMoneyBag() ?

La réponse est simple : Java utilise le type dynamique du récepteur, si amount est une instance de SingleMoney, il invoquera la première, et si amount est une instance de MoneyBag, il appellera la seconde.

Néanmoins, cette solution présente un inconvénient important, l’interface Money doit spécifier les méthodes addSingleMoney(SingleMoney) et addMoneyBag(MoneyBag). En d’autres termes, l’interface dépend de ses implémentations, ce qui est une mauvaise pratique.

1.1. Exercice : L’interface Money

  1. Créez une interface nommée Money, contenant 3 méthodes : add(), addSingleMoney() et addMoneyBag().

1.2. Exercice : La classe SingleMoney

Ensuite, créez une classe immuable nommée SingleMoney qui implémente l’interface Money et ses trois méthodes. Cette classe doit posséder deux champs, représentant leur montant ainsi que le nom de la devise utilisée.

  1. Pour commencer, ajoutez deux constructeurs à cette classe, permettant de l’instancier à partir d’un montant et d’une devise et aussi à partir de deux instances de la classe SingleMoney.

  2. Ensuite, implémentez les trois méthodes d’addition.

1.3. Exercice : La classe MoneyBag

Enfin, créez une classe immuable nommée MoneyBag qui implémente l’interface Money et ses trois méthodes. Cette classe doit posséder un champ, représentant un multi-ensemble de monnaies simples.

  1. Pour commencer, ajoutez trois constructeurs à cette classe, permettant de l’instancier à partir de

    1. Deux monnaies simples

    2. Un porte-monnaie et une monnaie simple

    3. Deux portemonnaies

  2. Ensuite, implémentez les trois méthodes d’addition.


2. État

La machine d’états présentée ci-dessous est attachée à la classe Connection. Ce diagramme nous permet de savoir que la classe possède au moins cinq opérations : connect(), disconnect(), setNotAvailable(), setAvailable() et setFreeForChat(). Chacune de ces opérations résultent en un changement d’état.

Dans cet exercice, nous allons utiliser le patron de conception "État" pour implémenter la classe Connection.

Nous allons implémenter seulement le comportement correspondant aux changements d’état.
Diagramme État-Transition de la classe Connection
Figure 1. Diagramme État-Transition de la classe Connection

2.1. Exercice : Diagramme de classes

  1. Pour commencer, dessinez un diagramme de classes UML représentant votre solution

  2. Implémentez la classe Connection et ses méthodes.

  3. Maintenant, proposez une interface pour la classe ConnectionState. Elle doit contenir toutes les méthodes qui dépendent de l’état de la connexion.

  4. Enfin, implémentez les classes état, ainsi que leurs méthodes.

3. Itérateur

En Java, la façon traditionnelle de parcourir une collection d’objets est d’utiliser des itérateurs externes. On les appelle externes, car ils ne sont pas contrôlés par la collection, mais par la classe cliente souhaitant la parcourir. Ils sont aussi robustes, parce qu’ils sont capables de détecter des modifications dans la collection durant le parcours et d’arrêter l’itération.

Une autre façon de parcourir les collections, c’est d’utiliser les itérateurs internes. Dans ce cas, c’est la collection elle-même qui contrôle l’itération. Par exemple, l’interface Collection propose les méthodes removeIf() et forEach(), qui utilisent des itérateurs internes. La première supprime tous les éléments de la collection qui satisfont à un prédicat donné. La deuxième effectue une action pour chaque élément de la collection, jusqu’à ce que tous les éléments aient été traités (ou que l’action lance une exception).

3.1. Exercice : itérateurs internes

Nous souhaitons ajouter 4 méthodes à l’interface Collection de Java :

  • select() : sélectionne les élements de la collection qui satisfont à un prédicat passé en paramètre ;

  • forAll() : retourne vrai si tous les éléments satisfont à un prédicat donné, faux autrement ;

  • forEach() : exécute une action pour chaque élément de la collection ;

  • collect() : exécute une action pour chaque élément de la collection et ajoute le résultat à une nouvelle collection, de même taille, mais potentiellement de type différent.

Travail à faire
  1. Proposez la signature des 4 méthodes ;

  2. Ensuite, proposez une façon d’étendre les classes qui implémentent l’interface Collection avec ces méthodes ;

  3. Enfin, implémentez les 4 méthodes et donnez des exemples d’utilisation de ces méthodes.