Appel de la bonne fonction dans une hiérarchie de classe

Le
Alexis Guillaume
Bonjour à tous.

Je dispose d'une hiérarchie de classe où chaque classe sait gérer
certains types de messages provenant du réseau. Bien sûr lorsque ces
messages me parviennent, je ne dispose que de pointeur sur la classe
de base.

Exemple fictif :

Base Messages gérés: evenement, identification
^
|
Dérivée1 Messages gérés: informations_joueurs, quitte
^
|
Dérivée2 Messages gérés: déplacement, tir,

Je suis donc à la recherche d'une solution pour que la bonne fonction
soit appelée quand j'écris du code du genre :

Base *base = .;
base->traite_message( type_message, message ) // (a)

Et ce en fonction du type de message.

Une première solution est de rendre traite_message virtuelle. Chaque
classe de la hiérarchie *doit* l'implémenter ainsi :

void ClasseDerivee::traite_message( std::string const &type_message,
Msg
message ) {
if (/* cette classe sait gérer ce type de message */) {
// gestion du message
} else {
ClasseParente::traite_message( type_message, message );
}
}

Ainsi, à chaque appel de traite_message, on est sûr que l'on passera
forcément par la classe qui sait traiter ce message.

Je trouve cette solution un peu bof. Ce qui m'ennuie surtout c'est
qu'il va être très facile de faire des erreurs dans le code, en se
trompant de classe parente par exemple, ou en oubliant de surchager
traite_message() dans une classe qui n'en traiterait aucun, ce qui
aurait quand même pour effet de casse les appels en chaînes à
traite_message().

Du coup, j'ai cherché une solution à base de map et de pointeurs de
fonction : La classe de base va gérer une map associant std::string
(supposé être un type de message) et pointeurs sur fonctions membres.
La méthode traite_message, non virtuelle, va utiliser cette map pour
appeler la bonne méthode.

-- code source --
#include <iostream>
#include <map>
#include <string>

class Base {
protected:
typedef void (Base::*msg_handler_t)( std::string const &type );
// Méthode appelée par les classes de la hiérarchie pour indiquer
// qu'elles savent gérer un certain type de message
void RegisterHandler( std::string const &type,
msg_handler_t handler )
{
m_type_to_handlers[ type ] = handler;
}
private:
typedef std::map<std::string, msg_handler_t> map_handlers_t;
map_handlers_t m_type_to_handlers;
void func_base( std::string const &type ) {
std::cerr << "Base::func_base";
}
public:
Base() {
RegisterHandler( "func_base", &Base::func_base );
}
void traite_message( std::string const &type ) {
map_handlers_t::iterator it = m_type_to_handlers.find( type );
if (it != m_type_to_handlers.end()) {
((*this).*(it->second))( type );
} else {
std::cerr << type << " not found.";
}
}
virtual ~Base() {}
};

class Derived1 : public Base {
void func_derived( std::string const &type ) {
std::cerr << "Derived1::func_derived";
}
public:
Derived1() {
// ****
RegisterHandler("func_derived",
(msg_handler_t)&Derived1::func_derived);
}
virtual ~Derived1() {}
};

class Derived2 : public Derived1 {
void func_derived2( std::string const &type ) {
std::cerr << "Derived2::func_derived2";
}
public:
Derived2() {
// *****
RegisterHandler("func_derived2",
(msg_handler_t)&Derived2::func_derived2);
}
virtual ~Derived2() {}
};

int main() {

Base *base1 = new Base();
Base *base2 = new Derived1();
Base *base3 = new Derived2();

base1->traite_message( "undefined" ); // not found
base1->traite_message( "func_base" ); // ok
base1->traite_message( "func_derived" ); // not found
base2->traite_message( "func_base" ); // ok
base2->traite_message( "func_derived" ); // ok
base2->traite_message( "func_derived2" );// not found
base3->traite_message( "func_derived2" );// ok

delete base1;
delete base2;
delete base3;
}

- fin code source --

Cette méthode semble fonctionner, mais j'ai un doute, notamment à
cause de la conversion que je dois faire aux lignes indiquée par des
astérisques. Cette conversion de void (*Derived::)(std::string const
&) à void (*Base::)( std::string const &) est-elle légale ?



Plus généralement, que pensez vous de ces deux manières de faire ?
Avez vous une préférence entre les deux, voire pour une troisième
méthode que je n'ai pas vue ?

