Twitter iPhone pliant OnePlus 11 PS5 Disney+ Orange Livebox Windows 11

Modèle Observateur

28 réponses
Avatar
David Fleury
Bonjour,
j'essaye d'implémenter un modèle observateur, j'arrive
à ce qui suit mais est-il possible d'avoir au niveau
de l'observateur concret une méthode ayant pour signature le
type exact de l'observé et non pas l'interface IObservable ?
Dans le cas ou je peux avoir plusieurs observateurs et observés
concrets différents (comme ici, Canard, Chat, et Chasseur et naturaliste)

D'avoir quelque chose comme :
struct Naturaliste : IObserver {
void notified( const Canard* c ) {
cout << "I am The Naturaliste ["
<< this << "] notified by [" << (*c) << "]" << endl;
}
void notified( const Chat* c ) {
cout << "I am The Naturaliste ["
<< this << "] notified by [" << (*c) << "]" << endl;
}
};

ou peut être y a t-il une manière plus élégante de le faire
de manière générale.

David



// [CODE]
#include <iostream>
#include <list>
#include <algorithm>
#include <functional>
using namespace std;

struct IObservable;
ostream& operator << ( ostream& os, const IObservable& o );

// --------------------------------------------------------------------
struct IObserver {
virtual ~IObserver() {}
virtual void notified( const IObservable* o ) = 0;
};

// ---------------------------------------------------------------------
struct IObservable {
list<IObserver*> observers_;
void add( IObserver* o ) {
observers_.push_back( o );
}
void remove( IObserver* observer ) {
observers_.erase( std::remove( observers_.begin(),
observers_.end(),
observer ) );
}

virtual void display( ostream& os ) const = 0;

protected:
void notify() const {
for_each( observers_.begin(),
observers_.end(),
std::bind2nd( std::mem_fun( &IObserver::notified ),
this ) );
}
};

ostream& operator << ( ostream& os, const IObservable& o )
{
o.display( os );
return os;
}

// ---------------------------------------------------------------------
struct Canard : IObservable {
void CoinCoin()
{
cout << "Coin Coin" << endl;
notify();
}

void display( ostream& os ) const { os << "The Canard"; }
};

// ---------------------------------------------------------------------
struct Chat : IObservable {
void MiaouMiaou()
{
cout << "Miaou Miaou" << endl;
notify();
}
void display( ostream& os ) const { os << "The Chat"; }
};

// ---------------------------------------------------------------------
struct Chasseur : IObserver {
void notified( const IObservable* c ) {
cout << "I am The Chasseur [" << this
<< "] notified by [" << (*c) << "]" << endl;
}
};

struct Naturaliste : IObserver {
void notified( const IObservable* c ) {
cout << "I am The Naturaliste ["
<< this << "] notified by [" << (*c) << "]" << endl;
}
};

// ---------------------------------------------------------------------
int main() {
Chasseur c1, c2;
Naturaliste n1;

Canard canard;
canard.add( &c1 );
canard.add( &c2 );
canard.add( &n1 );

canard.CoinCoin();

canard.remove( &c2 );

canard.CoinCoin();

Chat chat;
chat.add( &n1 );

chat.MiaouMiaou();
}

10 réponses

1 2 3
Avatar
David Fleury
[...]

(Typiquement, le listeneur connaît le générateur
directement, plus ou moins, tandis que le générateur ne connaît
les listeneurs que parce qu'ils s'y sont inscrits. C'est
peut-être ce que tu voulais dire, et que j'ai mal compris.)


je suis d'accord, et je l'ai mal exprimé.

ce que tu décris est le modèle habituel et en fait le PO reprends bien
ce modèle.

les exemples du PO, par ses noms de classes me faisait penser à une
retournement dans le sens où, j'imagine il y a ici plus de générateurs
que de listeneurs.


Mes noms de classes étaient peut être mal choisies car trop proche.


or, pour un schéma classique, on a souvent peu de générateurs (peu
d'instances d'une même classe) et possiblement de nombreux listeners,
d'où un classique "addListener" (public) fourni par le générateur.


Dans mon cas, j'aurais plutôt plusieurs type de générateurs (plusieurs
classes ) pour un type de listeneurs, une instance qui concentrerait les
résultat en quelque sort.


