Design Pattern : Observer

Nextoo
Nextoo
Jul 27, 2017 · 12 min read

Je vous propose au travers de ce document, de découvrir ou de redécouvrir l’un des design patterns les plus connus et les plus utilisés qui n’est autre qu’Observer (à prononcer à l’anglaise, sinon vous avez l’air bête !)

Nous verrons donc durant cette lecture :

  1. Un rappel sur ce que sont les design patterns
  2. En quoi consiste le design pattern Observer et dans quel cas l’utiliser ?
  3. Comment s’organise la solution ?
  4. Une application de ce concept avec Angular 4 et RxJS

1. Mais c’est quoi les design patterns ?

Derrière ce nom barbare, ce cache des solutions à des problèmes récurrents de développement orienté objet. Si on veut résumer le principe rapidement : Utiliser la roue plutôt que de la ré-inventer. L’idée est plutôt bonne puisqu’elle fait gagner du temps à tout le monde :

  • Au développeur : il n’a pas trop à se casser la tête, d’autres ce sont déjà cassés la tête avant lui.
  • Lors de la maintenance : ce sont des principes bien connus, ils sont robustes et efficaces, peu de chance que ça défaille. Et en plus, ils sont facilement testables.

Vous pouvez également trouver le terme “Patron de conception” pour désigner les design patterns.

On retrouve plusieurs types de design patterns :

  • Création d’objets : Apporte de la flexibilité sur la création d’objets. Notamment quand cela dépend de divers paramètres
  • Structure : Définit une hiérarchie d’objets qui permet de tirer un maximum de profit de l’héritage
  • Comportement : Concernent comment les objets discutent et interagissent entre eux

Remarque :
L’utilisation de ces design patterns peut paraître complexe à comprendre au début, en particulier pour les débutants. Il n’est donc pas rare qu’un problème de conception bien connu ne se voit pas solutionné par le design pattern qui convient, mais par une solution re-développée et donc moins efficace. Cependant, l’objectif n’est pas non plus d’en utiliser à outrance, il faut les utiliser quand nécessaire et trouver un juste milieu entre trop et pas assez.

Le but n’étant pas d’expliquer les design patterns en détail, voyons donc l’un d’entre eux … Observer !

2. Qu’est-ce c’est Observer ? Dans quel cas peut-on l’utiliser ?

On définit un design pattern par le problème qu’il résout. Observer intervient lorsque vous avez beaucoup d’objets qui ont besoin d’être mis à jour à la suite d’une modification d’un autre objet. Dans ce cas vous pouvez à coup sûr appliquer ce patron de conception.

Par exemple, prenons le cas d’un coach d’une équipe de football (Observé / Subject / Publisher) qui donne ses instructions à ses joueurs (Observateurs / Observers / Subscribers). Les joueurs sont “mis à jour” à chaque instructions du coach. L’avantage de ce genre de système c’est que le coach n’est pas obligé de tout savoir sur ses joueurs. Il sait qu’il a des joueurs qui l’écoutent, alors il donne ses instructions et laisse chaque joueurs décider de ce qu’il doit faire de l’instruction. Soit il est concerné et auquel cas, il applique l’instruction. Soit il n’est pas concerné et attend la prochaine instruction.

Petit point vocabulaire, nous avons vu 2 termes qui méritent leurs définitions :

  • Subject : Appelé aussi publisher ou observé, c’est lui qui est à l’origine de l’envoi des informations.
  • Observers : Appelés aussi subscribers ou observateurs, ce sont ceux qui réceptionnent les informations envoyées par le Subject.

Pour ceux qui veulent un exemple plus concret et plus orienté IT, en voici un : Admettons que nous sommes une entreprise d’informatique et nous développons la librairie ultime pour tous les développeurs. Elle est utilisée par des millions de développeurs autour du monde. Nous travaillons depuis quelques jours sur une nouvelle version 4 de notre librairie. Cette version corrige un problème qu’il y a sur la version 3. Les développeurs qui utilisent notre librairie dans sa version 1 et 2, ne sont donc pas impactés par cette nouvelle version. Nous connaissons tous les développeurs qui utilisent nos produits mais ne nous savons pas la version que chacun utilise. Nous, observé, envoyons une notification à tous les développeurs, observateurs, qui indique : “Si vous utilisez la version 3 de notre librairie, vous devez passer à la version 4”. C’est à la charge de chaque développeurs, observateur, de vérifier la version qu’il utilise et de mettre à jour ou non sa librairie.

