[DUMD 7/24] C'est quoi les design pattern ?

Auteur du billet de blog : Nicolas Hilaire - Neotech Solutions

Nicolas Hilaire

Consultant .NET
  Publié le vendredi 4 novembre 2016

Artisan logiciel particulièrement intéressé par les technologies .NET. Polyvalent et curieux, je suis néanmoins à l'écoute des autres technologies du marché. MVP (Microsoft Most Valuable Professional) de 2007 à 2014, je suis également auteur d'un ouvrage pour apprendre le C#, à destination des débutants et de plusieurs MOOCs sur le C#, Windows Phone ou ASP.NET MVC...

Septième billet sur comment devenir un meilleur développeur. Pour retrouver le sommaire ainsi que tous les liens, rendez-vous sur le premier billet.

Un patron de conception (en anglais « design pattern ») constitue une solution éprouvée et reconnue comme une bonne pratique à un problème récurrent dans la conception d’applications informatiques. En général, les design patterns décrivent une modélisation de classes utilisées pour résoudre un problème précis. Ils servent aussi de vocabulaire entre les développeurs et les architectes qui leur permettent d'exprimer des concepts avancés sans avoir à décrire tout le contenu.

Les plus classiques ont été formalisés et popularisés  dans le livre du « Gang of Four » (GoF, Erich Gamma, Richard Helm, Ralph Johnson et John Vlissides) intitulé Design Patterns – Elements of Reusable Object-Oriented Software en 1995. Il s'agit d'un ouvrage de référence, un peu difficile d'accès ; il existe beaucoup d'autres éléments de littérature qui décrivent les designs patterns de manière plus ou moins réussie, mais l'ouvrage du GoF reste la référence.

Connaître les designs pattern est important dans la carrière d'un développeur. Pas forcément les connaître tous par cœur, mais au moins savoir que des solutions existent pour des problèmes classiques que vous retrouverez régulièrement. Il est important également d'avoir une idée des intentions de chacun afin de pouvoir communiquer plus facilement avec d'autres experts.

Il existe beaucoup de patrons de conceptions, je ne vais pas tous vous les présenter, seulement ceux qui me paraissent indispensables à connaître. J'espère que votre curiosité vous poussera à découvrir régulièrement de nouveaux patrons de conception.

Mon objectif premier est que vous puissiez comprendre les concepts inhérents aux design pattern, qui sont une force des développeurs orientés objet.

 

Programmer une interface et non une implémentation

Il s'agit d'un principe de conception fondamental que l'on retrouve dans les design pattern et qui est mis en avant par le GoF. Il est primordial à comprendre et à utiliser dans vos développements.

Nous en avons déjà un peu parlé finalement dans le billet de rappels sur l'orienté objet ; et on en revient à la gestion des dépendances qui doivent être correctement gérées dans les applications.

Programmer une interface revient à dire que l'on s'occupe de ce qui est fait et pas de comment cela est fait. Lorsque l'on suit le principe d'encapsulation, ce qui est important c'est l'interface, dans le sens de ce qui est fait. Comment cela est géré en interne, on s'en moque et c'est justement le principe de l'encapsulation. C'est un peu comme l'utilisation d'un tri de liste, d'un tableau ou de quoi que ce soit d'autre ; j'ai besoin de cette fonctionnalité, et je me moque d'où elle vient et de comment elle est faite.

Rappelez vous notre exemple de calcul de panier, nous avons juste besoin d'un truc qui sait calculer le montant du panier. Peu importe ce que c'est, donc pour l'instant mettons ça dans une classe :

 

 

Et il n'y a plus qu'à l'utiliser dans les autres classes, par exemple dans la méthode AfficheLePanier de la classe Panier :

 

 

Les lignes importantes sont les lignes 9 et 10 où l'on crée la classe de calcul et où on réalise le calcul.

Cela fonctionne, mais on peut mieux faire. Ici, la méthode d'affichage du panier est complètement liée au calculateur de montant de panier. On dit qu'elle est couplée. Ce qui fait que si le calculateur doit changer ou s'il doit évoluer, la méthode AfficheLePanier va aussi devoir évoluer. Par exemple, si nous sommes en période de soldes, pourquoi devoir changer la méthode d'affichage du panier juste parce nous devons utiliser une autre façon de calculer ?

En développement logiciel, on n'aime pas beaucoup modifier plusieurs choses qui dépendent entre elles, car on risque d'avoir des bugs dûs à la fragilité de notre code.

On voit bien que dans cet exemple, nous sommes liés à l'implémentation et non à un comportement. Pour utiliser un comportement, on doit passer par une abstraction. On peut par exemple utiliser une interface pour cela :

 

 

Cette interface contient le comportement de calcul et peut être implémentée par la classe CalculateurDePanier :

 

 

Mais elle pourra aussi être implémentée par un autre calculateur, celui des soldes par exemples :

 

 

Implémentation un peu naïve je le conçois, mais peu importe. Ce qui est important c'est que notre classe Panier (ou la classe Commande d'ailleurs) va désormais utiliser ce comportement de calcul, sans avoir à se soucier de l'implémentation qu'il y a derrière :

 

 

On passe le calculateur en paramètre du constructeur et il est sauvegardé dans une variable privée pour pouvoir être réutilisé dans le reste de la classe.

Ici, le panier ne sait rien du calcul en lui même, tout ce qu'il sait c'est qu'il peut faire un calcul car les implémentations s'engagent à implémenter la méthode de calcul.

L'interface est un contrat.


