Observer (Observateur)

Dans la programmation orientée objets, les objets contiennent à la fois des données et des comportements qui, une fois réunis, correspondent à un aspect spécifique du domaine métier. Un des avantages à utiliser des objets pour construire des applications est qu'il est possible d'encapsuler dans l'objet toutes les manipulations de données. L'objet est ainsi autonome et son potentiel à être réutilisé dans d'autres applications augmente. Toutefois, les objets ne peuvent pas simplement fonctionner isolément. Dans pratiquement toutes les applications standard, les objets doivent collaborer pour exécuter des tâches plus complexes. Dans le cadre de cette collaboration, les objets peuvent avoir à s'informer mutuellement si l'un d'entre eux change d'état. Par exemple, le modèle MVC ( Model-View-Controller ) prescrit la séparation des données métier (le modèle) et de la logique de présentation (la vue). Lorsque le modèle change, le système doit en avertir la vue afin qu'elle puisse actualiser le contenu de la présentation de façon à refléter précisément l'état du modèle. En d'autres termes, la vue attend du modèle qu’il l’informe des modifications de son état interne.

Problème

Comment un objet peut-il notifier d'autres objets des changements d'état sans être dépendant de leurs classes ?

Facteurs à prendre en compte

Les facteurs et les points suivants doivent être pris en compte dans la recherche de la solution au problème :

  • La solution la plus facile pour informer des objets dépendants d'un changement d'état est de les appeler directement. Cependant, la collaboration directe entre objets crée une dépendance entre leurs classes. Ainsi, si l'objet modèle appelle l'objet vue pour l'informer de modifications, la classe modèle devient elle aussi dépendante de la classe vue. Ce type de couplage direct entre deux objets (également appelé couplage étroit) limite la capacité de réutilisation des classes. Par exemple, chaque fois que vous voulez réutiliser la classe modèle, vous devez aussi réutiliser la classe vue puisque le modèle lui adresse un appel. Si vous avez plusieurs vues, le problème est encore plus complexe.
  • La nécessité de dissocier les classes apparaît souvent dans les frameworks gérés par événement. Le framework doit pouvoir avertir l'application des événements qui surviennent, mais il ne peut pas être dépendant de classes de l'application spécifiques.
  • De même, si vous modifiez la classe vue, le modèle sera probablement affecté. Les applications qui contiennent des classes à couplage étroit ont tendance à être fragiles et difficiles à gérer, car les modifications effectuées dans une classe peuvent avoir des répercussions sur toutes les classes étroitement couplées.
  • Si vous appelez directement des objets dépendants, le code contenu dans l'objet source doit être modifié chaque fois qu'un nouvel objet dépendant est ajouté.
  • Dans certains cas, le nombre d'objets dépendants peut être inconnu au moment de la conception. Par exemple, si vous autorisez l'utilisateur à ouvrir plusieurs fenêtres (vues) pour un modèle en particulier, vous devrez mettre à jour plusieurs vues chaque fois que l'état du modèle changera.
  • Un appel direct à une fonction reste encore le moyen le plus efficace pour transmettre des informations entre deux objets (les performances étant toutefois moins bonnes que si les fonctionnalités des deux objets sont réunies au sein d'un objet unique). En conséquence, le découplage d'objets avec d'autres mécanismes risque d'avoir un impact défavorable sur les performances. Selon les performances recherchées, vous devrez peut-être à ce niveau faire un compromis.

Solution

Utilisez le modèle Observer pour gérer une liste des objets dépendants concernés (observateurs) dans un objet séparé (le sujet). Veillez à ce que tous les observateurs implémentent une interface Observer commune afin d'éliminer les dépendances directes entre le sujet et les objets dépendants (voir la figure 1).

20031127-Des-Observer-1.gif

Figure 1 : Structure de base d'un observateur

Lorsqu'un changement d'état survient au niveau du client associé aux objets dépendants, ConcreteSubject appelle la méthode Notify(). La superclasse Subject gère une liste de tous les observateurs afin que la méthode Notify() puisse s'exécuter en boucle dans cette liste et appelle la méthode Update() sur chaque observateur inscrit. Les observateurs s'inscrivent et se désinscrivent pour des mises à jour en appelant les méthodes subscribe() et unsubscribe() sur la superclasse Subject (voir la figure 2). Une ou plusieurs instances de ConcreteObserver peuvent également accéder à la classe ConcreteSubject pour obtenir davantage d'informations et elles dépendent donc en général de la classe ConcreteSubject. Cependant, comme le montre la figure 1, il n'y a pas de dépendance directe ni indirecte entre la classe ConcreteSubject et la classe ConcreteObserver.

20031127-Des-Observer-2.gif

Figure 2 : Interaction du modèle Observer de base

Avec le système générique de communication entre le sujet et les observateurs, il est possible de construire des collaborations dynamiquement, et non plus de façon statique. En raison de la séparation de la logique de notification et de la logique de synchronisation, il est possible d'ajouter de nouveaux observateurs sans modifier la logique de notification, et celle-ci peut aussi être modifiée sans qu'il y ait de répercussions sur la logique de synchronisation dans les observateurs. Le code est à présent bien plus distinct et donc plus facile à gérer et à réutiliser.

Le fait de mettre des objets au courant des changements affectant d'autres objets sans risquer une dépendance avec leurs classes est une nécessité si commune que certaines plates-formes fournissent des fonctionnalités de langage pour assurer cette tâche. Par exemple, Microsoft® .NET Framework définit la notion de délégués et d'événements pour accomplir le rôle d'Observer. Par conséquent, vous aurez rarement à implémenter le modèle Observer de façon explicite dans .NET, mais vous devrez en revanche utiliser des délégués et des événements. La plupart des développeurs .NET vont s'imaginer que le modèle Observer est une solution compliquée pour implémenter des événements.

La solution présentée dans la figure 1 montre la classe ConcreteSubject qui hérite de la classe Subject. La classe Subject contient les implémentations des méthodes pour ajouter ou supprimer des observateurs et lancer une itération dans la liste des observateurs. Il suffit à la classe ConcreteSubject d'hériter de Subject et d'appeler Notify() en cas de changement d'état. Dans les langages qui ne prennent en charge que l'héritage unique (comme Java ou C#), le fait d'hériter de la classe Subject empêche celle-ci d'hériter d'autres classes. Ceci peut être problématique car, dans bien des cas, ConcreteSubject est un objet domaine qui peut hériter d'une classe de base de l'objet domaine. Par conséquent, il est préférable de remplacer la classe Subject par une interface Subject et de fournir une classe Helper pour l'implémentation (voir la figure 3). De la sorte, vous n'épuisez pas votre relation de superclasse unique avec la classe Subject et vous pouvez utiliser la classe ConcreteSubject dans une autre hiérarchie d'héritage. Certains langages (Smalltalk par exemple) implémentent même l'interface Subject comme faisant partie de la classe Object, de laquelle chaque classe hérite implicitement.

20031127-Des-Observer-3.gif

Figure 3 : Utilisation d'une classe d'assistants pour éviter l'héritage de la classe Subject

Malheureusement, vous devez à présent ajouter du code dans chaque classe qui hérite de l'interface Subject afin d'implémenter les méthodes qui y sont définies. Cette tâche peut vite devenir fastidieuse. Par ailleurs, du fait que l'objet domaine coïncide avec ConcreteSubject, il ne peut pas faire la distinction entre les différents types de changements d'état éventuellement associés aux différents sujets. Ceci permet uniquement aux observateurs de prendre connaissance de tous les changements d'état de ConcreteSubject, même si vous vouliez être plus sélectif (par exemple, si l'objet source contient une liste, vous souhaiterez peut-être être informé des mises à jour mais pas des insertions). Une autre possibilité serait que les observateurs filtrent les notifications non pertinentes, mais la solution devient alors moins efficace car ConcreteSubject appelle tous les observateurs uniquement pour découvrir qu'ils ne sont pas vraiment concernés.

Pour résoudre ces questions, vous pouvez séparer totalement le sujet de la classe source (voir la figure 4). Dans ce cas, la classe ConcreteSubject se limite à implémenter l'interface Subject et elle n'a aucune autre tâche à exécuter. Ceci permet d'associer DomainObject à plusieurs instances de ConcreteSubject afin de pouvoir distinguer les différents types d'événements pour une classe de domaine unique.

20031127-Des-Observer-4.gif

Figure 4 : Séparation de DomainObject et de Subject

Les événements et les délégués de l'environnement .NET Framework implémentent cette approche comme construction de langage, de sorte que vous ne devez même plus implémenter vos propres classes ConcreteSubject. En clair, des événements remplacent les classes ConcreteSubject et les délégués implémentent le rôle de l'interface Observer.

Propagation des informations d'état

Jusqu'à présent, cette solution a décrit comment un objet client peut tenir les observateurs au courant des changements d'état ; or, nous n'avons pas encore vu comment les observateurs déterminent l'état dans lequel se trouve l'objet client. Il existe deux mécanismes pour transmettre cette information aux observateurs :

  • La technique push. Dans la technique push, le client envoie toutes les informations pertinentes sur le changement d'état au sujet, qui à son tour passe l'information à chaque observateur. Si l'information est passée dans un format neutre (XML par exemple), ce modèle évite que les observateurs dépendants n'accèdent directement au client pour avoir plus d'informations. En revanche, le sujet doit faire certaines hypothèses sur les informations susceptibles d'intéresser les observateurs. Si un nouvel observateur est ajouté, le sujet peut avoir à publier d'autres informations requises par ce dernier. Ce faisant, le sujet et le client deviennent une fois encore dépendants des observateurs, ce qui rétablit le problème que nous cherchions à résoudre au départ. Aussi, si vous utilisez la technique push, il est préférable de choisir le procédé d'inclusion pour déterminer la quantité d'informations à transmettre aux observateurs. Le plus souvent, vous aurez à inclure une référence au sujet dans l'appel à l'observateur concerné. Les observateurs peuvent ensuite utiliser cette référence pour connaître un état.
  • La technique pull. Avec la technique pull, le client informe le sujet d'un changement d'état. Une fois que les observateurs ont reçu la notification, ils accèdent au sujet ou au client pour toute donnée supplémentaire (voir la figure 5) en utilisant la méthode getState(). Ce modèle n'exige pas du sujet qu'il passe des informations en même temps que la méthode update(), mais il se peut que l'observateur ait à appeler la méthode getState() simplement pour vérifier que le changement d'état n'était pas pertinent. Pour cette raison, ce modèle risque de s'avérer un peu moins efficace. Une autre complication possible survient lorsque l'observateur et le sujet s'exécutent dans des threads différents (par exemple, si vous utilisez un appel de méthode à distance pour tenir les observateurs au courant). Dans ce scénario, l'état interne du sujet a pu changer à nouveau pendant que l'observateur obtenait les informations d'état via le callback. Dans ce type de situation, l'observateur risque de sauter une opération.

20031127-Des-Observer-5.gif

Figure 5 : Propagation de l'état selon la technique pull

Quand déclencher une mise à jour

Lorsque vous implémentez le modèle Observer, vous avez deux options pour gérer le déclenchement de la mise à jour. La première consiste à insérer l'appel à la méthode Notify() dans le client, immédiatement après chaque appel à l'objet Subject qui affecte un état de changement interne. Ceci donne au client un contrôle total sur la fréquence de notification, mais lui impose une charge supplémentaire qui peut entraîner des erreurs si le développeur oublie d'appeler la méthode Notify(). L'autre option est d'encapsuler l'appel à la méthode Notify() dans chaque opération de changement d'état de l'objet Subject. De cette façon, un changement d'état provoque toujours l'appel à la méthode Notify() sans aucune action supplémentaire de la part du client. L'inconvénient est que plusieurs opérations imbriquées risquent d'entraîner de multiples notifications. La figure 6 montre un exemple de ce type, dans lequel l'Opération A appelle la Sous-opération B et où un observateur peut recevoir deux appels à sa méthode Update.

20031127-Des-Observer-6.gif

Figure 6 : Notifications superflues

L'appel de multiples mises à jour pour une opération unique mais imbriquée peut donner lieu à des inefficacités, mais aussi à des effets indésirables beaucoup plus graves : le sujet peut être dans l'état non valide lorsque la méthode Notify imbriquée est appelée à la fin de l'Opération B (figure 6) du fait que l'Opération A n'a été traitée qu'en partie. Il convient alors de ne pas utiliser de méthode Notify imbriquée. Par exemple, l'Opération B peut être extraite et placée dans une méthode sans logique de notification et compter sur l'appel à Notify() au sein de l'Opération A. La méthode Template [Gamma95] est une construction utile pour s'assurer que les observateurs ne sont mis au courant qu'une seule fois.

Observateurs qui affectent le changement d'état

Dans certains cas, un observateur peut changer l'état du sujet pendant qu'il traite l'appel update(). Il peut en résulter des problèmes si le sujet appelle automatiquement Notify() après chaque changement d'état. La figure 7 en explique la raison.

20031127-Des-Observer-7.gif

Figure 7 : La modification de l'état de l'objet à partir de Update entraîne une boucle sans fin

Dans cet exemple, l'observateur exécute l'Opération A en réponse à la notification de changement d'état. Si l'Opération A modifie l'état de DomainObject, il déclenche ensuite un autre appel à Notify(), qui à son tour rappelle la méthode Update de l'observateur. Le résultat est une boucle sans fin. La boucle sans fin est facile à détecter dans cet exemple simple, mais si les relations sont plus complexes, il peut s'avérer difficile de déterminer la chaîne des dépendances. Une manière de réduire la probabilité des boucles sans fin consiste à faire que la notification soit spécifique d'un intérêt. Ainsi, dans le langage C#, utilisez l'interface suivante pour le sujet, où « Interest » pourrait par exemple être une énumération de tous les types d'intérêt :

interface Subject 
{
public void addObserver(Observer o, Interest a);
public void notify(Interest a);
...
}

interface Observer
{
 public void update(Subject s, Interest a);
}

Le fait que les observateurs ne soient mis au courant que lorsqu'un événement relié à leur intérêt spécifique se produit réduit la chaîne des dépendances et sert à éviter les boucles sans fin. Le procédé équivaut dans .NET à établir plusieurs types d'événements toujours plus étroitement définis. L'autre option permettant d’éviter la boucle sans fin consiste à introduire un mécanisme de verrouillage pour éviter que le sujet ne publie de nouvelles notifications tant qu'il est dans la boucle Notify() initiale.

Contexte final

Du fait que le modèle Observer accepte le couplage lâche et réduit les dépendances, devez-vous établir un couplage lâche sur chaque paire d'objets qui dépendent l'un de l'autre ? Certainement pas. Comme pour la plupart des modèles, une solution résout rarement tous les problèmes. Lorsque vous utilisez le modèle Observer, vous devez envisager les compromis suivants.

Avantages
  • Couplage lâche et réduction des dépendances. Le client n'est plus dépendant des observateurs car il est isolé grâce à l'utilisation d'un sujet et de l'interface Observer. Cet avantage est mis en œuvre dans de nombreux frameworks où les composants de l'application peuvent s'inscrire en vue d'être tenus au courant lorsque des événements de framework (niveau inférieur) se produisent. En conséquence, le framework appelle le composant de l'application, sans en dépendre.
  • Nombre variable d'observateurs. Les observateurs peuvent être attachés et détachés pendant le runtime car le sujet n'émet aucune hypothèse sur leur nombre. Cette caractéristique est utile lorsque le nombre d'observateurs est encore inconnu au moment de la conception (par exemple, si vous avez besoin d'un observateur pour chaque fenêtre que l'utilisateur ouvre dans l'application).
