Design patterns
Il s'agit de quelques notes pour me souvenir des Design Patterns, tels que présentés dans le livre de la collection "Tête la première". Je les complèterai au fil de ma lecture.
Principes objet
- Abstraction
- Encapsulation
- Polymorphisme
- Héritage
Bonnes pratiques Objet
- Encapsulez ce qui varie.
- Préférez la composition à l'héritage.
- Programmez des interfaces, non des implémentations.
- Efforcez-vous de coupler faiblement les objets qui interagissent.
- Les classes doivent être ouvertes à l'extension mais fermées à la modification
- Principe d'inversion des dépendances : dépendez des abstractions. Ne dépendez pas des classes concrètes.
Design patterns
Stratégie
Il définit une famille d'algorithmes, encapsule chacun d'eux et les rend interchangeables. Il permet à l'algorithme de varier indépendamment des clients qui l'utilisent.
L'exemple du livre est celui du simulateur de mare aux canards. On souhaite ajouter la possibilité de voler, mais pour certains canards seulement. On choisit alors de créer une interface "ComportementVol", et des classes concrètes implémentant les différents comportements de vol voulus. Il suffit ensuite que la classe générique Canard ait, parmi ses attributs, une référence à l'interface "ComportementVol". Ensuite, chaque classe héritant de Canard aura la possibilité de choisir son comportement. En ajoutant un "setter" à l'attribut comportement, on a même la possibilité de changer celui-ci dynamiquement.
Observateur
Il définit une relation entre objets de type un-à-plusieurs, de façon que, lorsqu'un objet change d'état, tous ceux qui en dépendent en soient notifiés et soient mis à jour automatiquemen.
L'exemple du livre est celui de la station de météo ayant un objet DonnéesMétéo. On souhaite réaliser plusieurs affichages différents (prévision, actuel, statistiques) Ceux-ci doivent être mis à jour instantanément lorsque les données météo changent.
Il suffit donc de créer une interface Sujet et une interface Observateur. Le Sujet a 3 méthodes principales : ajouterObservateur, supprimerObservateur, et notifierObservateurs. L'Observateur n'a qu'une seule méthode : actualiser. L'objet DonnéesMétéo implémente l'interface Sujet, et donc les 3 méthodes. L'objet AffichageMétéo implémente l'interface Observateur (et sa méthode) Lors de son instanciation, il faut penser à notifier DonnéesMétéo de l'existence de cet observateur.
Décorateur
Il attache des responsabilités supplémentaires à un objet de façon dynamique. Il permet une solution alternative pratique à la dérivation pour étendre les fonctionnalités.
L'exemple du livre est le menu d'un café. Chaque type de café dérive d'une classe abstraite Boisson et a deux méthodes principales : getDescription, et calculePrix. On souhaite pouvoir ajouter des ingrédients (chocolat, caramel, etc.) et fournir, pour chaque commande, une description complète (café et ingrédients), ainsi que le prix total.
On crée donc une classe abstraite DecorateurBoisson qui étend la classe abstraite Boisson, puis des "décorateurs concrets" Chocolat, Caramel, etc. qui ont 2 caractéristiques principales :
- une référence à une boisson (la classe abstraite Boisson), référence définie au moment de l'instanciation de la classe (dans le constructeur donc)
- la méthode calculePrix renvoie la somme du prix de l'ingrédient et du prix de la Boisson (obtenu par la méthode calculePrix de la boisson)
- la méthode getDescription renvoie la concaténation de la description de la boisson et du nom de l'ingrédient
Les classes décorateurs refllètent le type du composant qu'elles décorent
Fabrique
Il définit une interface pour la création d'un objet, mais en laissant aux sous-classes le choix des classes à instancier. Fabrication permet à une classe de déléguer l'instanciation à des sous-classes.
L'exemple du livre est une pizzeria ayant deux franchises, l'une basée à Brest, l'autre à Strasbourg. Chacune des pizzérias a une façon différente de préparer la pizza, mais procède de même pour le reste (découper, emballer, expédier).
La solution est donc d'avoir une classe abstraite Pizzeria ayant une méthode abstraite créerPizza, et une méthode concrète commanderPizza. Ensuite, on crée deux classes concrètes PizzeriaBrest et PizzeriaStrasbourg, implémentant différemment la méthode créerPizza.
Notons bien que la méthode concrète commanderPizza ne connait pas le type concret de Pizza créé ! On utilise un type abstrait Pizza, qui est ensuite sous classé en types concrets dépendant de la pizzéria.
Bien entendu, les Pizza dépendent de l'origine géographique, et leur création dépendra de la classe concrète PizzeriaBrest ou Strasbourg !
Ce pattern s'appuie donc sur l'héritage.
Fabrique abstraite
Il fournit une interface pour créer des familles d'objets apparentés ou dépendants sans avoir à spécifier leurs classes concrètes.
L'exemple du livre continue avec les pizzérias régionales. Chacune d'entre elle a ses propres ingrédients, de même famille (fromage, légumes, sauce, etc.) mais de nature différente. Ainsi, le fromage utilisé à Strasbourg est la mozzarella, alors qu'il s'agit du reggiano à Brest.
Il suffit de créer une interface FabriqueIngredientsPizza, comportant autant de méthodes qu'il y a d'ingrédients différents (creerSauce, creerLegumes, creerFromage, etc.) Ensuite, on implémente cette interface au sein de fabriques régionales : FabriqueIngredientsPizzaBrest par exemple, avec une implémentation concrète de chacune de ces méthodes.
La classe abstraite Pizza est retravaillée pour intégrer chacun des ingrédients (Fromage, Legumes, Sauce, etc.) et définit une méthode abstraite preparer. Ensuite, on crée les classes concrètes correspondantes aux différentes pizzas, PizzaFromage par exemple. Celle-ci comporte une référence à la FabriqueIngredientsPizza (l'interface !) Sa méthode preparer fait ensuite appel aux méthodes contenues dans la FabriqueIngredientsPizza.
Enfin, la classe concrète PizzeriaBrest (la fabrique de pizzas brestoise) intègre la référence à la fabrique concrète d'ingrédients qui lui est liée : FabriqueIngredientsPizzaBrest. Lorsqu'une pizza brestoise est créée, c'est avec la bonne fabrique conrète d'ingrédients.
Ce pattern d'appuie donc sur la composition ! (par opposition au pattern Fabrique) Ces deux patterns favorisent le couplage faible en réduisant la dépendance de votre application aux classes concrètes.
Singleton
Il garantit qu'une classe n'a qu'une seule instance et fournit un point d'accès de type global à cette instance.
On l'implémente en utilisant un constructeur privé, une variable statique de classe le singleton, et une méthode statique (getInstance) qui se charge de vérifier l'instanciation de la variable statique, et sinon de la créer.
Attention aux applications multithread - on peut se retrouver parfois avec plusieurs singleton !
Commande
Il encapsule une requête comme un objet autorisant ainsi le paramétrage des clients par différentes requêtes, files d'attente et récapitulatifs de requêtes, et de plus, permettant la réversibilité des opérations.
Il faut 4 classes et 1 interface :
- une classe Client, responsable de la création de CommandeConcrete et de la définition de son Recepteur
- une classe Invocateur, qui contient la commande. Il demande ensuite à la commande de satisfaire une requête en appelant sa méthode executer()
- une interface Commande, ayant une méthode executer() et une méthode annuler() (si besoin)
- une classe Recepteur qui sait comment effectuer le travail nécessaire pour répondre à la requête
- une classe CommandeConcrete qui définit une liaison entre une action et un Recepteur. L'Invocateur émet une requête en appelant executer() et la CommandeConcrete y répond en appelant une ou plusieurs actions sur le Recepteur.
L'exemple du livre est l'implémentation d'une Telecommande universelle, dotée de 7 emplacements. Des fabricants d'objet ont développé des API pour utiliser leurs objets, et nous devons donc faire en sorte de pouvoir lier chaque emplacement de la télécommande avec une méthode d'un objet.
On voit qu'ici la classe Client est la Telecommande, la classe invocateur est le "chargeur de télécommande" (qui contient le lien entre un emplacement de la télécommande et la commande), la classe Lampe (par exemple) est la classe Recepteur, et les classes CommandeAllumerLampe et CommandeEteindreLampe sont les CommandeConcrete.