On pourra par exemple créer le panier de ces façons :

 

 

en fonction du contexte qui nous intéresse. Et même pourquoi pas changer dynamiquement de calculateur, par exemple en fournissant à chaque fois le calculateur en paramètre de la méthode AfficherLePanier ou en permettant de modifier à la volée la variable _calculateur, via un mutateur.

Mais peu importe, ce qui compte c'est que la classe Panier utilise un comportement abstrait et non une implémentation concrète.

 

Utiliser une abstraction permettra également à votre classe Panier de pouvoir être testée indépendamment de la classe de calcul.


Interface ou classe abstraite ?

J'ai utilisé ici une interface pour réaliser l'abstraction, mais j'aurais également pu utiliser une classe abstraite :

 

 

Et la classe Panier aurait été :

 

 

Le principe est le même, on dépend ici d'une abstraction.

 

Que choisir entre une interface et une classe abstraite ?


L'intérêt d'une classe abstraite est qu'elle est en mesure de faire un peu plus qu'une interface ; par exemple si la classe n'est pas abstraite pure, vous pouvez partager des comportements réutilisables dans les classes dérivées. Mais ce n'est pas forcément toujours une bonne chose comme on l'a déjà vu. De plus, partager un comportement quand les classes n'ont absolument rien à voir entre elles ne peut raisonnablement pas se faire avec une classe abstraite. Imaginez une classe représentant une Augmentation et une autre représentant une Glace. Les deux classes partagent le fait de procurer du plaisir à une classe Personne, mais il n'y a aucune relation entre une glace et une augmentation (sauf si vous êtes payés en glace ! ^^).

Et puis avec certains langages orienté objet, comme le C# ou le Java, il n'est pas possible de faire de l'héritage multiple de classe, on va vite se retrouver bloqué lorsqu'on voudra que notre Personne implémente d'autres comportements.

Donc, j'aurai tendance à vous encourager à :

 

utiliser plutôt des interfaces pour programmer une abstraction.

 


Utilisez la composition plutôt que l'héritage

Dans la même optique, vous pouvez afficher ce principe sur le mur à côté de vous. La composition et l'héritage sont deux techniques qui permettent de réutiliser des fonctionnalités, mais la composition va vous offrir plus de souplesse que vous découvrirez au fur et à mesure. Même si l'héritage semble plus simple à utiliser, le simple fait de ne pas pouvoir changer de comportement au runtime est pénalisant.

Mais vous devez déjà le sentir naturellement j'imagine. Préféreriez-vous utiliser cette façon de loguer - à base d'héritage :

 

 

ou bien ceci - à base de composition :

 

De plus, vous pourrez mieux gérer vos dépendances avec de la composition qu'avec de l'héritage.

Nous verrons dans des futurs billets des patrons de conception qui utilisent l'héritage. Je ne dis pas que l'héritage n'est pas bon, soyez juste bien sûrs que l'héritage est la bonne réponse à votre problème (et bien souvent, la réponse sera non).


Formalisme d'un patron de conception

La description d'un patron de conception suit un formalisme fixe (wikipédia) :

  • Nom
  • Description du problème à résoudre
  • Description de la solution : les éléments de la solution, avec leurs relations. La solution est appelée patron de conception
  • Conséquences : résultats issus de la solution.

Ce formalisme aide à comprendre l'intention du patron de conception.

 

Familles de patron de conception

Il existe trois familles de patrons de conception selon leur utilisation (wikipédia) :

  • de construction : ils définissent comment faire l'instanciation et la configuration des classes et des objets
  • structuraux : ils définissent comment organiser les classes d'un programme dans une structure plus large (séparant l'interface de l'implémentation)
  • comportementaux : ils définissent comment organiser les objets pour que ceux-ci collaborent (distribution des responsabilités) et expliquent le fonctionnement des algorithmes impliqués.


Cela permet facilement de les classifier, mais ce n'est pas grave si vous ne retenez pas tout par cœur :).

 

Attention à la patternite aigue

Plus vous allez apprendre des patrons de conception et plus vous allez vouloir les caser partout.  Malheureusement, même là où il ne faudrait pas. Il faut pas mal d'expérience pour savoir utiliser le bon pattern au bon endroit sans que cela ne soit surdimensionné ou simplement que cela ne réponde pas au besoin.

Au début, on se sent intelligent lorsqu'on dit qu'on a mis en place le pattern mediator dans la factory du proxy visitor, mais n'oubliez pas un des premiers principes que l'on a vu : KISS.

Gardez les choses simples. Alors oui, les patrons de conceptions sont des solutions éprouvées, mais pas partout. Parfois cela revient quand même à sortir le bazooka pour écraser une mouche. Ayez l’œil averti et critique.

 

Conclusion

Les patrons de conception, c'est du bon, mangez-en. Mais surtout, il faut les pratiquer, les connaître, s'entraîner, y revenir plus tard pour se rendre compte que finalement on n'avait pas tout compris. Alors, oui, vous allez faire des erreurs. Oui, vous allez sûrement vous tromper et mettre un pattern stratégie à la place du pattern commande.

Mais ce n'est pas grave. C'est en développant que l'on devient meilleur développeur.

Il y a beaucoup d'exemples de design pattern sur internet, un bon point de départ est comme bien souvent wikipédia, mais certains exemples ne sont pas les meilleurs pour comprendre les patrons de conception. N'hésitez pas à diversifier les sources et à lire plusieurs exemples différents pour bien les assimiler et de voir différents cas d'usages.

 

Prochain billet : le design pattern Decorator