Normalement avec ces 2 exemples, vous avez bien compris le principe. Au final, c’est un principe assez simple à comprendre pas vrai ?

3. Comment cela s’organise ?

Comme nous l’avons vu plutôt, il nous faut donc 2 entités : un Subject et un Observer. Ensuite, il est nécessaire d'avoir une liste d' Observer dans notre objet Subject, nous verrons un peu plus tard pourquoi. Pour faciliter l'ajout et la suppression d'un Observer dans notre liste, nous allons créer 2 méthodes qui vont gérer cela. On peut imaginer que ces opérations soient soumises à différents contrôles, d'où l'importance de les isoler dans des méthodes. Pour terminer, il nous faut traiter la liste que nous avons construit. Il nous faut donc une méthode qui va parcourir notre liste et va appeler la méthode de mise à jour de chaque Observer.

Voyons ce que cela donne si on décide de le schématiser :

La méthode attach() de la classe Subject permet d'ajouter un Observer à notre liste. La méthode detach() fait l'action opposée, elle retire un Observer de la liste.

La méthode update()de la classe Observer est la méthode qui va s'occuper de traiter les messages qu'envoie le Subject. C'est dans cette méthode que l'on vérifie si l'on est concerné ou non par le message envoyé et que l'on agit en conséquence.

Voici un implémentation basique de la méthode notify() de la classe Subject, elle ne fait rien d'autre qu'appeler la méthode update() de la classe Observer :

La structure est assez simple et basique, mais elle peut évidemment évoluer, notamment s’il a un besoin particulier lors de l’envoi de message au travers de la méthode update(). Effectivement, on pourrait très bien imaginer une méthode de ce genre update(Message message).

Remarque :
Les design patterns apportent un cadre de travail ainsi que de bonnes pratiques, mais rien n’est figé. Il est tout à fait possible de les utiliser pour solutionner un problème et d’en créer une variante qui permet de s’intégrer au mieux à nos besoins de développement.

Bien, maintenant que nous avons une idée claire de ce que sont les design patterns et ce qu’est Observer. Place désormais à la pratique !

4. Application

Angular 4, avec l’aide de RxJS, intègre pleinement ce principe dans sa gestion des flux asynchrones. Et comme nous l’avons vu plutôt c’est une bonne chose de se baser sur ce patron de conception lorsque des traitements dépendent du résultat d’une requête. D’où le choix de ces technologies pour mettre en pratique la théorie que l’on a vue.

Durant cette courte introduction, j’ai parlé de flux asynchrones. Il est donc important d’en faire une petite définition, histoire de fixer les choses. Il s’agit d’un mécanisme non bloquant qui permet d’appeler des fonctions et de poursuivre l’exécution du code sans attendre les réponses de ces fonctions. Une fois que les fonctions en question se terminent, ces dernières appellent une fonction de callback (littéralement, “de rappel” en français) qui s’occupe de traiter le résultat. Si on veut résumer, à chaque fois où on fait appel à une fonction asynchrone, on sait que l’on aura une réponse, on ne sait juste pas quand. Mais le système ne manquera pas de nous informer quand une réponse bonne ou mauvaise est disponible.

Un exemple des plus parlants, c’est l’envoi de SMS. Vous organisez une petite fête, vous envoyés alors un SMS à vos amis en leur demandant s’il veulent venir. Vous pouvez continuer à vivre tant que vos amis n’ont pas tous répondus. Chacun vous répondra quand il le pourra. Certains vont répondre plus vite que d’autres, mais cela ne vous gêne pas.

Que prévoit Angular pour gérer ces flux asynchrones ?

Il existe 2 concepts pour régir ces flux : les promesses (Promises) et les Observables. Pour le second, cela devrait vous rappeler des choses puisqu’il tire son nom du design pattern Observer.

S’il en existe 2 différents, c’est qu’ils ont chacun leurs domaines d’expertise. En effet, il est préférable d’utiliser les promesses lorsque l’on a qu’un seul événement. Typiquement, lorsque l’on envoie une requête GET au serveur, on affiche le résultat et puis c’est tout. Les observables sont surtout utiles dans les cas où l’on veut continuer à envoyer des requêtes même si on n’a pas encore eu les réponses des requêtes précédentes. Par exemple lorsque l’on souhaite créer une requête, l’annuler, et la modifier avant même que le serveur est totalement répondu. Le cas d’une recherche qui se met à jour après quelques secondes où l’on tape dans le champ de recherche est un cas parlant puisque les résultats que l’on attend changent en fonction de notre recherche. Ainsi, il faut pouvoir mettre à jour la requête que l’on envoi au serveur.

