Les patterns comportementaux permettent de faciliter la coopération entre différents objets pour implanter des actions à réaliser.
Patron de méthode (template method)
- Ce pattern permet d'implanter une action dans une classe abstraite qui repose sur des implantations spécifiques qui seront différées et mises en oeuvres dans les classes concrètes dérivées.
- Cela permet d'implanter un comportement de haut-niveau qui reposera sur des sous-comportements de plus bas-niveau implantées dans les classes concrètes.
Exemple : repas d'animaux
- Nous implantons une méthode permettant à un animal de prendre un repas.
- Tous les animaux mangent de la nourriture et boivent... par contre chacun a ses habitudes alimentaires en matière de nourriture et a besoin d'une quantité différente d'eau à boire
public abstract class Animal { public void takeMeal(FoodStore fs) { eat(fs); drink(fs); } public void drink(FoodStore fs) { fs.takeWater(getDrinkPortion()); } /** The normal drink portion of the animal in liters, to be implemented in concrete classes */ public abstract double getDrinkPortion(); /** Eat a portion of food (specifical for each animal) */ public abstract void eat(FoodStore fs); }
public class Cat extends Animal { public double getDrinkPortion() { return 0.1; } public void eat(FoodStore fs) { fs.takeMeat(0.2); } }
public class Dog extends Animal { public double getDrinkPortion() { return 0.2; } public void eat(FoodStore fs) { fs.takeMeat(0.5); } }
Patterns strategy et state
Présentation
- Une action incorporée dans une classe peut posséder différentes implantations alternatives selon certains paramètres
- On peut alors déléguer l'action à réaliser à un objet spécifique (utilisation du pattern Strategy)
Exemple 1 : TVA variable
- On souhaite implanter des articles à vendre sur une boutique en ligne avec des taux des TVA variables
- On calcule le prix TTC de l'article en utilisant le pattern Strategy en ajoutant un champ vers un objet permettant de calculer la TVA
public abstract class BuyableItem { private VATComputer vat; public BuyableItem(VATComputer vat) { this.vat = vat; } public abstract int getRawPrice(); public int getTaxIncludedPrice() { return vat.computeTaxIncludedPrice(getRawPrice()); } } public interface VATComputer { public int computeTaxIncludedPrice(int rawPrice); }
Remarque : VATComputer pourrait être une classe Enum si les différents taux de TVA sont fixés une fixés une fois pour toute à la compilation.
Exemple 2 : une voiture hybride
- Nous implantons maintenant une voiture hybride électrique/essence qui circule en mode électrique tant que la capacité de sa batterie le permet et utilise ensuite le moteur à combustion essence.
- La stratégie de propulsion dépend donc de l'état de la batterie : on parle alors de pattern State (variante du pattern Strategy). L'objet change son mode de fonctionnement en choisissant l'objet à qui il déléguera la fonctionnalité de propulsion selon son état.
/** Implementation of an hybrid car using first the electric propulsion then the fuel propulsion if the battery is exhausted */ public class HybridCar { protected PropulsionStrategy propulsionStrategy = PropulsionStrategies.ELECTRIC; /** Drive with the car (distance in meters) */ public double drive(double distance) { double driven = propulsionStrategy.drive(distance); if (driven < distance) if (propulsionStrategy == PropulsionStrategies.ELECTRIC) { // we switch to the fuel propulsion since the battery is exhausted propulsionStrategy = PropulsionStrategies.FUEL; // drive the remaining distance with the fuel tank driven += propulsionStrategy.drive(distance - driven); } return driven; // return the driven distance }
Observateur (observer)
Présentation
- Le pattern Observer est utilisé lorsque l'on souhaite être informé de la survenue d'un événement concernant un objet.
-
Deux objets participent à ce pattern :
- L'observé qui est l'objet créateur d'événements. Cet objet comporte une méthode permettant d'enregistrer un observateur qui sera contacté lors de la survenue d'un événément
-
L'observateur qui s'abonne pour recevoir des événements auprès de l'observé.
- Dans certaines circonstances, on peut autoriser plusieurs observateurs (l'évément est diffusé vers tous les observateurs)
- Ce pattern est très utilisé par les frameworks d'interface graphique (où il est également connu sous le nom de Listener) afin de signaler des événements d'entrée (clic sur un bouton, sélection d'un item, frappe d'une touche...).
-
Ce pattern est au coeur de la communication de composants dans le paradigme Modèle-Vue-Contrôleur
- La vue est un observateur du modèle : lorsque le modèle est mis à jour, celui-ci informe la vue afin que l'affichage soit rafraîchi
- Le contrôleur est un observateur de la vue : lorsqu'une action est réalisée sur la vue (clic, sélection...), la vue communique un événement au contrôleur qui réalise alors des opérations susceptibles de modifier le modèle
- Le langage Java intègre dans sa bibliothèque standard la classe Observable et l'interface Observer permettant la mise en oeuvre de ce pattern.
- Attention à bien désabonner un observateur lorsqu'il n'a plus besoin d'événements de l'observé : en cas d'oubli, l'observateur est toujours référencé par l'observé et cela peut l'empêcher le cas échéant d'être supprimé de la mémoire par le ramasse-miettes.
Exemple : observation de lignes de texte
- Implantons un observateur à l'écoute de lignes de texte de l'entrée standard et retournant ces lignes à l'envers
- Puis écrivons l'observé chargé de lire les lignes de texte et d'envoyer un événément à chaque ligne lue
public class LineReverser implements Observer { /** This method is called by the observable when a new line arrives public void update(Observable o, Object arg) { String reverse = reverse((String)arg); System.out.println(reverse); } public static String reverse(String s) { return new StringBuilder(s).reverse().toString(); } } public class LineGetter extends Observable implements Runnable { private Scanner scanner; public LineGetter(Scanner s) { scanner = s; } public void run() { try { while (scanner.hasNextLine()) { String line = scanner.nextLine(); notifyObservers(line); } } catch (IOException e) { System.err.println("An error occurred: " + e); } finally { try { scanner.close(); } catch (Exception e) {} } } } public class LineMain { public static void main(String[] args) { LineGetter lg = new LineGetter(new Scanner(System.in)); LineReverser lr = new LineReverser(); lg.addObserver(lr); try { lg.run(); } finally { lg.deleteObserver(lr); // do not forget to avoid memory leaks } } }
Chaîne de responsabilité
- La pattern Chain of responsibility permet d'établir une liste d'objets chargés du traitement de requêtes ou de messages.
- Le message est communiqué au premier gestionnaire de traitement qui peut lui-même choisir de le retransmettre au gestionnaire suivant s'il l'estime nécessaire (et ainsi de suite pour les gestionnaires suivants).
Exemple 1 : traitement d'événement pour une interface graphique
- La plupart des frameworks de gestion d'interfaces graphiques introduisent un modèle de composants graphiques hiérarchiques : les composants sont organisés sous la forme d'un arbre en fonction de leur imbrication.
- Par exemple un bouton peut être enfant d'un panneau B, lui-même enfant d'un panneau A, lui-même enfant de la fenêtre globale.
-
Lorsque l'on clique sur le bouton :
- la fenêtre reçoit un message avec l'événement clic et les coordonnées du clic : la fenêtre communique l'événement au panneau A car le clic a lieu dans sa zone de responsabilité
- le panneau A reçoit l'événément clic avec ses coordonnées et constate que celles-ci sont incluses dans le panneau B : il communique l'événement au panneau B
- le panneau B reçoit l'événement et remarque que le clic a lieu sur le bouton : l'événément est transmis au bouton
- finalement le bouton reçoit l'événement et déclenche les actions enregistrées pour cet événement
Exemple 2 : broadcast ordonné sous Android
- Le mécanisme de broadcast permet sous Android de transmettre des messages globalement au sein du système.
- Toute application peut capturer un broadcast ordonné à l'aide d'un BroadcastReceiver (qui peut être vu comme un observateur de broadcast).
- Un broadcast ordonné est un message qui est transmis aux BroadcastReceiver intéressés dans leur ordre de priorité déclaré.
- Un BroadcastReceiver peut traiter le message et s'il le souhaite stopper sa propagation : cela signifie que les BroadcastReceiver de plus faible priorité ne pourront le recevoir.
- Exemple (Android < 19) : lorsqu'un SMS est reçu, un broadcast ordonné est créé et les BroadcastReceiver interessés peuvent traiter le SMS reçu et éventuellement stopper sa propagation (ce qui peut conduire à son élimination silencieuse).
Mediateur (mediator)
- Le pattern Mediator introduit un intermédiaire, le médiateur, entre plusieurs objets pour permettre leur interaction.
- Cela permet de mettre en place des schémas plus complexes de communication comme le passage de messages vers plusieurs objets séquentiellement ou simultanément en testant différentes conditions.
- Les objets expéditeurs et destinataires ne communiquent donc pas directement ce qui réduit leur inter-dépendance.
- On peut aussi utiliser un pattern Mediator pour superviser une chaîne de responsabilité : le médiateur se charge d'envoyer successivement le message provenant d'un producteur d'événement à chaque gestionnaire d'événement (rôle de dispatcher d'événements).
Exemple : un gestionnaire de virements bancaires
- On écrit un médiateur chargé de réaliser des virements d'un compte bancaire à un autre.
- Un compte bancaire ne peut donc pas agir directement sur un autre compte : le médiateur se charge de vérifier que le compte débité dispose de suffisamment de fonds et vérifie si le mouvement est autorisé.
public interface BankAccount { /** Withdraw the given amount and return the new balance of the account */ public int withdraw(int amount); /** Deposit the given amount and return the new balance */ public int deposit(int amount); } public class BankTransferer { private TransferAuthorizer authorizer; // we delegate the authorization for the transfor to a dedicated object public void transfer(BankAccount source, BankAccount destination, int amount) { int balance = source.withdraw(amount); if (balance < 0) { source.deposit(amount); // we recredit the source since the transfer is not possible throw new IllegalStateException("Not enough balance on the source account"); } else { if (authorizer.isAuthorizedTransfer(source, destination, amount)) destination.deposit(amount); else { source.deposit(amount); // we recredit the source since the transfer is not authorized throw new RuntimeException("Forbidden transfer"); } } } }
Memento
- Le pattern Memento permet de mémoriser une liste d'états antérieurs pour un objet.
- Un objet spécifique se charge de conserver la sauvegarde des états (qui peut être réalisée en mémoire centrale ou sur disque).
- On peut sauvegarder les états complets ou mieux, uniquement les différences entre état
- Ceci permet de pouvoir restaurer un état antérieur (possibilité d'annulation d'une action)
- Applications possibles : sauvegarde de l'état d'une interface graphique (avec actions undo et redo), sauvegarde de l'état d'un jeu...
Example : un wiki
- Un wiki est un dépôt de documents versionnés utilisant un langage de balisage pour la mise en forme du texte
- L'état actuel de chaque document et les états actuels sont conservés
public class WikiPage { private final String title; private String content; private Archive archive; private int version = 0; public WikiPage(String title, String content, Archive archive) { this.title = title; this.content = content; this.archive = archive; } public void setContent(String newContent) { if (! content.equals(newContent)) { content = newContent; StringsComparator cmp = new StringsComparator(content, newContent); EditScript<Character> script = cmp.getScript(); archive.postDiff(title, version++, script); } } public void restoreVersion(int wantedVersion) { var editScript = archive.getDiff(version, wantedVersion); // TODO: apply the edit script } }
Commande
- Le pattern Command représente une action à réaliser.
-
Ce pattern est utile lorsqu'une classe doit réaliser de nombreuses actions :
- plutôt que d'implanter beaucoup de méthodes différentes (doTheCoffee(), doTheTea, doTheCake()) ...
- ... on créé une seule méthode prenant en paramètre une instance de commande : executeTheCommand(Command c)
- on définit préalablement l'interface Command avec les méthodes nécessaires
- la méthode executeTheCommand examine la commande et l'exécute
-
Un intérêt majeur est de pouvoir stocker les commandes passées dans une liste pour pouvoir les annuler, les rejouer
- On peut ainsi utiliser la pattern Command en complément ou substitution du pattern Memento (une commande peut être vue comme un delta entre deux états : on a ainsi besoin de stocker l'état initial puis toutes les commandes ultérieures qui représentent incrémentalement les différences avec cet état initial)
- On peut aussi stocker les commandes pour les planifier dans le futur (tâches à réaliser)
Exemple : une interface graphique de dessin
- Une interface de dessin peut proposer différents outils afin de dessiner : un pinceau avec des formes et épaisseurs diverses, un crayon, une bombe aérosol...
- On peut alors créer une classe DrawCommand avec une hiérarchie correspondant aux différents outils (BrushCommand, PencilCommand, SprayCommand..)
- La classe DrawSketch représentant une planche à dessin dispose d'un méthode executeDrawCommand(DrawCommand command) afin de réaliser l'action représentée par la commande. On stocke la commande dans une liste.
- Si un mouvement de dessin ne nous convient pas, il devient alors possible d'annuler les dernières commandes réalisées, jusqu'au mouvement litigieux que l'on supprimera. On réexécutera ensuite tous les mouvements après la mouvement litigieux.
Visiteur
Présentation
- Le pattern Visitor est utilisé pour exécuter du code sur une structure généralement sous la forme d'arbre.
- La structure est visitée par le visiteur et des expressions sont calculées ou des actions réalisées en fonction du type de noeud examiné.
- Ce pattern promeut la séparation entre la structure de données et les actions qui lui sont appliquées. On peut ainsi implanter différents visiteurs s'appliquant sur la même structure.
- Pour les langages avec sélection de méthode à la compilation pour les types des arguments et à l'exécution pour le type de this, on utilise la technique du double-dispatch
Exemple : visiteurs sur expression arithmétiques
- On implante des expressions arithmétiques qui peuvent être des opérations binaires ou alors un entier.
-
On écrit ensuite deux visiteurs :
- L'un est chargé de réaliser le calcul représenté par une expression
- L'autre retourne une chaîne de caractères représentant l'expression au format infixe
- En Java, on est obligé d'utiliser l'astuce du double-dispatch car le langage sélectionne statiquement les méthodes à appeler selon les paramètres passés ; la sélection est par contre dynamique sur l'objet this. On exploite cette particularité en ajoutant une méthode R apply(Visitor<R> v) sur toutes les classes : l'appel de cette méthode permettra de dispatcher l'appel sur la méthode du visiteur correspondant au type réel.
public interface Expr { public <R> R apply(Visitor<R> visitor); } public class IntegerExpr implements Expr { public int number; public IntegerExpr(int number) { this.number = number; } @Override public <R> R apply(Visitor<R> visitor) { return visitor.visit(this); } } public abstract class BinaryExpr implements Expr { public Expr operand1, operand2; public BinaryExpr(Expr operand1, Expr operand2) { this.operand1 = operand1; this.operand2 = operand2; } } public class AdditionExpr extends BinaryExpr { public AdditionExpr(Expr operand1, Expr operand2) { super(operand1, operand2); } @Override public <R> R apply(Visitor<R> visitor) { return visitor.visit(this); } } public class ProductExpr extends BinaryExpr { public ProductExpr(Expr operand1, Expr operand2) { super(operand1, operand2); } @Override public <R> R apply(Visitor<R> visitor) { return visitor.visit(this); } }
/** Interface for visitor * The visitors should implements all the visit method (otherwise an exception could be raised) */ public interface Visitor<R> { public default R visit(Expr expr) { throw new RuntimeException("unimplemented"); } public default R visit(IntegerExpr expr) { throw new RuntimeException("unimplemented"); } public default R visit(BinaryExpr expr) { throw new RuntimeException("unimplemented"); } public default R visit(AdditionExpr expr) { throw new RuntimeException("unimplemented"); } public default R visit(ProductExpr expr) { throw new RuntimeException("unimplemented"); } } /** A visitor to compute the value of the expression */ public class ComputerVisitor extends Visitor<Integer> { public Integer visit(IntegerExpr expr) { return expr.number; } public Integer visit(AdditionExpr expr) { return expr.operand1.apply(this) + expr.operand2.apply(this); } public Integer visit(ProductExpr expr) { return expr.operand1.apply(this) * expr.operand2.apply(this); } } /** A visitor to display the expression using an infix format */ public class InfixDisplayVisitor extends Visitor<String> { public String visit(IntegerExpr expr) { return "" + expr.number; } public String visit(AdditionExpr expr) { return "(" + expr.operand1.apply(this) + "+" + expr.operand2.apply(this) + ")"; } public String visit(ProductExpr expr) { return "(" + expr.operand1.apply(this) + "*" + expr.operand2.apply(this) + ")"; } }
⚠ Il est obligatoire de redéfinir la méthode apply sur tous les types de noeuds (même si son implantation est la même) pour permettre le double-dispatch.