Bonne lecture et bon week-end à tous,
Alexis Guillaume.
Vidéos High-Tech et Jeu Vidéo
Téléchargements
Vos réponses
Gagnez chaque mois un abonnement Premium avec GNT : Inscrivez-vous !
Trier par : date / pertinence
James Kanze
Le #17409451
On Oct 3, 11:33 am, Alexis Guillaume wrote:

Je dispose d'une hiérarchie de classe où chaque classe sait
gérer certains types de messages provenant du réseau. Bien sûr
lorsque ces messages me parviennent, je ne dispose que de
pointeur sur la classe de base.



Exemple fictif :



Base Messages gérés: evenement, identification
^
|
Dérivée1 Messages gérés: informations_joueurs, quitte
^
|
Dérivée2 Messages gérés: déplacement, tir, ...



Je suis donc à la recherche d'une solution pour que la bonne fonction
soit appelée quand j'écris du code du genre :



Base *base = ....;
base->traite_message( type_message, message ) // (a)



Et ce en fonction du type de message.



Une première solution est de rendre traite_message virtuelle.
Chaque classe de la hiérarchie *doit* l'implémenter ainsi :



void ClasseDerivee::traite_message( std::string const &type_message,
Msg
m essage ) {
if (/* cette classe sait gérer ce type de message */) {
// gestion du message
} else {
ClasseParente::traite_message( type_message, message );
}
}



Ainsi, à chaque appel de traite_message, on est sûr que l'on
passera forcément par la classe qui sait traiter ce message.



Je trouve cette solution un peu bof.



C'est cépendant une des solutions classiques. Surtout quand le
type de message est encodé à part. Sinon, on fait des messages
même un type polymorphique, avec une classe dérivée par type de
message, et on implémente l'idiome de « double dispatch ». À
peu près :

class MessageHandler ;

class Message
{
public:
virtual ~Message() {}
virtual void dispatch( MessageHandler* handler ) const
= 0 ;
} ;

class MessageA : public Message
{
public:
virtual void dispatch( MessageHandler* handler )
const ;
// ...
} ;
class MessageB : public Message { ... } ;
class MessageC : public Message { ... } ;

class MessageHandler
{
public:
virtual void notificationA( MessageA const* message ) =
0 ;
virtual void notificationB( MessageB const* message ) =
0 ;
virtual void notificationC( MessageC const* message ) =
0 ;
} ;

void
MessageA::dispatch(
MessageHandler* handler ) const
{
handler->notificationA( message ) ;
}

Ensuite, dans ton hièrarchie, chaque classe implemente les
fonctions de notification qui l'intéressent.

Ce qui m'ennuie surtout c'est qu'il va être très facile de
faire des erreurs dans le code, en se trompant de classe
parente par exemple, ou en oubliant de surchager
traite_message() dans une classe qui n'en traiterait aucun, ce
qui aurait quand même pour effet de casse les appels en
chaînes à traite_message().



Je me démande si une partie du problème n'est pas que tu as
conçu trop de niveaux. Je vois bien une classe de base
abstraite, du genre MessageHandler, ci-dessus, mais je ne vois
pas trop l'intérêt de créer une hièrarchie aussi profonde. En
général, il y a l'interface (la classe de base abstraite, qui
n'implémente rien), et ensuite, ses diverses implémentations,
tout à un niveau, ou éventuellement deux, si l'implémentation
utilise le modèle de template (« template method pattern », à
ne pas confondre avec les templates C++). Il y a bien sûr des
exceptions, mais à ta place, je m'assurerais que chaque niveau
correspond bien à un concepte signifiant. Donc, par exemple, en
reprenant ton exemple, à quel concepte correspondrait une classe
qui s'intéresserait à des informations joueurs, mais non aux
mouvements. Reduire la profondeur reduira nettement les
endroits où une erreur telle que tu décris pourrait se produire.
(S'il n'y a qu'une classe dérivée à partir d'une base totalement
abstraite, il n'y a pas besoin d'appeler le parent. Et s'il n'y
en a que deux, la deuxième ne peut pas trop se tromper en ce qui
concerne le parent à appeler.)

Du coup, j'ai cherché une solution à base de map et de
pointeurs de fonction : La classe de base va gérer une map
associant std::string (supposé être un type de message) et
pointeurs sur fonctions membres.



C'est faisable, mais c'est lourd. Et ça ne vaut que rarement la
peine.

Une des premières bibliothèques de fenêtrage, XViews, utlisait
quelque chose de ce genre. On s'inscrivait pour les types de
message intéressant, en passant le pointeur this et l'adresse
d'une fonction membre. La solution que je viens de décrire me
semble plus robuste et plus simple à mettre en oeuvre.

