|
|
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é.
|
|
|
|
|
|
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 fournie, comme dans cet exemple :
|
|
|
|
|
|
```java
|
|
|
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 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 , ainsi pour enregistrer notre type le code suivant suffira :
|
|
|
|
|
|
```java
|
|
|
ObservableManager.addObservable(Individual.class);
|
|
|
```
|
|
|
|
|
|
Par ailleurs cette méthode renvoie un objet de type 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 , 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 :
|
|
|
|
|
|
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 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 :
|
|
|
|
|
|
```java
|
|
|
ObservableManager.getObservable(Individual.class).fireChanges(instance,time);
|
|
|
```
|
|
|
|
|
|
Connexion à un observateur
|
|
|
--------------------------
|
|
|
|
|
|
La connexion d'une observable à un observateur se fait simplement grâce à l' de la façon suivante :
|
|
|
|
|
|
```java
|
|
|
ObservableManager.getObservable(Individual.class).addObserverListener(new MonObserver());
|
|
|
```
|
|
|
|
|
|
Où est le type d'observateur à utiliser, et 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](ObservablesModelisateur) pour explications sur les ClassObservable) à l'aide de la méthode `addObserverListener(ObserverListener)`
|
|
|
* Observer les règles de bonne conduite des Observers.`
|
|
|
|
|
|
Exemple :
|
|
|
|
|
|
```java
|
|
|
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](ObservablesModelisateur), 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](http://java.sun.com/j2se/1.5.0/docs/api/java/lang/reflect/Field.html) 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 :
|
|
|
|
|
|
```java
|
|
|
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 :
|
|
|
|
|
|
```java
|
|
|
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.
|
|
|
|
|
|
```java
|
|
|
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](http://java.sun.com/j2se/1.5.0/docs/api/java/lang/reflect/Field.html) 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. |