dans le cas, où 1 ou 2 chasseurs / naturalistes écouteraient un groupe
de canards, j'aurais tendance à définir un "addObservable()" dans cette
classe listener - et cette méthode utiliserait un service ami du
générateur pour s'enregistrer, ceci donnant, imho, une meilleure vision
des priorités / responsabilités des agents.



Là, je ne vois pas trop, mais je vais continuer à faire quelques essais
sur mon cas concret sans vouloir faire une solution trop générale qui
n'aurait qu'un intérêt que théorique pour moi.

David


Avatar
David Fleury
"David Fleury" a écrit dans le message de news:
46aba941$0$10323$
Je suis pas sur de comprendre. La solution générale ne consiste-t-elle
pas, justement, à se restreindre à l'interface IObservable ?
Dans ce cas, dans la mesure où restreindre l'interface me donne plus de

travail, j'assimile plus ça à une spécialisation. Une généralisation du
problème devrait me faire mon travailler.


Eun non. Te restreindre à l'interface IObservable, ça veut dire faire avec
l'existant sans rien ajouter.




oui mais sans répondre à mon problème. ;)
du coup je serais obligé dans ma fonction de faire des cast à tout va
pour identifier mon type d'Observable, et j'aimerai éviter ça si
possible ( d'où mes essais jusqu'ici infructueux avec les templates)



Avatar
Sylvain
David Fleury wrote on 28/07/2007 22:52:

oui mais sans répondre à mon problème. ;)
du coup je serais obligé dans ma fonction de faire des cast à tout va
pour identifier mon type d'Observable, et j'aimerai éviter ça si
possible ( d'où mes essais jusqu'ici infructueux avec les templates)


je ne comprends pas pourquoi vous souhaitez identifier (et pire caster)
le type d'observable !

pouvez-vous motiver ce point?

Sylvain.

Avatar
Sylvain
David Fleury wrote on 28/07/2007 22:50:

or, pour un schéma classique, on a souvent peu de générateurs (peu
d'instances d'une même classe) et possiblement de nombreux listeners,
d'où un classique "addListener" (public) fourni par le générateur.


Dans mon cas, j'aurais plutôt plusieurs type de générateurs (plusieurs
classes ) pour un type de listeneurs, une instance qui concentrerait les
résultat en quelque sort.


dans c'est bien le second cas que je décrivais.

dans le cas, où 1 ou 2 chasseurs / naturalistes écouteraient un groupe
de canards, j'aurais tendance à définir un "addObservable()" dans
cette classe listener - et cette méthode utiliserait un service ami du
générateur pour s'enregistrer, ceci donnant, imho, une meilleure
vision des priorités / responsabilités des agents.


Là, je ne vois pas trop, mais je vais continuer à faire quelques essais
sur mon cas concret sans vouloir faire une solution trop générale qui
n'aurait qu'un intérêt que théorique pour moi.


l'idée est de ne pas avoir à coder:

monBestiau1.addlistener(theObserver);
monBestiau2.addlistener(theObserver);
monBestiau3.addlistener(theObserver);
...
monBestiauN.addlistener(theObserver);

mais plutôt, ayant un groupe de bestiaux (disons Bestiole* ou
whateverSet<Bestiole>) faire un seul:

myObserver.listens(myBestioles);

c'est une simple question d'écriture (pour le code applicatif et non la
toutouille interne).

Sylvain.


Avatar
David Fleury
David Fleury wrote on 28/07/2007 22:52:

oui mais sans répondre à mon problème. ;)
du coup je serais obligé dans ma fonction de faire des cast à tout va
pour identifier mon type d'Observable, et j'aimerai éviter ça si
possible ( d'où mes essais jusqu'ici infructueux avec les templates)


je ne comprends pas pourquoi vous souhaitez identifier (et pire caster)
le type d'observable !

pouvez-vous motiver ce point?


SI j'ai par exemple des capteurs différentes, par exemple
class Pression : public IObservable {
public: int getPression() const { return ... ; }
...
};

et
class Temperature : public IObservable {
public: int getTemperature() const { return ... ; }
...
};

Pour récupérer l'état de chacun des capteurs, je devrais utiliser une
méthode diférents (ou sinon peut être utiliser un Adapteur pour rendre
l'accès à l'état de mon Observable uniforme).

Dans le cas où j'ai des accesseurs différents en nombre, je risque
d'avoir d'autres soucis.