La méthode traite_message, non virtuelle, va utiliser cette
map pour appeler la bonne méthode.



-------------------------- code source -----------------------------


[coupé...]
------------------------- fin code source --------------------------



Cette méthode semble fonctionner, mais j'ai un doute,
notamment à cause de la conversion que je dois faire aux
lignes indiquée par des astérisques. Cette conversion de void
(*Derived::)(std::string const &) à void (*Base::)(
std::string const &) est-elle légale ?



Tu veux dire "void (Derived::*)( std::string const& )" à "void
(Base::*)( std::string const& )". Ça marche. (Je l'ai vérifié
quand j'ai rencontré XViews.) Mais à mon avis, c'est une
invitation à l'erreur (comme la plupart des conversions).

Plus généralement, que pensez vous de ces deux manières de
faire ? Avez vous une préférence entre les deux, voire pour
une troisième méthode que je n'ai pas vue ?



Franchement, je préfère la double dispatch (qui s'apparente au
modèle de visiteur) ; sinon, ta première solution n'est pas
vraiment mauvause non plus.

--
James Kanze (GABI Software) email:
Conseils en informatique orientée objet/
Beratung in objektorientierter Datenverarbeitung
9 place Sémard, 78210 St.-Cyr-l'École, France, +33 (0)1 30 23 00 34
pjb
Le #17414031
Alexis Guillaume
[...]
Base *base = ....;
base->traite_message( type_message, message ) // (a)

Et ce en fonction du type de message.



Ah, parce que chaque type de message a sa propre fonction?

Alors tu te trompe, il ne faut pas écrire
base->traite_message(type_message,message), il faut écrire simplement:

message->traite_toi_toi_meme(); // !!!!!


La seule question, c'est comment on passe d'un tampon contenant des
octets reçus, à un objet message de la bonne classe. Évidement, en
laissant le tampon se débrouiller:

abstract_message* message=tampon->transforme_toi_en_message();

Bien entendu, transforme_toi_en_message pourra utiliser un motif
fabrique (Factory Design Pattern), pour sélectionner et instancier un
message de la bonne classe, en fonction du contenu du tampon (this).


--
__Pascal Bourguignon__ http://www.informatimago.com/
-----BEGIN GEEK CODE BLOCK-----
Version: 3.12
GCS d? s++:++ a+ C+++ UL++++ P--- L+++ E+++ W++ N+++ o-- K- w---
O- M++ V PS PE++ Y++ PGP t+ 5+ X++ R !tv b+++ DI++++ D++
G e+++ h+ r-- z?
------END GEEK CODE BLOCK------
Alexis Guillaume
Le #17433431
Merci beaucoup à vous et à Pascal pour ces critiques utiles et ces
pistes intéressantes que je vais étudier avec le plus grand soin ! Je
constate avec plaisir qu'on peut toujours compter sur fclc++ pour ce
genre de questions ! :-)

Alexis Guillaume.
Alp Mestan
Le #17452871
On 3 oct, 21:07, (Pascal J. Bourguignon) wrote:
Alexis Guillaume > [...]
> Base *base = ....;
> base->traite_message( type_message, message ) // (a)

> Et ce en fonction du type de message.

Ah, parce que chaque type de message a sa propre fonction?

Alors tu te trompe, il ne faut pas crire
base->traite_message(type_message,message), il faut crire simplement:

message->traite_toi_toi_meme(); // !!!!!

La seule question, c'est comment on passe d'un tampon contenant des
octets re us, un objet message de la bonne classe. videment, en
laissant le tampon se d brouiller:

abstract_message* message=tampon->transforme_toi_en_message();

Bien entendu, transforme_toi_en_message pourra utiliser un motif
fabrique (Factory Design Pattern), pour s lectionner et instancier un
message de la bonne classe, en fonction du contenu du tampon (this).

--
__Pascal Bourguignon__ http://www.informatimago.com/
-----BEGIN GEEK CODE BLOCK-----
Version: 3.12
GCS d? s++:++ a+ C+++ UL++++ P--- L+++ E+++ W++ N+++ o-- K- w---
O- M++ V PS PE++ Y++ PGP t+ 5+ X++ R !tv b+++ DI++++ D++
G e+++ h+ r-- z?
------END GEEK CODE BLOCK------



Ca rejoint ce qu'a dit James à la fin de son message : effectivement
il semble judicieux d'appliquer un visitor à tes messages.
Publicité
Poster une réponse
Anonyme