LOCODUINO
Discussions Générales => Discussions ouvertes => Discussion démarrée par: Pierre59 le février 07, 2019, 03:43:54 pm
-
Bonjour
Je vais vous parler ici des "Patrons de Conception" (Design Pattern) en langage orienté objet et tout particulièrement du patron "observateur". Un patron de conception est une solution standard, indépendante du langage, pour résoudre un problème de conception. L'idée, pour le patron observateur, est qu'un objet puisse observer un autre objet pour savoir quand son état change. Il y a deux approches possibles :
- soit l'objet observateur teste périodiquement l'objet observé
- soit l'objet observé notifie son changement à l'objet observateur
Pour la première solution ce qui peut venir à l'esprit est de questionner périodiquement l'objet observé, c'est de l'attente active, elle peut être éventuellement déléguée à un thread, mais outre la consommation de ressources elle présente un temps de latence dû au questionnement périodique. Ce n'est pas la bonne solution.
La deuxième solution est plus intéressante mais plus technique à mettre en oeuvre, elle a l'avantage de ne pas gaspiller de ressources et de ne pas avoir de temps de latence.L'objet observé notifie directement son changement d'état.
Tous les exemples seront donnés en C++ (pas en Arduino) et Processing (Java), juste pour pouvoir faire facilement des affichages.
Patron de conception observateur
Le patron de conception observateur (Design pattern Observer) donne une solution très souple pour réaliser ces échanges d'information entre deux objets et plus généralement entre des objets hétérogènes.
Partons sur deux objets pour commencer, l'objet qui observe l'autre est l'observateur, l'autre est l'observé. Il va falloir des nouvelles classe pour préciser les méthodes standard utilisée et donner des types à nos deux objets (pour le polymorphisme).
Commençons par l'observateur, celui ci a juste besoin d'une méthode qui sera appelée par l'observé quand son état change, l'observé n'a pas à savoir ce que l'observateur fera de l'information, c'est à l'observateur d'implémenter l'utilisation de cette information. Pour rendre le mécanisme général on a besoin d'une classe abstraite :
class Observateur { // observe les changements d'etat d'un objet
public:
virtual void etatChange(Observable* o)=0; // methode appelee sur l'observateur lors de changement d'etat
};
----------
interface Observateur { // observe les changements d'etat d'un objet
void etatChange(Observable o); // methode appelee sur l'Observateur lors de changement d'etat
}
En C++ une classe abstraite se fait avec une méthode virtuelle dite pure ( =0; ). La méthode abstraite etatChange() a un paramètre pointeur sur un objet observé (avec le polymorphisme, n'importe quel objet pourra être utilisé). En Java qui n'a pas d'héritage multiple, une classe n'ayant que des méthodes abstraites est un interface (permettant ainsi un ersatz d' héritage multiple indispensable ici).
Passons à l'observé, il a besoin d'une méthode pour notifier un changement à l'observateur, il faut aussi une méthode pour permettre à l'observateur de s'enregistrer auprès de lui. Plus délicat il faut une structure de données pour mémoriser les différents observateurs (il peut en avoir un nombre quelconque et de type quelconque). Un simple tableau ne convient pas, il faut un tableau extensible, fortuitement on trouve de tels objets en bibliothèque, "vecteur" en C++, "ArrayList" en Java. Cela donne :
class Observable {
vector<Observateur*> observateurs; // liste des observateurs
public:
void ajouterObservateur(Observateur* e) { observateurs.push_back(e); } // ajouter un observateur
void notifier(bool b) { for (auto & e : observateurs) e->etatChange(this); } // notifier aux observateurs
};
----------
class Observable {
ArrayList<Observateur> Observateurs=new ArrayList<Observateur>(); // liste des Observateurs
void ajouterObservateur(Observateur e) { Observateurs.add(e); } // ajouter un Observateur
void notifier(boolean b) { for (Observateur e: Observateurs) e.etatChange(this); } // notifier aux Observateurs
}
La méthode ajouterObservateur() rajoute le pointeur sur l'observateur au tableau extensible, la méthode notifier() appelle la méthode etatChange() sur tous les observateurs (noter le paramètre qui fourni à l'observateur un pointeur sur l'objet à l'initiative du changement d'état). En Java c'est très semblable à la version en C++, sauf pour la gestion des pointeurs et de l'écriture du "forall".
Utilisation
Voyons quelques exemples d'utilisation (inspirés du gestionnaire du Locoduinodrome), commençons par le dialogue entre une balise et une zone, pour cela il faut deux classes une pour les balises et une pour les zones :
class Balise : public Observateur {
bool etat=false;
public:
Balise(Zone& zone) { zone.ajouterObservateur(this); } // constructeur
void etatChange(Observable* o) { cout<<"balise : la zone observee a change\n"; }
};
class Zone : public Observable {
bool etat=false;
public:
Zone() {} // constructeur
void occuper() { etat=true;}
void liberer() { etat=false; notifier(etat); }
};
----------
class Balise implements Observateur {
boolean etat=false;
Balise(Zone z) { z.ajouterObservateur(this); } // constructeur
void etatChange(Observable o) { println("balise : la zone observee a change"); }
}
class Zone extends Observable {
boolean etat=false;
Zone() {} // constructeur
void occuper() { etat=true;}
void liberer() { etat=false; notifier(etat); }
}
La classe balise hérite de la classe abstraite Observateur et implémente la méthode abstraite etatChange(), méthode qui fait ici juste un affichage. Le constructeur reçoit une zone en paramètre et s'enregistre auprès de cette zone comme observateur.
La classe Zone hérite de la classe Observable qui lui fournit la gestion toute faite de la liste des observateurs. La méthode liberer() notifie l'information à tous les observateurs.
Reste plus que à faire un essai, pour cela il faut au moins une instance de Balise et une instance de Zone :
Zone zone;
Balise balise(zone);
----------
Zone zone=new Zone();
Balise balise=new Balise(zone);
on obtient à l'exécution dans la console :
balise : la zone observee a change
Bien évidemment on pourrait afficher aussi le nom des zones si elles avaient un nom. On pourrait aussi enregistrer d'autres balises sur d'autres zones (ou la même).
Autre utilisation
Les itinéraires ont aussi besoin de la libération des zones pour se détruire (libération de la dernière zone de l'itinéraire). On écrit facilement une classe Itineraire (sur le modèle de Balise) :
class Itineraire : public Observateur {
public:
Itineraire(Zone& zone) { zone.ajouterObservateur(this); } // constructeur
void etatChange(Observable* o) { cout<<"itineraire : detruit\n"; }
};
----------
class Itineraire implements Observateur {
Itineraire(Zone zone) { zone.ajouterObservateur(this); } // constructeur
void etatChange(Observable o) { println("itineraire : detruit"); }
}
il faut une instance :
Itineraire itineraire;
----------
Itineraire itineraire=new Itineraire();
et cela donne :
balise : la zone a change
itineraire : detruit
On a mis dans la liste des observés deux types différents, des balises et des itinéraires, c'est tout à fait normal car les balises et les itinéraires ont tous un type commun Observateur (on joue sur le polymorphisme). Ceci est rendu possible par le subtil jeu des types des classes Observateur et Observable et l'héritage multiple (ceci est un bon exemple de son utilité).
Dernière utilisation
Supposons que les zones aient besoin de savoir quand une aiguille est bougée. Il faut une classe Aiguille, sur le modèle de Zone :
class Aiguille : public Observable {
bool etat;
public:
void manoeuvrer() { etat=!etat; notifier(etat); }
};
----------
class Aiguille extends Observable {
boolean etat;
void manoeuvrer() { etat=!etat; notifier(etat); }
}
Maintenant zone devient aussi un observateur tout en continuant d'être observable, cela donne :
class Zone : public Observable,Observateur {
bool etat=false;
public:
Zone(Aiguille& aiguille) { aiguille.ajouterObservateur(this); } // constructeur
void etatChange(Observable* o) { cout<<"zone : l'aiguille a change\n"; }
void occuper() { etat=true;}
void liberer() { etat=false; notifier(etat); }
};
----------
class Zone extends Observable implements Observateur {
boolean etat=false;
Zone(Aiguille aiguille) {aiguille.ajouterObservateur(this); } // constructeur
void occuper() { etat=true;}
void liberer() { etat=false; notifier(etat); }
void etatChange(Observable o) { println("zone : l'aiguille a change"); }
}
zone hérite de deux classe (héritage multiple) donc a trois types pour l'objet : Zone, Observable et Observateur
Bilan
Ce patron est très souple mais très puissant, il permet des évolutions faciles à mettre en oeuvre. Dans la classe Observable on pourrait avoir plus de méthodes pour gérer la liste des observateurs (suppression par exemple …). On peut aussi spécialiser par héritage les classes Observateur et Observable.
Ce patron est utilisé dans les interfaces graphiques pour gérer les évènements, de souris par exemple. Il pourrait être aussi utilisé dans un gestionnaire de réseau.
Pierre