Les observables ne sont pas donc à utiliser à chaque fois où on fait un appel au serveur. Par moment, choisir d’utiliser les promesses est plus judicieux.

Remarque :
Il est possible de convertir un observable en une promesse quand nécessaire, via la méthode
toPromise() provenant de RxJS : 'rxjs/add/operator/toPromise'

De part la puissance de leurs opérateurs, les observables restent, dans la majeure partie des cas, ce qu’il faut privilégier. En effet, l’un des avantages, c’est qu’ils peuvent être transformés, combinés, filtrés dans de nouveau observables.

Remarque générale :
Il est important de clarifier tout ces sujets qui tournent autour de l’asynchrone. Ce concept est un gros morceau des récentes versions d’Angular. Il y a beaucoup de choses à voir, et beaucoup de choses que l’on pourrait approfondir. Il n’est pas évident de tout saisir au début, mais une fois tout cela assimilé, vous apprécierez à coup sûr l’utilisation de fonctions asynchrones dans votre application.

Parlons observables !

Angular supporte de manière très basique les observables, c’est la raison pour laquelle, on couple cela avec l’utilisation de la librairie RxJS.

On différencie 2 types d’observables. Il faut différencier les observables Cold et Hot. Pour se faire, je vais reprendre la définition de l’équipe de RxJS (Utiliser la roue plutôt que de la ré-inventer.):

Les cold observables démarrent dès leurs souscriptions à un observable. L’observable va alors commencer à envoyer des données aux observateurs quand la méthode subscribe() est appelée. Ce qui est différent des hot observables, comme les événements de mouvement d'une souris ou les côtes boursières qui produisent déjà des valeurs même si aucune souscription n'a eu lieu. Quand un observateur va souscrire à un hot observable, il va obtenir toutes les valeurs du flux qui sont émises depuis sa souscription.

Sachez qu’il est possible de transformer un cold observable en un hot observable.

Pour plus de détails à ce sujet, je vous invite à lire l’article de Ben Lesh, ingénieur chez Google. Ainsi que ce que nous fourni l’équipe RxJS à ce sujet : Cold Vs. Hot Observables

Comment cela s’utilise ?

Commençons d’abord par une utilisation très basique. Pour cela, reprenons l’un de nos premiers exemples, le coach qui envoi ses instructions à ses joueurs. Nous avons pour cela besoin d’un coach (élémentaire mon cher watson !). Je crée donc un CoachService, son but est d’envoyer des instructions. J’ai décidé pour l’exemple, d’en faire une méthode sendInstructions() qui retourne un Observable<Instruction[]>. Dans cette méthode, on envoi toutes les 2 secondes, notre liste d' instructions. Cette dernière peut être mis à jour grâce à la méthode add(), qui prend en paramètre un message. Nos instructions sont constitués d'un message et d'un booléen pour déterminer si l'instruction concerne ou non la tactique. La méthode se charge de générer aléatoirement ce booléen. L'envoi de données prend fin après 20 secondes.

Voici l’implémentation de mon CoachService :

Nous avons désormais un coach qui émet des instructions, pendant 20 secondes certes, mais pour l’exemple cela sera suffisant. Il nous faut donc des joueurs qui vont écouter attentivement ces instructions. Je décide donc de créer un composant Player, lorsqu'il va s'initialiser, il va alors se mettre à écouter le coach et logger les instructions. Pour cela il faut faire appel à la méthode subscribe() de l'observable retourné par le CoachService. Cette méthode attend 3 paramètres, dans ce cas 3 fonctions de callback. Dans l'ordre, une appelée lorsque tout se passe bien, une appelée en cas d'erreur, une dernière appelée lorsque l'observable nous annonce qu'il est terminé.

Il s’agit là d’une implémentation très basique, mais elle nous a permis de voir comment s’inscrire à un flux de données asynchrone en utilisant les observables.

Se désinscrire

Admettons maintenant que l’un de nos joueurs change de club. Il n’est donc plus nécessaire pour lui d’écouter les instructions du coach. Il faut donc se désinscrire. Reprenons notre méthode ngOnInit(), nous allons nous désinscrire après 10 secondes d'écoute. Il faut simplement faire appel à la méthode unsubscribe().

