Pont (bridge) et mandataire (proxy)
Présentation
- Le pattern Bridge consiste à systématiser l'usage d'interfaces : lorsqu'une classe concrète doit être implantée, nous crééons une interface (ou classe totalement abstraite) ne comportant que les définitions de méthodes. Extérieurement, on ne manipule que le type interface : on n'a jamais accès au type concret.
- Il est possible d'utiliser une Factory ou un Builder pour créer l'objet implantant l'interface (pour éviter d'être en contact avec le type concret lors de la création)
-
Usages du bridge :
- Classiquement lorsque plusieurs classes concrètes peuvent implanter l'interface (le code extérieur ne dépendant pas du type concret mais uniquement d'une seule interface)
-
Pour mettre en oeuvre un système d'appel de méthodes distantes (RMI, Corba, ICE...) :
- La machine cliente a connaissance de l'interface (appelée stub)
- La machine cliente appelle des méthodes via un objet spécial de type proxy implantant cette interface et contactant le serveur
- La machine serveur reçoit l'appel et le distribue à l'objet adéquat (qui implante le stub)
- Le pattern Bridge promeut la bonne pratique consistant à découpler les interfaces de programmation (API) des implantations concrètes. Cela est utile pour la programmation de bibliothèques où l'on expose uniquement les interfaces. Les modifications du code interne de la bibliothèque sont donc facilitées car tant que ces modifications n'impactent pas les interfaces définies, le code externe manipulant la bibliothèque n'a pas à être modifié.
- L'utilisation d'interfaces peut toutefois réduire les performances lors de l'appel de méthodes (la résolution à l'exécution est moins efficace car on ne connaît pas à l'avance l'adresse mémoire de la méthode à appeler : un niveau d'indirection supplémentaire est nécessaire). Le pattern Bridge est donc à réserver pour des appels de méthodes peu fréquents entre des composants étrangers.
Exemple : un point en 2D
Nous souhaitons implanter un point à deux dimensions modifiable. Nous crééons son interface et son implantation :
public interface Point { public void set(int x, int y); public int getX(); public int getY(); }
public class ConcretePoint implements Point { private int x = 0; private int y = 0; public void set(int x, int y) { this.x = x; this.y = y; } public int getX() { return x; } public int getY() { return y; } }
On pourra bien sûr par la suite mettre en oeuvre d'autres types point héritant de l'interface (des points colorés,...).
Cas du mandataire (proxy)
- Le pattern Proxy consiste à intercaler un objet intermédiaire entre l'objet appelant et l'objet appelé.
-
Ce pattern est utilisé pour les systèmes d'appels de méthodes distantes mais peut également être utilisé localement lorsque l'on veut ajouter du code à exécuter à l'entrée ou à la sortie d'une méthode (programmation orientée aspect) :
- Pour gérer l'accès à une méthode (authentification) : on teste si l'objet appelant est autorisé à exécuter la méthode, si c'est le cas le Proxy appelle la méthode
- Pour réaliser des tests de performance (profiling) ou pour journaliser des traces d'appel : on rajoute du code chronométrant le temps d'exécution de la méthode
- Pour modifier des arguments communiqués par l'appelant ou alors pour modifier le résultat communiqué par la méthode appelée
- Généralement l'utilisation d'un pattern Proxy est coûteuse en temps d'exécution : il faut éviter son utilisation pour des méthodes fréquemment appelées
- L'utilisation d'un proxy nécessite l'utilisation d'une interface pour la classe appelée, cette même interface étant implantée par le proxy. L'objet appelant, s'il ne manipule que le type interface n'a pas conscience de l'existence du proxy (utilisation transparente).
- En Java, il existe la classe Proxy permettant la mise en place de façon dynamique du pattern Proxy.
Composite
Présentation
- La pattern Composite permet de représenter une structure arborescente en conservant toujours la même interface commune pour les noeuds internes et les feuilles.
- On peut ainsi appeler les méthodes de l'interface sans se soucier de savoir si on a affaire à un noeud interne ou une feuille.
Exemple : des expressions arithmétiques
- On peut représenter une expression arithmétique sous la forme d'un arbre
- On souhaite proposer une méthode String getExpressionString() permettant d'obtenir l'expression sous la forme d'une chaîne de caractères (avec les opérateurs en position infixe)
La première étape consiste à créer l'interface commune :
public interface Expression { /** Returns the expression as a string with the operators at a suffix position */ public String getExpressionString(); }
On implante maintenant une expression noeud interne (une opération binaire) avec un opérateur binaire et deux enfants :
public class BinaryOperation implements Expression { public char operator; public Expression operand1; public Expression operand2; public BinaryOperation(char operator, Expression operand1, Expression operand2) { this.operator = operator; this.operand1 = operand1; this.operand2 = operand2; } public String getExpressionString() { return String.format("(%s) %c (%s)", operand1.getExpressionString(), operator, operand2.getExpressionString()); } }
Et maintenant nous implantons une expression feuille représentant un entier :
public class IntegerExpression implements Expression { public int value; public IntegerExpression(int value) { this.value = value; } public String getExpressionString() { return "" + value; } }
Mais comment pourrait-on faire si l'on souhaitait disposer également de méthodes traduisant les expressions en utilisant d'autres ordre d'affichage (préfixe, suffixe) ?
- Solution la plus simple : créer de nouvelles méthodes pour l'interface
- Solution plus propre : utiliser plusieurs objets utilisant le pattern visiteur
Interpréteur (interpreter)
- Les arbres utilisant le Pattern composite peuvent être construits programmatiquement ou alors depuis une chaîne de caractères. En reprenant l'exemple précédent, cela consisterait à implanter la méthode inverse de getExpressionString() qui permettrait d'obtenir une expression depuis une chaîne de caractère.
- La conversion d'une chaîne en expression (sous la forme d'un arbre de syntaxe) nécessite l'implantation d'un interpréteur : celui-ci analyse lexicalement et syntaxiquement la chaîne pour construire l'arbre
- Il existe des interpréteurs plus ou moins complexes selon le langage analysé : implanter un interpréteur d'expressions arithmétiques simples est sans aucun doute plus aisée qu'un interpréteur de langage Java ;)
- Pour les cas complexes, on peut s'aider de générateurs automatiques d'interpréteur supportant une grammaire données. En Java, les JFlex et Java Cup peuvent être utiles, SableCC peut générer des arbres de syntaxe...
Itérateur (iterator)
- Le pattern Iterator permet d'exposer une interface permettant d'itérer sur des sous-éléments d'un objet tout en masquant la façon dont les éléments sont stockés.
- L'utilisation typique est pour les listes d'éléments
-
La plupart des langages POO intègrent dans leur API la possibilité de mettre en place un itérateur :
- En Java, une classe proposant un itérateur implante l'interface Iterable<T>
- Une telle classe doit alors implanter la méthode Iterator<T> iterator() pour récupérer un itérateur
- L'itérateur est utilisable avec une boucle for-each ou en utilisant les méthodes boolean hasNext() pour vérifier qu'il existe un élément suivant et T next() pour le récupérer (et avancer à l'élément suivant)
-
La méthode remove() de Iterable<T> enlève de la structure l'élément le plus récemment récupéré avec l'itérateur
- L'implantation par défaut lève une UnsupportedOperationException
-
Beaucoup de structures de données peuvent être itérées
- Toutes les collections (en Java : List, Set, Map peuvent être itérés)
- Les structures de données arborescentes, voire les graphes de façon plus générale (itération par parcours em profondeur, itération par parcours en largeur)
- Un fichier texte peut être lu ligne par ligne (itérateur sur lignes)
- Les documents structurés peuvent être lus avec un itérateurs d'événements (ouverture, fermeture de balise pour XML : API StAX)
- Les enregistrements de base de données peuvent aussi être itérés (variante par curseur)
- ...
- Lorsque l'accès au i-ème élément de la structure (avec i arbitraire) est nécessaire, un itérateur n'est pas adapté
Façade
Présentation
- Le pattern Facade permet la consolidation d'API de différentes provenances en une API plus simple et réalisant des actions de plus haut niveau.
- L'utilisation des APIs de plus bas niveau est donc masquée.
- Il est possible de choisir d'autres APIs de bas niveau sans changer l'API consolidée de haut niveau.
Exemple : envoi d'une photo chiffrée par email
On souhaite écrire une API permettant d'envoyée une photo chiffrée par email, on va pour cela utiliser des APIs que l'on supposera déjà existantes :
- Tout d'abord, nous convertirons la photo de type Picture en type EmailMessage pouvant être envoyé par courrier électronique
- Il faut ensuite chiffrer le message pour le correspondant en utilisant une API de cryptographie
- Et finalement nous utilisons une API permettant d'envoyer le message en utilisant un serveur SMTP
public interface CryptedPictureSender { /** Crypt and send a picture to a given recipient */ public void send(Picture p, String recipient); } /** We use the Builder pattern to create an instance implementing CryptedPictureSender */ public class CryptedPictureSenderBuilder { private String smtpServer; public setSMTPServer(String smtpServer) { this.smtpServer = smtpServer; } public CryptedPictureSender build() { return new ConcreteCryptedPictureSender(smtpServer); } } public class ConcreteCryptedPictureSender { private String smtpServer; public ConcreteCryptedPictureSender(String smtpServer) { this.stmpServer = smtpServer; } public void send(Picture p, String recipient) { EmailMessage message = EmailMessageConverter.getInstance().convert(p); // use a singleron pattern EmailMessage cryptedMessage = EmailMessageCrypter.getInstance().crypt(message, recipient); // Now we send the message SMTPConnection smtp = new SMTPConnection(smtpServer); try { smtp.connect(); smtp.send(cryptedMessage.getBytes(), recipient); } finally { smtp.close(); } } }
Enveloppeur (wrapper)
Présentation
- Un wrapper permet d'étendre une classe en utilisant la délégation plutôt que l'héritage.
- La classe à étendre est encapsulée en tant que champ dans la nouvelle classe.
Exemple : des animaux gourmands
- On suppose que l'on dispose d'une hiérarchie de classe répresentant des animaux (Animal, Mammalian, Lion, Tiger, Cat...). La classe abstraite Animal dispose d'une méthode eat(FoodStore fs) permettant à l'animal de se servir dans un buffet d'aliments.
- Les animaux gourmands se servent deux fois plus que les autres
-
Comment implanter des animaux gourmands ?
- En créant pour chaque type concret (Lion, Tiger, Cat) un sous-classe (HungryLion, HungryTiger, HungryCat) avec redéfinition de la méthode eat() : cela nécessite du travail et si d'autres types concrets étaient créés dans le futur, il ne faudrait pas oublier des sous-classes supplémentaires
- En utilisant l'héritage multiple : difficile en Java (mais possible dans certains langages comme Scala)...
- En rajoutant un champ booléan hungry dans la classe ancêtre abstraite Animal ; mais il faudrait tout de même soit réécrire les méthodes eat() de toutes les classes dérivées ; soit implanter définitivement eat() dans Animal qui ferait appel à une méthode eatImpl() abstraite implantée dans les classes dérivées (pattern Template command).
- En utilisant un wrapper HungryAnimal
public class HungryAnimal implements Eater { private Animal animal; // the wrapped animal public HungryAnimal(Animal animal) { this.animal = animal; } public void eat(FoodStore fs) { // the animal eat twice because he is hungry! animal.eat(fs); animal.eat(fs); } }