Patrons de conception II

1. Le patron Observer

1.1. Illustration du problème

Admettons que l'on désire écrire un tableur simple. Dans un tableur, une feuille de calcul est composée de cellules qui contiennent soit des nombres, soit des formules. Une formule décrit comment calculer la valeur de la cellule en fonction de la valeur d'autres cellules. L'image ci-dessous présente un tableur simple, les formules commençant par le caractère = :

spreadsheet;16.png

Le contenu d'une cellule contenant une formule doit être mis à jour dès que le contenu d'une cellule dont elle dépend change. Dans l'exemple ci-dessus, la cellule A3 dépend des cellules A1 et A2, B3 dépend de B1 et B2, et C3 dépend de A3 et B3 — donc indirectement de A1, A2, B1 et B2.

Comment organiser le programme pour garantir que les mises à jour nécessaires soient faites ?

1.2. Solution

Une solution consiste à permettre aux cellules d'en observer d'autres. Lorsque la valeur d'une cellule change, toutes les cellules qui l'observent sont informées du changement.

Une fois cette possibilité offerte, toute cellule contenant une formule observe la ou les cellules dont elle dépend — c-à-d celles qui apparaissent dans sa formule. Lorsqu'elle est informée du changement de l'une d'entre elles, elle met à jour sa propre valeur.

Afin d'illustrer cette solution, modélisons un tableur très simple dont les cellules contiennent soit un nombre, soit une formule. Pour simplifier les choses, nos formules sont toutes des sommes de la valeur de deux autres cellules — c-à-d qu'elles ont la forme =C1+C2C1 et C2 sont des cellules.

1.2.1. Sujets et observateurs

Lorsqu'un objet observe un autre objet, on appelle le premier l'observateur (observer) et le second le sujet (subject). Notez qu'un objet donné peut être à la fois sujet et observateur. Ainsi, dans notre exemple, la cellule A3 est à la fois observatrice — des cellules A1 et A2 — et sujet d'une observation — par la cellule C3.

Un observateur doit pouvoir être informé des changements du (ou des) sujet(s) qu'il observe. A cet effet, il possède une méthode, nommée update ci-dessous, qui est appelée lorsqu'un sujet qu'il observe a été modifié. Le sujet en question est passé en argument à cette méthode, pour permettre son identification. L'interface Observer ci-dessous représente un tel observateur :

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

Un sujet doit mémoriser l'ensemble de ses observateurs, et les avertir lors d'un changement de son état. Pour ce faire, le sujet offre des méthodes permettant l'ajout et la suppression d'un observateur à son ensemble. De plus, un sujet doit prendre garde à bien avertir ses observateurs lorsque son état change. L'interface Subject ci-dessous représente un tel sujet :

public interface Subject {
  void addObserver(Observer o);
  void removeObserver(Observer o);
}

1.2.2. Cellule

Le code commun à toutes les cellules est placé dans une classe abstraite de cellule nommée Cell. Toute cellule doit pouvoir être sujet d'une observation, donc Cell implémente l'interface Subject et met en œuvre les méthodes addObserver et removeObserver. Elle offre également une méthode protégée (notifyObservers) pour avertir les observateurs d'un changement.

public abstract class Cell implements Subject {
  private Set<Observer> observers = new HashSet<>();
  abstract public int value();

  public void addObserver(Observer o) {
    observers.add(o);
  }

  public void removeObserver(Observer o) {
    observers.remove(o);
  }

  protected void notifyObservers() {
    for (Observer o: observers)
      o.update(this);
  }
}

Une cellule contenant une valeur ne fait rien d'autre que mémoriser celle-ci et notifier ses observateurs lorsqu'elle change.

public final class ValueCell extends Cell {
  private int value = 0;
  public int value() {
    return value;
  }
  public void setValue(int newValue) {
    if (newValue != value) {
      value = newValue;
      notifyObservers();
    }
  }
}

Une cellule somme diffère d'une cellule valeur en ce qu'elle doit observer les deux cellules à sommer. Lorsque l'une d'elles change, la valeur de la cellule somme est mise à jour en fonction.

Pour pouvoir observer les cellules à sommer, la classe des cellules somme doit implémenter l'interface Observer et s'enregistrer comme observateur de ces cellules.

public final class SumCell
    extends Cell implements Observer {
  private final Cell c1, c2;
  private int sum = 0;

  public SumCell(Cell c1, Cell c2) {
    this.c1 = c1;
    this.c2 = c2;
    c1.addObserver(this);
    c2.addObserver(this);
  }

  public void update(Subject s) {
    int newSum = c1.value() + c2.value();
    if (newSum != sum) {
      sum = newSum;
      notifyObservers();
    }
  }

  public int value() {
    return sum;
  }
}

1.2.3. Tableur

La classe représentant le tableur (très) simplifié se contente de créer les cellules et de leur attribuer une valeur ou une formule. Afin de pouvoir observer les changements de valeur des cellules, le tableur est lui-même un observateur — c-à-d qu'il implémente l'interface Observer. Sa méthode d'observation affiche le nouvel état de la cellule modifiée à l'écran.

public final class SpreadSheet implements Observer {
  public SpreadSheet() {
    ValueCell A1 = new ValueCell(), A2 = …;
    SumCell A3 = new SumCell(A1, A2);
    // … C3
    A1.addObserver(this); // A2 … C3 idem
    A1.setValue(10);      // A2 … C3 idem
  }
  public void update(Subject s) {
    Cell c = (Cell)s;
    System.out.println("nouvelle valeur de "
		       + c + ": " + c.value());
  }
}

1.3. Généralisation

Chaque fois que l'état d'un premier objet — appelé observateur — dépend de l'état d'un second objet — appelé sujet — l'observateur peut observer l'état de son sujet et se mettre à jour dès que celui-ci change. Pour que l'observation soit possible, il faut que le sujet gère l'ensemble de ses observateurs et les avertisse lors d'un changement.

C'est l'idée du patron de conception Observer.

1.4. Diagramme de classes

observer-classes;16.png

1.5. Avantages et inconvénients du patron

Le gros intérêt du patron Observer est qu'il découple les sujets et les observateurs, en ce que le sujet ne sait généralement rien de ses observateurs. Pour cette raison, le patron Observer est très souvent utilisé par les bibliothèques d'interface graphique, par exemple.

Cela dit, le patron Observer possède également plusieurs inconvénients :

  • il impose un style de programmation impératif (non immuable), dans lequel les objets ont un état qui change au cours du temps,
  • il peut rendre les dépendances cycliques entre états difficiles à voir,
  • il peut rendre observables des états qui ne devraient pas l'être (glitches).

Examinons plus en détail ces trois problèmes.

1.5.1. Observer et immuabilité

Le patron Observer est fondamentalement incompatible avec l'immuabilité, dans le sens où tout sujet doit avoir un état modifiable. Observer un objet immuable n'est d'aucune utilité ! L'utilisation du patron Observer force donc un style de programmation basé sur des objets non immuables ce qui, nous l'avons vu, pose plusieurs problèmes.

1.5.2. Dépendances

Lorsqu'on utilise le patron Observer, il peut être utile de dessiner le graphe d'observation du programme afin de détecter d'éventuels problèmes. Ce graphe a les sujets et observateurs du programme comme nœuds et un arc va de tout observateur à son (ou ses) sujet(s).

Ainsi, le graphe d'observation de notre tableur simplifié est :

spreadsheet-graph;16.png

Pour mémoire, le tableur lui-même se présente ainsi :

spreadsheet;16.png

Normalement, le graphe d'observation devrait être acyclique — dénué de cycles — car ceux-ci peuvent provoquer des boucles infinies d'envoi de notifications. En pratique, ces cycles sont parfois difficiles à éviter et on utilise donc différents moyens — plus ou moins propres — pour éviter de boucler à l'infini lors de l'envoi de notifications.

Notez que les tableurs détectent et interdisent les cycles, mais rien n'empêche un graphe d'observation d'être cyclique dans un programme réel utilisant le patron Observer.

1.5.3. Glitches

Même en l'absence de cycles d'observation, le patron Observer peut poser problème. On peut l'illustrer au moyen de la feuille de calcul suivante :

spreadsheet-glitch;16.png

dont le graphe d'observation est :

spreadsheet-glitch-graph;16.png

Que peut-il se passer si on introduit la valeur 9 dans la cellule A1 ?

Etonnamment, il est possible qu'une division par zéro se produise, même si cela n'est mathématiquement pas possible étant donnée la formule utilisée ! En effet, si la nouvelle valeur de A1 est d'abord communiquée à B2 et que celle-ci met sa valeur à jour en utilisant les valeurs actuelles des cellules, elle le fait en ayant déjà connaissance de la nouvelle valeur de A1 (9) mais pas encore de la nouvelle valeur de B1, qui vaut toujours 9.

Le patron Observer ne donne aucune garantie concernant l'ordre dans lequel les notifications sont propagées dans le graphe d'observation. En conséquence, il est possible d'observer des états théoriquement impossibles, que l'on nomme glitches en anglais, comme ci-dessus.

Notez que les tableurs s'assurent que les glitches ne se produisent pas en propageant les modifications en ordre topologique, mais là encore rien de similaire n'existe dans un programme utilisant le patron Observer.

1.6. Exemples réels

Le patron de conception Observer est très fréquemment utilisé dans les bibliothèques de gestion d'interfaces graphiques.

Les composants graphiques d'une telle application doivent souvent refléter l'état interne de l'application. Pour rester à jour, ils observent simplement la partie de l'état interne de l'application dont ils dépendent.

Par exemple, un composant affichant un graphique dans un tableur peut observer les données qu'il représente pour savoir quand se mettre à jour.

2. Le patron MVC

2.1. Illustration du problème

Une application graphique typique est composée d'un certain nombre de composants standards (boutons, menus, listes, etc.) qui permettent à l'utilisateur de visualiser et de manipuler l'état de l'application.

Comment faire en sorte que les composants n'aient aucune connaissance précise de l'application et soient ainsi réutilisables ?

2.2. Solution

La solution (évidente) à ce problème consiste à découpler la gestion de l'interface utilisateur de la gestion de l'état propre à l'application.

ui-decoupling;16.png

2.3. Généralisation

Le découplage entre la gestion de l'interface utilisateur et la logique propre à l'application peut se faire de différentes manières. Le patron Model-View-Controller (MVC) propose d'organiser le code du programme en trois catégories :

  1. le modèle, qui contient la totalité du code propre à l'application et qui n'a aucune notion d'interface graphique,
  2. la vue, qui contient la totalité du code permettant de représenter le modèle à l'écran,
  3. le contrôleur, qui contient la totalité du code gérant les entrées de l'utilisateur et les modifications du modèle correspondantes.

Cette organisation et les interactions qu'elle implique avec l'utilisateur sont illustrées dans la figure ci-dessous :

mvc;16.png

Notez que le patron MVC n'est pas au même niveau que les autres patrons que nous avons vus jusqu'à présent. En effet, il propose une organisation de la totalité du programme, pas seulement d'une petite partie de celui-ci pour résoudre un problème local. Pour cette raison, MVC est parfois qualifié de patron architectural (architectural pattern).

2.3.1. Modèle

Le modèle (model) est l'ensemble du code qui gère les données propres à l'application. Le modèle ne contient aucun code lié à l'interface utilisateur.

Par exemple, dans un navigateur Web, le modèle contient le code responsable de gérer les accès au réseau, d'analyser les fichiers HTML reçus, d'interpréter les programmes JavaScript, de décompresser les images, etc.

2.3.2. Vue

La vue (view) est l'ensemble du code responsable de l'affichage des données à l'écran.

Par exemple, dans un navigateur Web, la vue contient le code responsable de transformer le contenu HTML reçu en quelque chose d'affichable, l'affichage de l'état d'avancement des téléchargements en cours, etc.

2.3.3. Contrôleur

Le contrôleur (controler) est l'ensemble du code responsable de la gestion des entrées de l'utilisateur.

Par exemple, dans un navigateur Web, le contrôleur contient le code responsable de gérer les clics sur des liens, l'entrée de texte dans des zones textuelles, la pression sur le bouton d'interruption de chargement, etc.

2.4. MVC et Observer

Le patron Observer joue un rôle central dans la mise en œuvre du patron MVC. Par exemple, pour que le modèle puisse être découplé de la vue, il faut que cette dernière ait la possibilité de réagir aux changements de ce premier. Cela se fait généralement au moyen du patron Observer.

2.5. Intérêt du patron

Le patron MVC a plusieurs avantages :

  • le modèle peut être réutilisé avec différentes interfaces utilisateur (p.ex. application de bureau, application Web, application mobile),
  • le modèle est relativement facile à tester puisqu'il n'est absolument pas lié à une interface graphique,
  • le code gérant les composants graphiques standards (boutons, menus, etc.) est réutilisable pour de nombreuses applications.

Ces avantages proviennent principalement de la séparation entre le modèle et le couple (vue, contrôleur). En pratique, il est d'ailleurs souvent difficile de découpler la vue du contrôleur, et cela n'est donc que partiellement fait, voire pas du tout.

2.6. Exemples réels

De nombreuses bibliothèques d'interface utilisateur graphique utilisent le patron MVC ou l'un de ses dérivés, p.ex. :

  • Swing utilise une variante de MVC dans laquelle la vue et le contrôleur ne font souvent qu'un,
  • Cocoa — la bibliothèque graphique de macOS — utilise le patron MVC,
  • beaucoup de bibliothèques de gestion d'interface graphique pour les applications Web sont basées sur MVC,
  • etc.

3. Références

  • Design Patterns: Elements of Reusable Object-Oriented Software de Erich Gamma, Richard Helm, Ralph Johnson et John Vlissides.