Cette méthode a pour but de nous faire économiser des ressources. En effet, dans ce cas, si l’observable n’a plus d’observateur, il ne va plus prendre la peine d’émettre quoi que ce soit. Cependant, dans le plupart des cas, il vous sera inutile de faire appel à cette méthode, sauf si vous souhaitez annuler ou couper préalablement une échange. Ce comportement de désinscription est déjà inclus dans les observables, ils vont “résilier” l’abonnement eux-mêmes avec leurs observateurs dès les appels aux fonctions complete() ou error() .

Les requêtes HTTP

Pour le moment, nos requêtes étaient assez simples. Aucun serveur ou API n’était appelé. Le principe est quasiment le même pour les requêtes HTTP, mais il y a quelques petites différences. Prenons l’exemple d’une requête GET à l’aide de la méthode http.get(). Admettons, que les instructions de notre coach sont accessibles sur une API et que l'on peut les récupérer via un simple appel avec une méthode GET. On peut donc imaginer une requête de ce type :

Erreur ! La méthode http.get() ne nous renvoit pas un Observable<Instruction[]>, mais nous renvoit un Observable<Response>. Ce qui change c'est qu'il faut extraire nous-même la donnée depuis la Response. En effet, aucun mapping n'est disponible pour transformer une Response en Instruction[]. En parlant de mapping, c'est la méthode map() qui va nous corriger le problème.

Vous vous souvenez quand je disais que les observables pouvaient être transformés à souhait en utilisant des opérateurs. Et bien on peut aller un peu plus loin, en filtrant uniquement les instructions qui concernent uniquement le plan tactique. C’est grâce à la méthode filter() que l'on peut le faire.

On pourrait continuer ainsi indéfiniment en utilisant toujours plus d’opérateurs, mais ce ne serait pas une bonne idée. C’est faisable, rien nous l’empêche, mais si vous multipliez les opérateurs, il s’agit peut-être d’un problème provenant de votre source de données.

Les opérateurs des observables

Il existent beaucoup d’opérateurs. Il n’est donc pas facile de s’y repérer et d’utiliser l’opérateur qui convient à notre situation. Pour cela, il existe un site internet qui, à l’aide d’un arbre, vous donne l’opérateur à utiliser en fonction de vos besoins : Quel composant utilisé ?

Pour bien comprendre les actions de chaque opérateurs, un site internet explique chaque opérateurs grâce à différents diagrammes. Rendez-vous ici : RxMarbles

J’imagine qu’en plus de cela, vous aimeriez avoir des exemples pour chaque opérateurs. Et bien vous trouverez ici, des exemples commentés et régulièrement mis à jour.

Ceci n’est pas une pipe

Pour terminer ce tour d’horizon des observables avec Angular et RxJS. Voyons l’async pipe.

Admettons que l’on souhaite afficher les instructions de notre coach sur un page web, dans une liste. Reprenons notre joueur de tout à l’heure, et stockons la liste d’instructions reçue depuis le CoachService.

Il nous suffit simplement ensuite de parcourir la liste d’instructions à l’aide d’un *ngFor et d'afficher les instructions à notre utilisateur.

Ce code fonctionne convenablement, mais il existe un petit raccourci grâce à l’async pipe. Il suffit de la placer au niveau de notre *ngFor et elle s'occupera de souscrire à l'observable et de produire un tableau d'instructions. Voyons donc l'impact de cette dernière sur notre code.

On a pu retirer tout notre code de souscription. On laisse la pipe s’en charger.

La liste va ainsi automatiquement se synchroniser avec les instructions que nous envoie le coach.

Conclusion

Les observables sont donc un bon moyen de gérer les flux asynchrones de nos applications. Ils sont puissants et modulables, ce qui nous offrent beaucoup de perspectives dans nos développement. L’apport de RxJS est indispensable. En revanche, il ne faut pas associer RxJS avec Angular. Angular exploite au mieux ce qu’offre RxJS en terme d’observables. Mais il est tout à fait possible d’utiliser cette librairie dans toutes vos applications. Il s’agit peut-être là, d’une première étape intéressante à étudier, en cas de migration de technologie (AngularJS vers Angular, par exemple).

Lecture complémentaire

Il est impossible de tout aborder en un document. Cependant, je vous propose quelques articles, qui méritent de s’y attarder, si jamais le sujet vous intéresse et que vous souhaitez l’approfondir :

Article écrit par Valentin Delbeke (@valoo_io), ingénieur études et développement chez Nextoo.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade