Les design Patterns sont des modèles permettant de résoudre de façon élégante des problèmes rencontrés fréquemment en programmation objet. Nous en étudierons quelques uns, je vous laisserai le soin de consulter de la documentation sur Internet pour vous familiariser avec les autres.
Lorsque la création d’un objet doit être contrôlé, on met l’opérateur
new
en
private
et on en confie l’appel à une
méthode généralement
static
appelée une factory.
Un client possède des commandes, mais une commande n’existe pas sans client.
Lorsqu’une commande est crée sans qu’un client non
null
soit
spécifié, lever une exception peut toujours être une solution, mais elle
présente
l’inconvénient d’alourdir le code. Une solution assez élégante consiste à
réduire la visibilité du constructeur de commande et à créer une méthode non
statique
createCommande
dans client. Cette fonction se charge de
créer une commande dont le client est celui à partir duquel la fonction a été
appelée.
Créez une classe
Commande
et une classe
Client
.
Vous réduirez la visibilité du constructeur de
Commande
et
écrirez une méthode
createCommande()
dans la classe
Client
. Vous pourrez utiliser une inner class.
Le singleton est une classe dont il ne peut exister qu’une seule instance. Il se code en utilisant une factory.
Lorsqu’un programme se connecte à une base de données, le fait qu’il dispose de plusieurs ouvertures simultanées vers la même base peut être une source de bugs très difficiles à trouver. Le singleton empêche l’utilisateur de la classe de créer directement une connexion et contrôle le nombre de connexions.
Créez une classe
Singleton
vide.
Le wrapper est une classe masquant une autre classe. Son but est de d’adapter d’utilisation d’une classe à un besoin, ou plus simplement de cacher la classe que vous avez choisi d’utiliser.
Une matrice s’utilise avec deux indices, les fonctions permettant de manipuler des collections avec plusieurs indices sont peu commodes et source d’erreurs.
Écrire une classe permettant de wrapper un Integer dans une implémentation
de l’interface
Anneau<E>
présentée dans le diagramme ci-dessous.
Écrire une classe permettant de gérer une matrice de type
T
à deux indices. Vous implémenterez les fonctions de somme et de produit
et ferez en sorte que l’on puisse faire des matrices de matrices. Le
diagramme de classes correspondant est présenté ci-dessus.
L’adapteur est l’implémentation - quelques fois vide - la plus simple que l’on peut faire d’une interface. L’avantage qu’elle présente est qu’elle évite au programmeur d’écrire un grand nombre de méthodes vides ou contenant du code répétitif pour implémenter l’interface. L’inconvénient est que comme l’héritage multiple est interdit dans beaucoup de langages, une classe ne peut hériter que d’un adapteur à la fois.
Attention, dans beaucoup de documentations, l’adapteur est assimilé à un wrapper (ce qui si vous y réflechissez bien, est tout à fait pertinent).
Les interfaces comme
MouseMotionListener
contiennent
beaucoup de méthodes et il est quelques fois pénible d’avoir
plein de méthodes vides dans les classes d’implémentation. La classe
abstraite
MouseMotionAdapter
contient une implémentation
vide de
MouseMotionListener
et héritant de cette classe
il est possible de ne redéfinir que les méthodes dont on a besoin.
Lorsque l’on souhaite disposer de plusieurs algorithmes dans un projet et choisir lequel utiliser lors de l’exécution, ou lors de différentes étapes du projet, les diverses modification à opérer peuvent s’avérer particulièrement laides. Le pattern Strategy consiste à définir une classe mère représentant l’opération à effectuer et d’implémenter algorithme dans des classes filles.
Si par exemple, vous souhaitez accéder à des données et que selon la situation le mode d’accès aux données peut changer (fichier, SGBD, accès réseau, etc.). Une façon élégante de procéder est de créer une classe abstraite chargée d’accéder aux données, et de créer une sous-classe par mode d’accès.
Créer une classe chargée de calculer le carré d’un nombre n. Vous implémenterez deux méthodes :
Vous utiliserez une factory pour permettre à l’utilisateur de choisir la méthode de calcul.
Les type abstraits de données comme les
Set
,
Map
ou
encore
Tree
ne dispose pas les éléments dans une ordre aussi
clair qu’un tableau ou une liste. itérateur est un moyen d’exploiter
des collections avec le même code, en les parcourant avec une
boucle
for
.
En java, toute collection héritant de l’interface
Iterable<T>
peut se parcourir avec une boucle de la forme
for (T element : collection) /*...*/
.
Créez un wrapper implémentant
Iterable<T>
et encapsulant un tablau.
Votre itérateur retournera tous les éléments non
null
du tableau.
Dans une classe paramétrée en java, on ne peut pas instancier
un tableau T[], vous êtes obligé de remplacer le tableau par une collection.
Rendre itérable la matrice de l’exercice sur les wrappers. Ne réinventez pas la roue, utilisez l’itérateur fourni avec le type Vector<E>.
Créez une interface
Condition<U>
contenant une méthode
boolean check(U item)
.
Créez une classe filtre
Filtre<T, U>
contenant un
Condition<U>
et permettant de filtrer une sous-classe
T
de
Iterable<U>
. Créez une méthode
filtre(Collection<T> condition)
retournant un
itérable contenant tous les éléments de la
collection
qui vérifient
check
.
Le modèle en couche est souvent problématique lorsque deux classes dans des couches différentes ont besoin d’interagir. L’Observer permet d’éviter les références croisées et de simplifier considérablement le code. Un observer est un objet surveillant un autre objet, il contient une méthode appelée automatiquement lorsqu’une modification intervient sur l’objet surveillé.
Dans les interfaces graphiques en Java, il est possible d’associer aux objets graphiques des
ActionListener
. L’interface
ActionListener
contient une seule méthode,
actionPerformed
qui est appelée automatiquement lorsqu’un utilisateur clique sur
un objet graphique. De cette façon, une classe de la bibliothèque standard de java peut,
sans que vous ayez à la modifier, exécuter du code que vous avez écrit.
Reprenez le tableau creux et créez un observateur qui va afficher toutes les modifications survenant dans le tableau.
Le proxy cache un objet qui est généralement difficile d’accès (via une connexion réseau, une lecture sur un fichier, une base de données, etc.), et a pour but de masquer la complexité de cet accès, ainsi que d’appliquer un lazy loading. On parle de lazy loading si un objet est lu que lorsque quelqu’un le demande.
L’autre avantage du proxy est qu’il permet via des interfaces de changer la méthode de chargement (réseau, base de donnée, etc.).
Si l’accès à un objet nécessite une connexion réseau qui peut prendre du temps, le proxy va le charger une seule fois, et seulement au moment où il sera demandé.
Écrivez une classe qui calcule la somme des nombres de 1 à n avec une boucle. Le calcul ne sera fait que la première fois que la fonction sera appelée et le résultat sera stocké dans un champ. Les fois suivantes, le résultat ne sera pas recalculé.
Le decorator est un moyen élégant d’éviter l’héritage multiple sans pour autant dupliquer de code. Rappelons que l’héritage est en général déconseillé (risque de conflit entre les noms de méthodes), et dans certains langages (Java) interdit.
Un décorateur est un wrapper qui contient un objet dont le comportement est modifié, la puissance de ce pattern vient du fait qu’un décorateur peut contenir un autre décorateur, il est ainsi possible de placer les décorateurs en cascade.
L’avantage du décorateur est qu’il permet de créer des intersections entre sous-classes et de remplacer l’héritage multiple. Les inconvénients sont de taille : le fait qu’une liste chaînée de décorateurs précède un objet est d’une part une source de bugs pour les programmeurs inexpérimentés, et par ailleurs le compilateur ne peut plus contrôler le type des objets de façon aussi précise (nous verrons pourquoi dans les exercices).
Si l’on souhaite représenter des messages affichables en gras, en italique ou en souligné, trois sous-classes ainsi que les multiples combinaisons permettant de recouper ces sous-classes sont nécessaires. En utilisant trois wrappers on obtient la possibilité de les combiner.
Programmez l’exemple précédent.
Implémentez la facturation d’un dessert. Il est possible de choisir des boules de glaces (vanille, fraise et café), à 1 euro chacune, sachant qu’on peut mettre dans la même coupe des parfums différents. L’utilisateur peut aussi ajouter de la chantilly (pour 0.5) et le nappage (sauce caramel ou chocolat) est à 0.75 euros.