Alors qu'en castant, j'aurais directement l'interface (Pression ou
Temperature), donc accès directement à mes méthodes permettant
d'extraire tous les état de mes capteurs.


Avatar
Sylvain
David Fleury wrote on 29/07/2007 10:50:

SI j'ai par exemple des capteurs différentes, par exemple
class Pression : public IObservable {
public: int getPression() const { return ... ; }
};

class Temperature : public IObservable {
public: int getTemperature() const { return ... ; }
};

Pour récupérer l'état de chacun des capteurs, je devrais utiliser une
méthode diférents (ou sinon peut être utiliser un Adapteur pour rendre
l'accès à l'état de mon Observable uniforme).


ok, je comprends maintenant le point, et le post initial.
reste ce qui semble vous géner, car ici je vous trouve pas si inélégant
la multiplication des interfaces listener.

l'écriture finale peut suivre différents choix, dont:
- une interface avec paramètre polymorphe d'une classe Obeservable,
- une interface avec paramètre polymorphe d'une classe Evenement,
- n interfaces avec paramètre explicite (votre PO)
- n interfaces avec paramètre Evenement explicite.

j'aurais tendance à choisir la dernière.

dans votre PO, les générateurs d'évenements héritent de IObservable,
cela fait sens pour gérer la liste des listeners une fois pour toutes
mais pour autant vous choississez de transmettre les types finaux aux
listeners, c'est peut être ce choix qu'il faut affiner.

parmi les options possibles, IObservable ne concerne que la gestion de
la liste des listeners; les classes concrêtes devraient hériter de
manière protégée de cette classe et vous définirez comme actuellement
autant de méthode de notification qu'il existe de classe notifiante,
mais contrairement à votre PO vous auriez intérêt à mieux caractériser
les interfaces, par exemple à la place d'un unique IObserver, définir:

struct PressureListener {
virtual void notifyPressure(double value) = null;
};
struct TemperatureListener {
virtual void notifyTemperature(double value) = null;
};

struct Naturaliste : PressureListener, TemperatureListener {
...
};

Naturaliste implémentera n interfaces mais chacune sera claire et
immédiatement compréhensible - le fait de transmettre un paramètre
valeur (pour les mesures intensives ou extensibles) clarifira également
l'ensemble.

autre approche possible, si les évenement ssont assez simple, définir
une structure évenement commune, par exemple:

struct Event {
enum EventType {
TEMPERATURE,
PRESSURE,
...
};
EventType type;
double value;
[ when, why, ?, ... ]
// éventuellement, mais devrais être évité
// pour ne pas créer des dépendances inutiles
IObservable* source;
};

Observer est alors réduit à:

struct IObserver {
virtual void notify(Event&) = null;
};

ou, si event très simple:

struct IObserver {
virtual void notify(int /*ou enum*/ type, double value) = null;
};

et Naturaliste switchera, si besoin, sur le type de l'event.
cette façon de faire peut convenir si vous définissez un système où les
types d'évènements sont connus / fixés d'avance; elle sera par contre
pénalisante si le système gère des events de type très différents (pas
de struct Event simple) ou si ces types d'events peuvent évoluer dans la
vie du code (danger d'oublier un switch).

Alors qu'en castant, j'aurais directement l'interface (Pression ou
Temperature), donc accès directement à mes méthodes permettant
d'extraire tous les état de mes capteurs.