Inconvénients
  • Diminution des performances. Dans bon nombre d'implémentations, les méthodes update() des observateurs peuvent s'exécuter dans le même thread que le sujet. Si la liste des observateurs est longue, l'exécution de la méthode Notify() peut prendre du temps. L'abstraction des dépendances entre objets ne signifie pas que l'ajout d'observateurs n'a aucun impact sur l'application.
  • Pertes de mémoire. Le mécanisme de callback (lorsqu'un objet s'inscrit afin d'être appelé ultérieurement) utilisé dans le modèle Observer peut conduire à une erreur commune qui déclenche des pertes de mémoire, même dans un programme en C# géré. Si un observateur qui devient hors de portée oublie de se désinscrire du sujet, ce dernier garde toujours une référence à l'observateur. Cette référence empêche le nettoyage de la mémoire Garbage collection de réallouer la mémoire associée à l'observateur tant que l'objet sujet n'est pas détruit lui aussi. Il peut en résulter d'importantes pertes de mémoire si la durée de vie des observateurs est plus courte que celle du sujet (ce qui est souvent le cas).
  • Dépendances cachées. L'utilisation des observateurs transforme des dépendances explicites (via des appels de méthode) en dépendances implicites (via des observateurs). Si de très nombreux observateurs sont utilisés dans une application, il devient presque impossible pour un développeur de comprendre ce qui se passe en regardant le code source. Il devient très difficile de comprendre les implications des modifications du code. La complexité du problème augmente de façon exponentielle avec les niveaux de propagation (par exemple, un observateur agissant comme Subject). Par conséquent, vous devez limiter l'utilisation des observateurs à quelques interactions bien définies, telle qu'une interaction entre un modèle et une vue dans le modèle MVC (Model-View-Controller). L'utilisation d'observateurs entre des objets domaine doit généralement éveiller les soupçons.
  • Difficultés de test/de débogage. Autant un couplage lâche peut s'avérer d'un grand intérêt architectural, autant il peut compliquer le développement. Plus vous découplez deux objets, plus il devient difficile de comprendre les dépendances entre eux lorsque vous examinez le code source ou un diagramme de classe. Par conséquent, vous ne devez associer des objets en couplage lâche que si vous pouvez en toute sécurité ignorer l'association qui existe entre eux (si, par exemple, l'observateur ne présente aucun effet secondaire néfaste).


Dernière mise à jour le jeudi 27 novembre 2003



Afficher: