Last modified 10 years ago Last modified on 21/09/2007 11:18:38

Cette bibliothèque permet de facilement extraire des variables d'état d'un modèle en utilisant de patron de conception Observateur. Le patron de conception Observateur consiste à associer une liste d'observateurs à un sujet observable, pour que chaque observateur soit notifié et mis-à-jour lors de chaque modification du sujet observé.

Téléchargement

Téléchargez l'archive compressée qui contient le JAR du projet ainsi que les bibliothèques requises. Vous pouvez également consulter les sources.

Vous pouvez également consulter (et exécuter) les sources de l'exemple de démonstration.

Déclaration d'une variable observable

Le principe d'utilisation de cette bibliothèque est de définir dans le code source quels attributs de classe sont susceptibles d'être observés, en ajoutant une description explicite pour chacun de ces attributs. Ceci est effectué en utilisant l'annotation Observable fournie, comme dans cet exemple :

public class Individual {
  @Observable(description = "age in weeks")
  private short age;
  // ...

Enregistrement d'une observable

Les liaisons entre les observables et les observateurs sont effectuées par l'objet ObservableManager dont les méthodes sont statiques donc accessibles depuis partout (patron de conception Singleton). Il est donc nécessaire d'enregistrer un type observable auprès de cet agent, afin que les attributs annotés comme observables soient enregistrés et que des connexions puissent ensuite être établies avec des observateurs. Cet enregistrement est accompli avec la méthode ObservableManager.addObservable(Class type), ainsi pour enregistrer notre type Individual le code suivant suffira :

ObservableManager.addObservable(Individual.class);

Par ailleurs cette méthode renvoie un objet de type ClassObservable qui est utilisé par la suite pour notifier les observateurs d'un changement de valeur de l'observable. Cet objet permet également d'obtenir facilement des informations sur les attributs déclarés observables. Il est possible d'obtenir ultérieurement cet objet à l'aide de la fonction ObservableManager.getObservable(Class type), mais la récupération de cet objet peut être coûteux si il est répété trop souvent. Dans la mesure du possible, il est donc préférable de garder une référence à cet objet comme cela est proposé dans la section suivante en utilisant un attribut statique.

Notification de mise à jour d'une observable

Ensuite, il faut notifier les observateurs lors d'un changement à l'aide de l'appel de méthode suivant :

ObservableManager.getObservable(Individual.class).fireChanges(instance,time);

Il est recommandé de faire cette notification dans un objet de plus haut niveau que le sujet observé, par exemple dans l'ordonnanceur qui contient a boucle qui est exécutée à chaque pas de temps. En effet, il sera souvent inutile (et coûteux) de notifier plusieurs fois les observateurs pendant un même pas de temps.

Par ailleurs, si un million d'individus doivent notifier à chaque pas de temps leur changement d'état, il est important que ceci ne se fasse pas au détriment des performances du modèle. En effet l'opération ObservableManager.getObservable(Individual.class) est somme toute coûteuse, et son résultat est constant. Il convient donc de garder une référence sur ce résultat, en utilisant par exemple un attribut statique dans la classe effectuant la boucle temporelle. Cela ressemblerait donc à ça :

public class MonModele {
  static ClassObservable  = ObservableManager.getObservable(Individual.class);

  // ...

  public void scheduleOneStep(Time t) {
    for (Individual individual : population)
        co.fireChanges(individual, t);
  }
}

Connexion à un observateur

La connexion d'une observable à un observateur se fait simplement grâce à l'ObservableManager de la façon suivante :

ObservableManager.getObservable(Individual.class).addObserverListener(new MonObserver());

MonObserver est le type d'observateur à utiliser, et Individual le type d'objet observable qui sera connecté à cet observateur.

Développement d'un observateur

Principe général

L'écriture d'un Observer ressemble beaucoup à l'écriture d'un listener sur un composant awt ou swing. Il suffit de :

  • Implémenter un fr.cemagref.simaqualife.observable.ObserverListener, dont l'unique méthode est public void valueChanged(ClassObservable,Object,Time)
  • Déclarer cet ObserverListener auprès du ClassObservable souhaité (voir Le point de vue du modélisateur? pour explications sur les ClassObservable) à l'aide de la méthode addObserverListener(ObserverListener)
  • Observer les règles de bonne conduite des Observers.

Exemple :

import fr.cemagref.simaqualife.observable.*;

public class MonObserver {

  public MonObserver() {
    ...
    ObserverListener principeActif = new ObserverListener() {
      public void valueChanged(ClassObservable clObservable, Object instance, Time t) {
        // C'est ici que l'on code ce qui sera exécuté à chaque fois qu'une instance
        // de l'observable (que l'on récupère dans ''instance'') notifiera ses changements.
      }
    }

    // Récupération du ClassObserver correspondant à la classe que l'on veut écouter
    ClassObserver co = ObservableManager.getObservableManager().getObservable(LaClasseQueJeVeuxEcouter.class);

    // Inscription du listener auprès du ClassObserver
    co.addObserverListener(principeActif);
  }
}

Récupération des valeurs observées

Récupérer un java.lang.reflect.Field

Le ClassObserver associé à la classe observée permet de récupérer la liste des observables. Ceux-ci sont désignés par la chaîne de caractères affectée à description (voir Le point de vue du modélisateur?, qui présente la déclaration des attributs observables).

L'accès à un attribut observable se fait par le biais d'un java.lang.reflect.Field récupéré à l'aide de la méthode ClassObservable.getAttribute(String).

Supposons, par exemple, que la classe que l'on observe contient les déclarations suivantes :

public class LaClasseQueJeVeuxEcouter {
  ...
  @Observable(description = "valeur")
  double val = 0.0;

  @Observable(description = "nombre de voisins")
  int nbVois = 0;
  ...
}

Si l'on souhaite accéder à l'observable "valeur", on commencera par récupérer le java.lang.reflect.Field correspondant :

  ClassObserver co = ObservableManager.getObservableManager().getObservable(LaClasseQueJeVeuxEcouter.class);
  java.lang.reflect.Field fValeur = co.getAttribute("valeur");

Attention : Le nom "valeur" doit respecter exactement la casse telle que donnée après la balise @Observable. Ainsi, "Valeur" ou "VALEUR" renverront null.

Une autre possibilité consiste à récupérer un tableau contenant toutes les descriptions des attributs observables d'une classe, à l'aide de la méthode ClassObservable.getDescriptions(). Une fois choisi un observable (ou plusieurs) parmi les descriptions, on récupère le Field qui lui correspond en utilisant le même indice dans le tableau renvoyé par ClassObservable.getAttributes().

PERFORMANCES : Les méthodes ClassObservable.getAttribute[s](...) et ClassObservable.getDescription[s](...) sont considérées commes lentes, c'est à dire qu'il est conseillé de récupérer les Field des Attributs que l'on souhaite surveiller dans une phase d'initialisation du programme, pour utiliser ces Field directement lors de l'exécution du modèle.

Récupérer la valeur proprement dite

Une fois le java.lang.reflect.Field récupéré, on accède à la valeur désirée à l'aide de la méthode Object get(Object), ou, plus précisément, comme on sait que valeur est du type générique double, avec la méthode double getDouble(Object).

Le paramètre attendu est l'instance dont on veut extraire la valeur. Elle nous est fournie à l'appel de la méthode ObserverListener.valueChanged(ClassObservable co, Object instance, Time t).

PERFORMANCES : Les méthodes get de Field, telles que get(Object), getDouble(Object), getInt(Object), etc... sont rapides. Elle sont bien destinées à être employées dans la boucle principale du modèle, et donc par voie de conséquence dans la méthode ObserverListener.valueChanged(...).

Exemple : Un observer qui écrit sur la sortie standard la moyenne des "valeur" à chaque pas de temps.

import fr.cemagref.simaqualife.observable.*;

public class MonObserver {

  java.lang.reflect.Field fValeur;
  double somme = 0.0;
  int effectif = 0;
  Time lastT = null;

  public MonObserver() {
    ClassObserver co = ObservableManager.getObservableManager().getObservable(LaClasseQueJeVeuxEcouter.class);
    fValeur = co.getAttribute("valeur");

    ObserverListener principeActif = new ObserverListener() {
      public void valueChanged(ClassObservable clObservable, Object instance, Time t) {
        if (!t.equals(lastT)) {
          System.out.println("Pas de temps="+lastT.toString()
              +(effectif==0?" - aucune valeur":" moyenne="+(somme / (double) effectif)));
          lastT = (Time) t.clone();
          effectif = 0;
          somme = 0.0;
        }
        somme += fValeur.getDouble(instance);
        effectif++;
      }
    }

    co.addObserverListener(principeActif);
  }
}

Remarques :

  • java.lang.reflect.Field propose entre autres les méthodes getType() et getGenericType(), bien pratique pour développer des Observers génériques. De toutes façons, aller jeter un œil sur sa javadoc dans l'API java est toujours une bonne idée.
  • Le code ci-dessus a deux inconvénients :
    • La méthode Time.equals peut s'avérer coûteuse.
    • La dernière itération ne provoque pas de sortie de la valeur moyenne.

Ces inconvénients seraient avantageusement résolus par l'utilisation d'un ObserverListener sur Time, et d'un autre sur le gestionnaire de la plateforme (qui réagirait au changements d'état du modèle -- démarrage, stoppé, terminé, ... -- par exemple), ces deux ObserverListeners pouvant tout à fait n'en faire qu'un (un test de type sur le ClassObservable étant bien moins coûteux à l'échelle du pas de temps qu'à l'échelle de l'individu dans un IBM).

Les règles de bonne conduite

Il n'y a qu'une règle : un Observer se doit d'être le plus discret possible.

Concrètement, cela signifie que l'on s'efforcera de faire en sorte que la méthode ObserverListener.valueChanged(...) s'exécute en un temps négligeable vis à vis de la dynamique du modèle, avec une consommation mémoire tout aussi discrète.

Le schéma standard consiste à

  • collecter les données au niveau de l'individu.
  • se permettre un calcul un peu plus complexe, rafraîchir un affichage, écrire des données, ... au niveau du pas de temps.

Bien entendu, dans des cas où la dynamique des individus est lente, ou bien si la simulation s'effectue sur un très grand nombre de pas de temps, on ajustera ce schéma en conséquence.

On peut aussi trouver un niveau intermédiaire par l'utilisation de Threads (de préférence en MIN_PRIORITY), notamment pour les affichages graphiques. La boucle du Thread consiste à :

  • récupérer les données collectées par l'Observer (on peut se permettre une méthode synchro à ce niveau)
  • effectuer des traitements sur ces données
  • rafraîchir l'affichage
  • dormir un peu, si
    • un certain nombre d'autres Threads similaires risquent d'être présents
    • la boucle est relativement rapide, et une méthode synchro lors de la collecte des données stoppe trop souvent le modèle.

Enfin bref, il faut s'adapter. Mais sans oublier que le but reste de développer des Observers génériques.

Attachments