Les designs patterns: Le Strategy Pattern

Introduction

En tant que développeur iOS, je n’ai aucun problème pour faire mon travail au quotidien. Cependant, je me suis rendu compte que lorsque l’on me posait la question « Combien de Design Patterns peux-tu me citer ? » la réponse est « Le singleton… » et… c’est tout… Je suppose que j’en utilise sans même le savoir… Partant de ce constat et afin de m’améliorer dans ma carrière de développeur, j’ai décidé d’entreprendre la lecture du livre : « Head First – Design Patterns ». Pour rendre cette expérience encore plus intéressante, je souhaite partager mon aventure en vous écrivant un article par chapitre.

À la fin de cette série nous aurons vu :

  • Les patterns présentés dans le livre
  • Quand les utiliser, et pourquoi
  • Comment les appliquer
  • Quand ne pas les utiliser (comment éviter la « fièvre des patterns »)
  • Les principes de conception Orienté Objet sur lesquels ils sont basés

Contexte

L’application SimUDuck est un jeu de simulation de mare aux canards. Les concepteurs du système ont utilisé des techniques orientés objets classiques et ont ainsi créé une superclasse Canard dont héritent tous les autres types de canards :

Le jeu connaît un succès international et une nouvelle fonctionnalité est demandée : On souhaite que les canards volent !

Piste 1 : Étendre la classe

La solution

Étant donné que nous avons déjà notre classe Canard, il nous suffit d’étendre la classe avec la nouvelle fonctionnalité demandée :

Conséquences

Seulement, nous rencontrons un problème lors des tests… Tous les canards sont en mesure de voler, y compris ceux qui ne le devraient pas!

Pour corriger le problème, on décide de réécrire (override) la méthode voler() afin qu’il ne puisse plus voler :

Mais alors, que se passe-t-il lorsque nous ajoutons des canards en bois au programme ? Ils ne sont pas censés voler ou caqueter non plus…

Avec cette manière de faire, nous observons plusieurs inconvénients dans notre implémentation :

  • Duplication du code dans les sous-classes : Si le jeu contient 200 canards qui ne volent pas, c’est 200 réécritures de la méthode voler().
  • Les changements de comportement sont difficiles à l’exécution : Comment fait-on si l’on souhaite qu’un canard ne fasse plus le même son lorsque l’application est en cours d’exécution ?
  • Il est difficile de connaître tous les comportements des canards
  • Les changements peuvent affecter involontairement d’autres canards : Notre fameux canard volant.

Piste 2: Créer une interface

Solution

Nous venons de voir que l’héritage n’est pas la bonne solution mais qu’en est-il des interfaces? Et bien ça ne résout pas notre problème non plus !

Conséquences

Nous avons abandonné l’héritage car cela sous-entendait de réécrire les méthodes des canards ne pouvant ni voler ni caqueter. Ici, nous avons le même problème: Lorsque nous aurons besoin de faire un petit changement de comportement dans la méthode voler()… nous devrions changer ce comportement dans les 48 sous-classes de Canard volant !

Si le fait que les sous-classes implémentent les interfaces Volant et/ou Bruyant résout une partie du problème (pas de canards en caoutchouc volant), cela empêche complètement la réutilisation du code pour ces comportements, et crée un autre problème de maintenance…
En plus, il pourrait y avoir plus d’un comportement de vol, même parmi les canards qui volent…

Piste 3 : Le Pattern Strategy

Pour commencer, voici un principe de conception qui va nous aider : Identifiez les aspects de votre application qui varient et séparez-les de ce qui reste identique. Autrement dit : Prenez les parties qui varient et encapsuler-les. De cette manière, vous pourrez les modifier ou les étendre plus tard sans affecter les parties qui ne varient pas.

On sait que voler() et caqueter() sont les parties de la classe Canard qui varient selon les canards. Pour séparer ces comportements, nous allons retirer les deux méthodes de la classe Canard et créer un nouvel ensemble de classes pour représenter chaque comportement :

Maintenant, nous avons deux interfaces, ComportementAuVol et ComportementDuBruit ainsi que les classes correspondantes comportant chaque comportement (leurs implémentations) :

Avec cette conception, d’autres objets pourront réutiliser les comportements de vol et de bruitage car ces comportements ne sont plus dans nos classes Canard. De plus, on peut ajouter de nouveaux comportements sans toucher aux classes existantes. Ainsi, on bénéficie des avantages de la réutilisation en évitant les inconvénients de l’héritage.

Intégration

On ajoute à la classe Canard deux variables : comportementAuVol (de type ComportementAuVol) et comportementDuBruit (de type ComportementDuBruit).
De cette manière, nous pouvons initialiser ces deux variables avec l’une des sous-classes présentées plus haut.
Au niveau des méthodes nous avons remplacé le caqueter() par déclencherLeBruitage() et voler() par déclencherLeVol() car nous avons envoyé ces deux méthodes dans les classes ComportementAuVol et ComportementDuBruit.

Au niveau du code, nous avons:

L’interface ComportementAuVol

L’interface ComportementDuBruit

Notre classe Canard ayant deux variables avec pour type les deux interfaces ci-dessus

Ici, on remarque que lancerLeVol() et lancerLeBruitage() délègue l’implémentation aux classes de comportements. On a également mis un constructeur personnalisé nous permettant de créer notre Canard avec les comportements souhaités. Ce qui nous donne les sous-classes suivantes :

Test de la solution

Au lancement de l’application, nous obtenons bien :

Amélioration

Imaginez maintenant que vous souhaitiez définir le type de comportement du canard par le biais d’une méthode dans la sous-classe du canard, plutôt qu’en l’initialisant dans le constructeur du canard. Cela nous donnerait :

Côté code, nous aurions ces deux nouvelles méthodes dans notre classe Canard:

Et enfin, à l’utilisation :

Au lancement de l’application, nous avons bien :

Vue d’ensemble

Le client utilise une famille encapsulée d’algorithmes pour les comportements. Il faut voir chaque ensemble de comportements comme une famille d’algorithmes.

Chaque canard possède un ComportementAuVol et un ComportementDuBruit. Lorsque l’on initialise une classe avec des classes, cela s’appelle la composition : De cette manière, créer des systèmes nous donne beaucoup plus de flexibilité. Non seulement cela nous permet d’encapsuler une famille d’algorithmes dans leur propre ensemble de classes, mais elle nous permet aussi de changer le comportement au moment de l’exécution aussi longtemps que l’objet avec lequel nous composons implémente la bonne interface. La composition est utilisée dans de nombreux modèles de conception (nous verrons leurs avantages et inconvénients tout au long de cette série).

Conclusion

Voilà, nous venons d’appliquer notre premier design pattern, le pattern STRATEGY. Grâce à ce modèle, le simulateur est prêt à faire face à tous les changements que les dirigeants pourraient imaginer lors de leur prochain voyage d’affaires à Las Vegas.

Maintenant que nous vous avons fait tout ce chemin pour l’appliquer, voici la définition formelle de ce modèle :
Le modèle de stratégie définit une famille d’algorithmes, encapsule chacun d’entre eux et les rend interchangeables. La stratégie permet à l’algorithme de varier indépendamment des clients qui l’utilisent.

Maintenant que nous avons vu ce qu’est précisément le strategy pattern, n’hésitez pas à réagir dans les commentaires ! Je ferai en sorte d’y répondre le plus rapidement possible !

J’espère que cet article t’auras aidé dans le chemin de l’apprentissage des designs patterns 😎

 

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée.

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.