certes, cependant lors de la génération d'un évènement distinct vous
disposez de toutes les informations: cet event est d'un type particulier
et est généré par une classe distincte; si on est obligé de caster la
source ou l'event lors de son traitement c'est que l'on a fait exprès de
perdre cette information (ou que l'on a oublié de la préserver); c'est
la raison pour laquelle je n'aimerais pas ce modèle.

l'observateur final ne peut pas / ne doit pas traiter une pression comme
un température, il fait donc sens (il n'est pas génant) d'avoir 2
interfaces distinctes pour recevoir ces 2 types d'events.

Sylvain.

Avatar
David Fleury

[...]
Merci, c'est vraiment intéressant, je vais étudier plus en détail
tout ça.


parmi les options possibles, IObservable ne concerne que la gestion de
la liste des listeners; les classes concrêtes devraient hériter de
manière protégée de cette classe et vous définirez comme actuellement
autant de méthode de notification qu'il existe de classe notifiante,
mais contrairement à votre PO vous auriez intérêt à mieux caractériser
les interfaces, par exemple à la place d'un unique IObserver, définir:

struct PressureListener {
virtual void notifyPressure(double value) = null;
};
struct TemperatureListener {
virtual void notifyTemperature(double value) = null;
};

struct Naturaliste : PressureListener, TemperatureListener {
....
};

Naturaliste implémentera n interfaces mais chacune sera claire et
immédiatement compréhensible - le fait de transmettre un paramètre
valeur (pour les mesures intensives ou extensibles) clarifira également
l'ensemble.


Je crois que ma préférence va à cette solution, les événements n'étant
pas simple et surtout potentiellement sans rapport.



autre approche possible, si les évenement ssont assez simple, définir
une structure évenement commune, par exemple:

struct Event {
enum EventType {
TEMPERATURE,
PRESSURE,
...
};
EventType type;
double value;
[ when, why, ?, ... ]
// éventuellement, mais devrais être évité
// pour ne pas créer des dépendances inutiles
IObservable* source;
};

Observer est alors réduit à:

struct IObserver {
virtual void notify(Event&) = null;
};

ou, si event très simple:

struct IObserver {
virtual void notify(int /*ou enum*/ type, double value) = null;
};

et Naturaliste switchera, si besoin, sur le type de l'event.
cette façon de faire peut convenir si vous définissez un système où les
types d'évènements sont connus / fixés d'avance; elle sera par contre
pénalisante si le système gère des events de type très différents (pas
de struct Event simple) ou si ces types d'events peuvent évoluer dans la
vie du code (danger d'oublier un switch).

Alors qu'en castant, j'aurais directement l'interface (Pression ou
Temperature), donc accès directement à mes méthodes permettant
d'extraire tous les état de mes capteurs.


certes, cependant lors de la génération d'un évènement distinct vous
disposez de toutes les informations: cet event est d'un type particulier
et est généré par une classe distincte; si on est obligé de caster la
source ou l'event lors de son traitement c'est que l'on a fait exprès de
perdre cette information (ou que l'on a oublié de la préserver); c'est
la raison pour laquelle je n'aimerais pas ce modèle.

l'observateur final ne peut pas / ne doit pas traiter une pression comme
un température, il fait donc sens (il n'est pas génant) d'avoir 2
interfaces distinctes pour recevoir ces 2 types d'events.

Sylvain.


Merci encore, pour tous ces points de conception dont je n'avais pas
réussi à tous identifier aussi clairement.

David.


Avatar
Michael DOUBEZ
David Fleury wrote on 28/07/2007 22:52:

oui mais sans répondre à mon problème. ;)
du coup je serais obligé dans ma fonction de faire des cast à tout va
pour identifier mon type d'Observable, et j'aimerai éviter ça si
possible ( d'où mes essais jusqu'ici infructueux avec les templates)


je ne comprends pas pourquoi vous souhaitez identifier (et pire
caster) le type d'observable !

pouvez-vous motiver ce point?


SI j'ai par exemple des capteurs différentes, par exemple
class Pression : public IObservable {
public: int getPression() const { return ... ; }
...
};

et
class Temperature : public IObservable {
public: int getTemperature() const { return ... ; }
...
};

Pour récupérer l'état de chacun des capteurs, je devrais utiliser une
méthode diférents (ou sinon peut être utiliser un Adapteur pour rendre
l'accès à l'état de mon Observable uniforme).

Dans le cas où j'ai des accesseurs différents en nombre, je risque
d'avoir d'autres soucis.

Alors qu'en castant, j'aurais directement l'interface (Pression ou
Temperature), donc accès directement à mes méthodes permettant
d'extraire tous les état de mes capteurs.


Si j'ai bien compris, il s'agit d'une composition de pattern observer et
visitor. L'observer permet d'être notifié d'un évènement, et le visitor
d'appliquer la bonne méthode d'observation.

Peut être pourriez vous considérer de séparer ces deux responsabilités
(les capteurs sont observable et visitables) et l'implémentation de
l'observer utiliserais les fonctions de visitation.

Michael



Avatar
Michael DOUBEZ

[...]
Merci, c'est vraiment intéressant, je vais étudier plus en détail
tout ça.


parmi les options possibles, IObservable ne concerne que la gestion de
la liste des listeners; les classes concrêtes devraient hériter de
manière protégée de cette classe et vous définirez comme actuellement
autant de méthode de notification qu'il existe de classe notifiante,
mais contrairement à votre PO vous auriez intérêt à mieux caractériser
les interfaces, [snip]

Naturaliste implémentera n interfaces mais chacune sera claire et
immédiatement compréhensible - le fait de transmettre un paramètre
valeur (pour les mesures intensives ou extensibles) clarifira
également l'ensemble.


Je crois que ma préférence va à cette solution, les événements n'étant
pas simple et surtout potentiellement sans rapport.



autre approche possible, si les évenement ssont assez simple, définir
une structure évenement commune [snip]
et Naturaliste switchera, si besoin, sur le type de l'event.



En général, j'utilise une combinaison des deux: passer l'objet typé
permet d'extraire d'autres informations et l'enum donne la raison de
l'évènement (valeur, erreur, timeout ...). Ca évite d'avoir à faire
toute une logique de déduction de l'état avec historique: par exemple,
je suis dans un état d'erreur mais je l'étais pas avant ... tout en
gardant raisonnable le nombre de fonction à définir dans le functor
callback.
Si l'objet a un diagramme d'état (ce qui est souvent le cas avec
l'observer), c'est encore plus facile, je transmet l'id de la transition
causant la notification.

Michael


Avatar
David Fleury
Bonsoir,

voici à quoi j'arrive finalement
qui ressemble assez bien à ce que je voulais au détail
près que ça ne compile pas sous VS2005
(la fonction notify et une conversion IObservable<Observer>
vers Canard ou Chat)
alors que ça passe bien sous gcc 3.4 et comeau

[CODE]
#include <iostream>
#include <list>
#include <algorithm>
#include <functional>
using namespace std;

//
---------------------------------------------------------------------------
template< typename Observable >
struct IObserver
{
virtual ~IObserver() {};
virtual void notified( const Observable* ) = 0;
};

//
---------------------------------------------------------------------------
template< typename Observable >
struct IObservable {
list<IObserver<Observable> *> observers_;
void add( IObserver<Observable>* o ) {
observers_.push_back( o );
}
void remove( IObserver<Observable>* observer ) {
observers_.erase( std::remove( observers_.begin(),
observers_.end(),
observer ) );
}

protected:
void notify() const {
for_each( observers_.begin(),
observers_.end(),
std::bind2nd( std::mem_fun(
&IObserver<Observable>::notified ),
this ) );
}
};

// ---------------------------------------------------------------------
struct Canard : IObservable<Canard> {
string name_;
Canard( const string& name ) : name_( name ) {}

void CoinCoin()
{
cout << "Coin Coin" << endl;
notify();
}
};

// ---------------------------------------------------------------------
struct Chat : IObservable<Chat> {
string name_;
Chat( const string& name ) : name_( name ) {}

void MiaouMiaou()
{
cout << "Miaou Miaou" << endl;
notify();
}
};

// ---------------------------------------------------------------------
struct Chasseur : IObserver<Canard>, IObserver<Chat> {
string name_;

Chasseur( const string& name )
: name_( name )
{}

void notified( const Canard* c ) {
cout << "I am " << name_ << " the Chasseur notified by "
<< (*c).name_ << " the Canard" << endl;
}
void notified( const Chat* c ) {
cout << "I am " << name_ << " the Chasseur notified by "
<< (*c).name_ << " the Chat" << endl;
}
};

// ---------------------------------------------------------------------
struct Naturaliste : IObserver<Canard>, IObserver<Chat> {
string name_;

Naturaliste( const string& name )
: name_( name )
{}

void notified( const Canard* c ) {
cout << "I am " << name_ << " the Naturalist notified by "
<< (*c).name_ << " the Canard" << endl;
}

void notified( const Chat* c ) {
cout << "I am " << name_ << " the Naturalist notified by "
<< (*c).name_ << " the Chat" << endl;
}
};

// ---------------------------------------------------------------------
int main() {
Chasseur c1( "Pierre" ), c2( "Paul" );
Naturaliste n1( "Jacques" );

Canard canard( "Daffy" );
canard.add( &c1 );
canard.add( &c2 );
canard.add( &n1 );

canard.CoinCoin();

canard.remove( &c2 );

canard.CoinCoin();

Chat chat( "Gros Minet" );
chat.add( &n1 );

chat.MiaouMiaou();
}
1 2 3