pdf - e-book - archive - github.com

2.2  Design patterns (En construction)

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.

2.2.1  Factory

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.

Exemple

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.

Exercice 1 - Clients et commandes

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.

2.2.2  Singleton

Le singleton est une classe dont il ne peut exister qu’une seule instance. Il se code en utilisant une factory.

Exemple

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.

Exercice 2 - Singleton

Créez une classe Singleton vide.

2.2.3  Wrapper

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.

Exemple

Une matrice s’utilise avec deux indices, les fonctions permettant de manipuler des collections avec plusieurs indices sont peu commodes et source d’erreurs.

Exercice 3 - Integer trafiqué

É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.

Exercice 4 - Matrice

É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.

2.2.4  Adapter

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).

Exemple

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.

2.2.5  Strategy

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.

Exemple

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.

Exercice 5 - Carré

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.

2.2.6  Iterator

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.

Exemple

En java, toute collection héritant de l’interface Iterable<T> peut se parcourir avec une boucle de la forme for (T element : collection) /*...*/.

Exercice 6 - Tableau creux

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.

Exercice 7 - Matrice itérable

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>.

Exercice 8 - Filtre

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.

2.2.7  Observer

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é.

Exemple

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.

Exercice 9 - Surveillance

Reprenez le tableau creux et créez un observateur qui va afficher toutes les modifications survenant dans le tableau.

2.2.8  Proxy

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.).

Exemple

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é.

Exercice 10 - Somme de un à n

É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é.

2.2.9  Decorator

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).

Exemple

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.

Exercice 11 - Messages

Programmez l’exemple précédent.

Exercice 12 - Choix de dessert

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.