PROFDINFO.COM

Votre enseignant d'informatique en ligne

Les patrons de conception

Les patrons de type Structure

Ces patrons simplifient le design en apportant une façon simple de réaliser des relations entre entités. Autrement dit, ils permettent un accès facile aux objets, fournissant souvent une interface simplifiée. Nous verrons aujourd'hui 5 des 6 patrons de Structure du document de Joan-Sébastien Morales.

L'adaptateur (Adapter)

L'adaptateur, comme son nom l'indique, permet d'adapter une classe à une interface complexe ou désuète à un usage plus moderne, plus simple ou repensé.

Dans l'exemple du document, on a une classe ChemicalDatabank, un objet qui ne sert qu'à fournir certaines informations (point d'ébullition, de congélation, formule chimique et poids moléculaire) sur différentes substances (ici l'eau, l'alcool et le benzène). On est en mesure de supposer que ce design date un peu, a simplement été un peu mal conçu dès le départ ou encore satisfaisait certains besoins qui ont maintenant changé.

On voudrait plutôt pouvoir créer des objets ChemicalCompound (substance chimique) qui contiendraient en eux-mêmes toutes les informations pertinentes. On ne veut toutefois pas tout recommencer à zéro vu l'ampleur (simulée!) des travaux requis. On décidera donc d'ajouter un adaptateur au ChemicalDatabank, qui sera en mesure de produire des ChemicalCompounds.

Cet adaptateur est représenté par la classe Compound (elle-même un dérivé de ChemicalCompound). Un Compound est donc un ChemicalCompound, qui contient différents attributs sur la substance chimique qu'il représente, ainsi que des propriétés pour y accéder. De plus, il contient une instance de ChemicalDatabank qu'il utilisera pour se créer. En effet, le constructeur de Compound, en plus d'appeler celui de ChemicalCompound (bien entendu), instancie un objet ChemicalDatabank puis l'interroge pour obtenir toutes les informations nécessaires à sa construction. Après coup, le ChemicalDatabank ne sera plus utilisé puisque le Compound contiendra en lui tout ce qu'il a besoin - un design objet bien supérieur.

De plus, le Compound ajoute une méthode Display() qui lui permet simplement d'afficher tous ses attributs.

Ainsi, le client n'aura qu'à traiter qu'avec Compound, qu'il instanciera en lui passant un nom de substance. Après coup il pourra aller consulter ses attributs directement. La Databank est encore présente et utilisée, mais jamais directement par le client.

Deux petites questions en passant:

La composition (Composite)

Ce patron permet d'implémenter aisément une structure d'arbre grossière pour des objets quelconques, en faisant abstraction de leur état de feuille ou de noeud interne. La structure ainsi formée ne représente pas un arbre parfait, mais en simule un de façon simple pour le client.

La classe Component est l'abstraction de tous les composants de l'arbre, noeuds ou feuilles. Elle déclare l'interface pour les objets dans la composition.

La classe Leaf représente une primitive, un objet qui ne peut pas en contenir d'autres (donc une feuille).

La classe Composite représente un noeud, un objet qui peut en contenir d'autres (un nombre indéterminé). Elle réimplémente les méthodes permettant d'ajouter, d'enlever et d'atteindre des enfants et ces enfants sont eux-mêmes des Components.

L'exemple fourni permet de stocker dans un arbre simulé des éléments graphiques, eux-mêmes pouvant en contenir d'autres (comme par exemple une image qui peut être composée de plusieurs formes, ou une forme de plusieurs lignes).

Le CompositeElement est l'abstraction, Le PrimitiveElement un élément "feuille", qui n'en contient pas d'autre, donc un élément de base non-décomposable. Le CompositeElement est un élément complexe, contenant d'autres éléments (primitifs ou complexes).

Ce patron permet donc de créer un arbre fonctionnel et efficace lorsqu'aucune notion de tri est nécessaire, sans garder la complexité inhérente aux arbres "standards".

La composition est intéressante à utiliser dans un cas où une application utilise plusieurs objets de la même façon, contenant chacun du code quasi-identique. La composition peut également être utilisée si on veut que le client puisse ignorer la différence entre une composition et une primitive.

Le décorateur (Decorator)

Le décorateur permet d'ajouter des fonctionnalités à une classe sans la modifier, ni utiliser la dérivation - c'est ni plus ni moins un enrobage autour d'une classe.

Il est évident que l'on pourrait toujours obtenir des résultats similaires à ceux obtenus par le décorateur en créant des classes dérivées de notre classe de base, héritant de toutes ses fonctionnalités et en ajoutant de nouvelles. Toutefois il y a des cas où l'explosion du nombre de classes dans le design serait trop importante pour être aisément gérée. Dans ces cas-là, le décorateur est fort utile.

Prenons un exemple pour s'en convaincre. Supposons que nous avons une classe Fenêtre qui représente une fenêtre minimaliste dans notre système d'exploitation préféré. On se rend compte que plusieurs Fenêtres sont trop petites pour le contenu qu'elles ont à afficher et on décide donc d'implémenter des barres de défilement. Il est possible qu'on ne puisse pas modifier la classe Fenêtre, ou qu'on décide de ne pas le faire de toute façon à cause de l'overhead impliqué puisque plusieurs fenêtres n'auront pas besoin de ces barres. On peut alors décider de créer une classe dérivée, FenêtreÀBarres.

Plus tard on décide de créer des fenêtres redimensionnables. Pour les mêmes raisons que tout à l'heure, on opte pour la dérivation. Ce coup-ci par contre, problème: que fait-on avec les FenêtresÀBarres? On se retrouve avec des Fenêtres, des FenêtresRedimensionnables, des FenêtresÀBarres et des FenêtresÀBarresRedimensionnables... Le nombre de classes grandit vite, sans aucune bonne raison.

Le décorateur règle le problème en fournissant un enrobage "ÀBarres" à une Fenêtre (qu'on lui passe à l'instanciation). On peut donc décider, à l'exécution et non à la compilation que certaines fenêtres seront munies de barres de défilement. Un autre décorateur "Redimensionnable" acceptera d'enrober soit une Fenêtre, soit un ÀBarre contenant déjà une fenêtre... Une classe de base, une classe décorateur par fonctionnalité et le tour est joué.

Le diagramme UML démontre bien ce principe: un composant (la classe "de base" à laquelle on voudra ajouter des fonctionnalités), abstrait, duquel dérive un composant concret (le tout simplement pour permettre au décorateur d'accepter plusieurs composants concrets). Un décorateur abstrait, contenant un composant, duquel dérivent des décorateurs concrets différents, permettant d'ajouter une fonctionnalité ou un état.

L'exemple du document implémente un LibraryItem (abstraits), duquel dérivent Book et Video. Un Decorator abstrait contient un LibraryItem. Borrowable dérive du Decorator et permet d'ajouter la fonctionnalité d'emprunt aux items voulus.

La façade (Façade)

La façade regroupe l'ensemble des interfaces d'un système en une interface unifiée, simple et conviviale. On l'utilisera souvent dans une librairie, de façon à rendre la librairie plus facile à utiliser ou encore pour rendre utilisable un API mal conçu.

La façade peut être comparée à l'adaptateur, toutefois il y a une différence: l'adaptateur change l'interface d'un objet et supporte le polymorphisme, tandis que la façade fournit une interface unifiée pour plusieurs objets. On ne fera pas de dérivés de la façade et il n'y a aucune abstraction d'impliquée.

L'exemple du document montre plusieurs objets de type bancaire, Bank, Credit et Loan qui permettent de vérifier l'épargne, le crédit et l'état des prêts d'un client. De plus, une classe Client représente un client de la banuqe. La façade est représentée par MortgageApplication (application hypothécaire), qui enrobe le tout et présente une interface simple et unique pour vérifier les différents aspects nécessaires à l'obtention d'une hypothèque.

La procuration (Proxy)

Le proxy fournit une interface qui permet d'accéder à quelque chose, que ce soit une connexion réseau, un gros objet en mémoire, un fichier ou toute autre ressource externe difficile à dupliquer.

L'exemple qui est fourni dans le document utilise des notions d'assemblée et de domaine d'application. Un domaine d'application (représenté par des objets AppDomain) sont deux zones différentes du système, dans lesquelles sont exécutées des programmes. On les utilisera pour des raisons de sécurité (par exemple lorsque certaines tâches ne doivent pas partager de données) ou de stabilité (par exemple lorsque certaines tâches instables risquent de devoir être déchargées de la mémoire sans affecter le processus initial). Il ne faut pas confondre threads et domaines, plusieurs threads peuvent appartenir au même domaine.

On peut donc créer un domaine et l'affecter à une instance de AppDomain grâce à la méthode statique AppDomain.CreateDomain. On peut y créer un objet avec CreateInstance, qu'on manipulera grâce à un ObjectHandle. L'ObjectHandle contient un objet d'un autre domaine, avec un enrobage utilisé pour passer l'objet d'un domaine à l'autre. La méthode Unwrap de l'ObjectHandle permet de retirer l'enrobage pour obtenir l'objet lui-même.

Lorsqu'un objet est déclaré de la façon habituelle, c'est une copie de l'objet qui sera passée d'un domaine à l'autre, et non l'objet lui-même. La conséquence de cela est que la copie de l'objet n'a pas accès aux données de son domaine d'origine puisqu'elle se retrouve alors dans un nouveau domaine. Par contre, si une classe dérive de MarshalByRefObject, elle sera alors "marshalée" (le terme officiel qui signifie "passée d'un domaine à un autre") par référence.

On a donc ici une interface IMath qui est une abstraction des fonctionnalités mathématiques voulues. Une classe Math implémente cette interface et dérive en plus de MarshalByRefObject, lui permettant d'être passée par référence d'un domaine à un autre (même si les fonctionnalités qu'elle implémentent n'en ont aucun besoin, on fait semblant).

La classe MathProxy nous fournira une interface pour intéragir avec un objet Math se trouvant dans un autre domaine - et c'est là l'utilité d'un proxy. Un proxy fournit une interface pour interagir avec n'importe quoi qui est difficile à atteindre.

Au constructeur, MathProxy crée un nouveau domaine, crée une instance de Math dans ce domaine et garde un ObjectHandle sur cette instance. Il enlève ensuite l'enrobage sur le Handle, résultant en une référence sur l'objet Math à travers les domaine.

Les méthodes de MathProxy appellent simplement les méthodes équivalentes de l'objet Math.

Notez que le MathProxy dérive lui aussi de IMath, nous assurant qu'il implémentera les mêmes fonctionnalités et nous permettant de l'utiliser comme si c'était un Math. Quelle beauté.