 |
C++Talk.NET C++ language newsgroups
|
| View previous topic :: View next topic |
| Author |
Message |
Aurelien Regat-Barrel Guest
|
Posted: Tue Sep 27, 2005 2:08 pm Post subject: Conception modulaire |
|
|
Bonjour,
Je suis en pleine réflexion sur comment gérer un projet composé de
nombreux modules, assemblés à la demande du client. J'ai beaucoup de mal
à trouver de la documentation là dessus.
Pourtant c'est assez courant je pense. Je me demande ainsi : comment
gérez vous les multiples versions simultanées de vos logiciels, je veux
dire, comment structurer son code pour avoir au final plusieurs build:
- version de base
- version complète
- version pro de luxe
- version sur mesure pour client X
- ...
Là où je tatonne c'est au niveau des dépendances / utilisation
inter-modules.
Actuellement, j'ai un objet central Application, qui concentre tous les
modules compilés. Une fonction globale App() renvoie l'instance. Par
exemple, pour utiliser les services du module base de données, un module
X fait:
class Application
{
public:
//...
#ifdef USE_DATABASE_MODULE
DatabaseMgr * Database();
#endif
//...
};
Application* App();
class X
{
void Toto()
{
App()->Database()->...
}
};
Vous imaginez les problèmes soulevés par la gestion des dépendances à
coup de #ifdef.
Je compte migrer vers quelque chose comme celà:
- pour chaque module, une classe ModuleUser donne le droit d'utiliser le
module. Il faut dériver de cette classe pour pouvoir utiliser la
fonction membre donnant accès à l'instance du module.
class DatabaseUser
{
protected:
DatabaseMgr* Database();
};
class X : public DatabaseUser
{
void Toto()
{
this->Database()->...
}
};
C'est moins pratique à l'utilisation (il faut dériver sa classe
utilisatrice...) mais ça me parrait pas trop mal.
Qu'en pensez-vous ? Que faites / feriez vous ?
--
Aurélien Regat-Barrel
|
|
| Back to top |
|
 |
Stan Guest
|
Posted: Tue Sep 27, 2005 2:16 pm Post subject: Re: Conception modulaire |
|
|
"Aurelien Regat-Barrel" <nospam.aregatba (AT) yahoo (DOT) fr> a écrit dans le message
de news: 43395255$0$22383$626a14ce (AT) news (DOT) free.fr...
| Quote: | Bonjour,
Je suis en pleine réflexion sur comment gérer un projet composé de
nombreux modules, assemblés à la demande du client. J'ai beaucoup de mal à
trouver de la documentation là dessus.
Pourtant c'est assez courant je pense. Je me demande ainsi : comment gérez
vous les multiples versions simultanées de vos logiciels, je veux dire,
comment structurer son code pour avoir au final plusieurs build:
- version de base
- version complète
- version pro de luxe
- version sur mesure pour client X
- ...
Là où je tatonne c'est au niveau des dépendances / utilisation
inter-modules.
[...]
Qu'en pensez-vous ? Que faites / feriez vous ?
|
Dans un premier temps, j'exploiterais à fond les namespace.
--
-Stan
|
|
| Back to top |
|
 |
Aurelien Regat-Barrel Guest
|
Posted: Tue Sep 27, 2005 2:50 pm Post subject: Re: Conception modulaire |
|
|
| Quote: | Dans un premier temps, j'exploiterais à fond les namespace.
|
Si ça peut t'éclairer (?), chaque module est déclaré dans un namespace
qui lui est propre, ce qui en fait une vingtaine au total.
Je vois pas trop le rapport en fait.
--
Aurélien Regat-Barrel
|
|
| Back to top |
|
 |
Stan Guest
|
Posted: Tue Sep 27, 2005 3:48 pm Post subject: Re: Conception modulaire |
|
|
"Aurelien Regat-Barrel" <nospam.aregatba (AT) yahoo (DOT) fr> a écrit dans le message
de news: 43395c50$0$23813$626a14ce (AT) news (DOT) free.fr...
| Quote: | Dans un premier temps, j'exploiterais à fond les namespace.
Si ça peut t'éclairer (?), chaque module est déclaré dans un namespace qui
lui est propre, ce qui en fait une vingtaine au total.
Je vois pas trop le rapport en fait.
|
Si tu as une flopée de classes ou de fonctions
qui sont selectionnées par un jeu de #idef, #endif,
il me semble qu'utiliser des using-déclaration présente
plus d'avantages.
--
-Stan
|
|
| Back to top |
|
 |
Patrick 'Zener' Brunet Guest
|
Posted: Tue Sep 27, 2005 4:44 pm Post subject: Re: Conception modulaire |
|
|
Bonjour.
Je réponds à Aurelien Regat-Barrel <nospam.aregatba (AT) yahoo (DOT) fr>
qui dans 43395255$0$22383$626a14ce (AT) news (DOT) free.fr a écrit :
| Quote: | Bonjour,
Je suis en pleine réflexion sur comment gérer un projet composé de
nombreux modules, assemblés à la demande du client. J'ai beaucoup de
mal à trouver de la documentation là dessus.
Pourtant c'est assez courant je pense. Je me demande ainsi : comment
gérez vous les multiples versions simultanées de vos logiciels, je
veux dire, comment structurer son code pour avoir au final plusieurs
build:
- version de base
- version complète
- version pro de luxe
- version sur mesure pour client X
- ...
Là où je tatonne c'est au niveau des dépendances / utilisation
inter-modules.
|
J'ai une certaine réflexion là-dessus, mais qui conduit à des pratiques de
perfectionniste. Il faut aimer...
Il y a plusieurs types de séparations à envisager :
- découpage en modules fonctionnels au sens du produit,
- séparation pour partage des utilitaires de type "librairie",
- découpage fonctionnel selon MVC (Model-View-Controler), proche du premier
type,
- séparation des ressources de localisation (langues, protocoles, etc.) pour
chacun des précédents modules,
- etc.
Quand j'arrive à ça, j'ai très envie de couper tous les liens statiques
entre modules, et de garantir la modularité par un assemblage de type
"contrat fonctionnel", comme dans l'implémentation initiale de COM (avec les
interfaces formelles implémentées par des classes imbriquées, avant que les
ATL ne nous fassent un grand mix).
Il est parfaitement possible de faire ça en C++ 100% system-independent,
sans IDL, avec simplement des interfaces formelles définies comme des
bundles de fonctions virtual=0, et fournies dans des headers individuels de
même nom, pouvant aussi en fournir la typologie, les constantes, etc.
Alors bien sûr ça conduit à faire de la connexion dynamique des modules même
quand les sources sont compilés ensemble, donc de manipuler des fonctions au
bout de pointeurs d'interfaces au lieu de faire du linkage statique
classique.
Mais en contrepartie, les liens entre modules sont 100% basés sur leurs
interfaces formelles. Tout module est substituable à fonctionnalités
équivalentes, et tout module dont les interfaces ne sont pas invoquées peut
être retiré du projet en bloc (donc 0 code mort).
Donc vous pouvez vraiment assembler une version sur mesure comme vous le
disiez.
Votre avis ?
Cordialement,
--
/***************************************
* Patrick BRUNET
* E-mail: lien sur http://zener131.free.fr/ContactMe
***************************************/
|
|
| Back to top |
|
 |
kanze Guest
|
Posted: Wed Sep 28, 2005 8:01 am Post subject: Re: Conception modulaire |
|
|
Aurelien Regat-Barrel wrote:
| Quote: | Je suis en pleine réflexion sur comment gérer un projet
composé de nombreux modules, assemblés à la demande du client.
J'ai beaucoup de mal à trouver de la documentation là dessus.
Pourtant c'est assez courant je pense. Je me demande ainsi :
comment gérez vous les multiples versions simultanées de vos
logiciels, je veux dire, comment structurer son code pour
avoir au final plusieurs build:
- version de base
- version complète
- version pro de luxe
- version sur mesure pour client X
- ...
Là où je tatonne c'est au niveau des dépendances / utilisation
inter-modules.
Actuellement, j'ai un objet central Application, qui concentre
tous les modules compilés.
Une fonction globale App() renvoie
l'instance. Par exemple, pour utiliser les services du module
base de données, un module X fait:
class Application
{
public:
//...
#ifdef USE_DATABASE_MODULE
DatabaseMgr * Database();
#endif
//...
};
Application* App();
class X
{
void Toto()
{
App()->Database()->...
}
};
Vous imaginez les problèmes soulevés par la gestion des
dépendances à coup de #ifdef.
|
Jusqu'à là, c'est bien. Mais je ne vois pas d'où vient les
#ifdef. Ce que je ferais, c'est un map statique des modules,
chargement dynamiquement par le constructeur d'un objet statique
dans chaque module. Ton code Application::Database() serait
alors quelque chose du genre :
DatabaseMgr*
Application::Database()
{
Map::const_iterator module = ourMap.find( databaseId ) ;
return module == ourMap.end()
? NULL
: static_cast< DatabaseMgr* >( module->second ) ;
}
La « configuration » se ferait lors de l'édition de liens, où tu
incorporeras ou non l'objet static d'initialisation de tel ou
tel module.
Alternativement, tu pourrais faire à peu près pareil avec des
objets dynamiques. Quand la fonction (par exemple
Application::Database) ne trouve pas l'entrée dans le map, il
essaie de charger l'objet dynamique correspondant (qui
s'insérera dans le map) avant de retourner NULL. Dans ce cas-ci,
la « configuration » ne se fait que lors du packaging : tu
inclus plus ou moins d'objets dynamiques dans le package.
Dans ce cas-là, la fonction devient :
DatabaseMgr*
Application::Database()
{
Map::const_iterator module = ourMap.find( databaseId ) ;
if ( module == ourMap.end() ) {
tryToLoadModule( databaseId ) ;
module = ourMap.find( databaseId ) ;
}
return module == ourMap.end()
? NULL
: static_cast< DatabaseMgr* >( module->second ) ;
}
| Quote: | Je compte migrer vers quelque chose comme celà:
- pour chaque module, une classe ModuleUser donne le droit d'utiliser le
module. Il faut dériver de cette classe pour pouvoir utiliser la
fonction membre donnant accès à l'instance du module.
class DatabaseUser
{
protected:
DatabaseMgr* Database();
};
class X : public DatabaseUser
{
void Toto()
{
this->Database()->...
}
};
C'est moins pratique à l'utilisation (il faut dériver sa
classe utilisatrice...) mais ça me parrait pas trop mal.
|
Ça me paraît un peu lourd, mais ça pourrait marcher aussi.
N'empèche que je chercherais à différer la décision au plus tard
possible, à l'édition de liens ou au packaging, et non dans le
code.
| Quote: | Qu'en pensez-vous ? Que faites / feriez vous ?
|
J'ai un cas déjà où j'utilise la technique des objets dynamiques
(pour d'autres raisons -- dans mon cas, il faut pouvoir ajouter
des modules par la suite sans réinstaller tout).
--
James Kanze GABI Software
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
|
|
| Back to top |
|
 |
Aurelien Regat-Barrel Guest
|
Posted: Wed Sep 28, 2005 8:23 am Post subject: Re: Conception modulaire |
|
|
| Quote: | J'ai une certaine réflexion là-dessus, mais qui conduit à des pratiques de
perfectionniste. Il faut aimer...
Il y a plusieurs types de séparations à envisager :
- découpage en modules fonctionnels au sens du produit,
- séparation pour partage des utilitaires de type "librairie",
- découpage fonctionnel selon MVC (Model-View-Controler), proche du premier
type,
- séparation des ressources de localisation (langues, protocoles, etc.) pour
chacun des précédents modules,
- etc.
Quand j'arrive à ça, j'ai très envie de couper tous les liens statiques
entre modules, et de garantir la modularité par un assemblage de type
"contrat fonctionnel", comme dans l'implémentation initiale de COM (avec les
interfaces formelles implémentées par des classes imbriquées, avant que les
ATL ne nous fassent un grand mix).
|
Je me suis un peu inspiré de COM. J'ai pas mal d'ABC qui me permettent
de séparer l'implémentation du module de son utilisation. Par exemple,
pour la database, j'ai un module "abstrait" database, et 2 modules
"concrets" SqlServerDataBase et OracleDataBase. Les modules ne savent
pas lequel ils utilisent (choix au moment de la compilation). C'est géré
via cet objet central Application. On peut voir ça ainsi:
class Application
{
public:
virtual DatabaseMgr * Database() = 0;
};
class ApplicationCore : public Application
{
virtual DatabaseMgr * Database()
{
#ifdef USE_SQL_SERVER
static SqlServerDataBaseMgr mgr;
#elif defined USE_ORACLE
static OracleDataBaseMgr mgr;
#endif
return &mgr;
}
};
Je suis obligé de séparer l'objet Application (que tous les modules
utilisent) de son implémentation (= ApplicationCore), car sinon on
aurait des dépendances circulaires (le module database utilise l'objet
Application, mais ce dernier utilise le module database...).
| Quote: | Il est parfaitement possible de faire ça en C++ 100% system-independent,
sans IDL, avec simplement des interfaces formelles définies comme des
bundles de fonctions virtual=0, et fournies dans des headers individuels de
même nom, pouvant aussi en fournir la typologie, les constantes, etc.
Alors bien sûr ça conduit à faire de la connexion dynamique des modules même
quand les sources sont compilés ensemble, donc de manipuler des fonctions au
bout de pointeurs d'interfaces au lieu de faire du linkage statique
classique.
Mais en contrepartie, les liens entre modules sont 100% basés sur leurs
interfaces formelles. Tout module est substituable à fonctionnalités
équivalentes, et tout module dont les interfaces ne sont pas invoquées peut
être retiré du projet en bloc (donc 0 code mort).
|
Je suis tout à fait d'accord. Je m'oriente vers un système de plugin,
même si au final tout sera linké dans un seul et même exe. Dans ma
version debug, j'ai bien de multiples dll chargées à l'initialisation du
programme. En release, tout est lié en statique.
| Quote: | Donc vous pouvez vraiment assembler une version sur mesure comme vous le
disiez.
Votre avis ?
|
Je suis parfaitement d'accord. Ma question est : comment faire ?
Créer les modules, ce n'est pas le problème. Mon problème, c'est les
assembler. Comment gérer ça ? Comment gérer le fait que un coup il peut
y avoir le module X, un coup il peut ne pas y être (assemblage à la
demande). Or ce module X doit être mis à jour en fonction des données
générées par le module Y, qui lui même peut ou non être présent...
*** Comment, une fois chargé, le module X va-t-il récupérer un pointeur
sur l'instance de Y ? ***
Actuellement, j'ai un objet central Application qui regroupe tous les
modules présents. Mais ça devient impossible à gérer.
Car il y a ce problèmes de cicles de dépendances : X utilise Application
pour obtenir Y, mais Application utilise X pour mettre X à disposition
des autres...
Je pars sur une idée d'enregistrement dynamique auprès de l'Application,
et de référencement via un nom sous forme de chaine de caractères, et le
dynamic_cast qui va bien.
class ModuleBase
{
public:
virtual std::string GetName() const = 0;
template<typename T>
T * Downcast()
{
return dynamic_cast<T*>( this );
}
};
class Application
{
public:
void RegisterModule( ModuleBase *M );
M * GetModule( std::string Name );
};
// *****
class ModuleX
{
void Toto()
{
// on utilise la base de données
DataBaseMgr *db = App()->GetModule( "DataBase"
)->Downcast<DataBaseMgr>();
}
}
ModuleBase c'est un peu IUnknown, et Application::GetModule joue le rôle
de CoCreateInstance.
--
Aurélien Regat-Barrel
|
|
| Back to top |
|
 |
Aurelien Regat-Barrel Guest
|
Posted: Wed Sep 28, 2005 8:27 am Post subject: Re: Conception modulaire |
|
|
| Quote: | Si tu as une flopée de classes ou de fonctions
qui sont selectionnées par un jeu de #idef, #endif,
il me semble qu'utiliser des using-déclaration présente
plus d'avantages.
|
Je ne suis pas sûr que tu ais compris mon problème.
--
Aurélien Regat-Barrel
|
|
| Back to top |
|
 |
Stan Guest
|
Posted: Wed Sep 28, 2005 9:33 am Post subject: Re: Conception modulaire |
|
|
"Aurelien Regat-Barrel" <nospam.aregatba (AT) yahoo (DOT) fr> a écrit dans le message
de news:433a5411$0$4337$626a14ce (AT) news (DOT) free.fr...
| Quote: | Si tu as une flopée de classes ou de fonctions
qui sont selectionnées par un jeu de #idef, #endif,
il me semble qu'utiliser des using-déclaration présente
plus d'avantages.
Je ne suis pas sûr que tu ais compris mon problème.
|
Je ne te présente pas les namespaces comme étant _la_ solution
à ton problème de modularité, mais ce qui m'étonne un peu
c'est que tu dis que chaque module est déclaré dans un namespace
qui lui est propre, et d'un autre côté tu utlises
une structure du type :
class ApplicationCore : public Application
{
virtual DatabaseMgr * Database()
{
#ifdef USE_SQL_SERVER
static SqlServerDataBaseMgr mgr;
#elif defined USE_ORACLE
static OracleDataBaseMgr mgr;
#endif
return &mgr;
}
};
Mais c'est pas grave vu que cela disparaitra dans ta nouvelle mouture...
Par contre, je qui reste flou pour moi, c'est est-ce que tu souhaites
une gestion des modules à l'execution ou à la compilation uniquement.
--
-Stan
|
|
| Back to top |
|
 |
Aurelien Regat-Barrel Guest
|
Posted: Wed Sep 28, 2005 10:11 am Post subject: Re: Conception modulaire |
|
|
| Quote: | Jusqu'à là, c'est bien. Mais je ne vois pas d'où vient les
#ifdef.
Ils sont définis en fonction des modules à compiler. |
| Quote: | Ce que je ferais, c'est un map statique des modules,
chargement dynamiquement par le constructeur d'un objet statique
dans chaque module. Ton code Application::Database() serait
alors quelque chose du genre :
DatabaseMgr*
Application::Database()
{
Map::const_iterator module = ourMap.find( databaseId ) ;
return module == ourMap.end()
? NULL
: static_cast< DatabaseMgr* >( module->second ) ;
}
La « configuration » se ferait lors de l'édition de liens, où tu
incorporeras ou non l'objet static d'initialisation de tel ou
tel module.
Alternativement, tu pourrais faire à peu près pareil avec des
objets dynamiques. Quand la fonction (par exemple
Application::Database) ne trouve pas l'entrée dans le map, il
essaie de charger l'objet dynamique correspondant (qui
s'insérera dans le map) avant de retourner NULL. Dans ce cas-ci,
la « configuration » ne se fait que lors du packaging : tu
inclus plus ou moins d'objets dynamiques dans le package.
Dans ce cas-là, la fonction devient :
DatabaseMgr*
Application::Database()
{
Map::const_iterator module = ourMap.find( databaseId ) ;
if ( module == ourMap.end() ) {
tryToLoadModule( databaseId ) ;
module = ourMap.find( databaseId ) ;
}
return module == ourMap.end()
? NULL
: static_cast< DatabaseMgr* >( module->second ) ;
}
|
C'est un peu vers quoi je m'oriente. J'explique un peu ça dans ma
réponse à Patrick:
"Je pars sur une idée d'enregistrement dynamique auprès de
l'Application, et de référencement via un nom sous forme de chaine de
caractères, et le dynamic_cast qui va bien.
class ModuleBase
{
public:
virtual std::string GetName() const = 0;
template<typename T>
T * Downcast()
{
return dynamic_cast<T*>( this );
}
};
class Application
{
public:
void RegisterModule( ModuleBase *M );
M * GetModule( std::string Name );
};
// *****
class ModuleX
{
void Toto()
{
// on utilise la base de données
DataBaseMgr *db = App()->GetModule( "DataBase"
)->Downcast<DataBaseMgr>();
}
}"
| Quote: | Je compte migrer vers quelque chose comme celà:
- pour chaque module, une classe ModuleUser donne le droit d'utiliser le
module. Il faut dériver de cette classe pour pouvoir utiliser la
fonction membre donnant accès à l'instance du module.
class DatabaseUser
{
protected:
DatabaseMgr* Database();
};
class X : public DatabaseUser
{
void Toto()
{
this->Database()->...
}
};
C'est moins pratique à l'utilisation (il faut dériver sa
classe utilisatrice...) mais ça me parrait pas trop mal.
Ça me paraît un peu lourd, mais ça pourrait marcher aussi.
N'empèche que je chercherais à différer la décision au plus tard
possible, à l'édition de liens ou au packaging, et non dans le
code.
|
Oui, c'est ce qui me freine. D'un point de vue conceptuel, je trouve ça
pas mal de se déclarer utilisateur de tel ou tel module. Je suis en
train de voir si c'est pas trop lourd de devoir hériter à chaque fois.
Dans ton exemple ci-dessus:
| Quote: | static_cast< DatabaseMgr* >( module->second )
|
tu sembles trouver acceptable mon idée de classe Module de base qu'on
downcast selon le besoin. Ca rend ma classe centrale Application
indépendante de tout module. Car dans ton exemple, il y a un problème:
| Quote: | DatabaseMgr*
Application::Database()
|
Application::Database() ne peut pas être implémenté dans Application, car:
- le module Database (comme tous les modules) utilise la classe Application
- la classe Application utilise le module Database (et les autres)
donc le serpent se mord la queue. J'ai donc une classe abstraite
Application qui ne fait que des forward declaration:
class DatabaseMgr;
class Application
{
public:
virtual DatabaseMgr* Database() = 0;
};
et une classe ApplicationCore dans le module principal qui implémente
Application::Database() (et le reste).
C'est à peu près mon design actuel, mais ceci me gêne:
si le module Database n'est pas inclus, il n'est donc pas utilisable,
d'où la question:
- pourquoi proposer la fonction membre Database() ?
Car je ne veux pas renvoyer NULL. Je préfère le principe "si c'est là tu
peux l'utiliser" qu'au principe "ok c'est là mais attention aucune
garantie". Ca simplifie l'utilisation.
Je ne compte pas charger les modules à la demande, mais au lancement de
l'application initialiser tous les modules (pour une base de données ça
peut être long), et alors si tout est ok, les modules sont présents et
utilisables directement, sans test de NULL.
--
Aurélien Regat-Barrel
|
|
| Back to top |
|
 |
Aurelien Regat-Barrel Guest
|
Posted: Wed Sep 28, 2005 10:12 am Post subject: Re: Conception modulaire |
|
|
| Quote: | Je ne te présente pas les namespaces comme étant _la_ solution
à ton problème de modularité, mais ce qui m'étonne un peu
c'est que tu dis que chaque module est déclaré dans un namespace
qui lui est propre, et d'un autre côté tu utlises
une structure du type :
class ApplicationCore : public Application
{
virtual DatabaseMgr * Database()
{
#ifdef USE_SQL_SERVER
static SqlServerDataBaseMgr mgr;
#elif defined USE_ORACLE
static OracleDataBaseMgr mgr;
#endif
return &mgr;
}
};
|
J'ai pas mis les namespaces ici pour alléger l'écriture. Si j'ai bien
compris ton étonnement, tu ne comprends pas pourquoi je n'utilise pas
directement DatabaseMgr au lieu de passer par App()->Database() ?
DatabaseMgr est une classe singleton abstraite, et App()->Database() me
renvoie son instance à utiliser. Quelque part il doit y avoir une
factory qui renvoie soit SqlServerDataBaseMgr soit OracleDataBaseMgr.
Cette factory c'est Application, qui s'occupe donc de l'instanciation ey
de l'initialisation du module database, mais aussi de tous les autres.
| Quote: | Mais c'est pas grave vu que cela disparaitra dans ta nouvelle mouture...
Par contre, je qui reste flou pour moi, c'est est-ce que tu souhaites
une gestion des modules à l'execution ou à la compilation uniquement.
|
A la compilation, ou plutot à l'édition des liens, car à la compilation,
il faut jongler avec des #ifdef, et c'est vite l'enfer. Le fait de gérer
ça dynamiquement simplifie l'intégration. C'est donc géré à l'exécution,
mais sur des modules intégrés de manière statique à la compilation /
édition de liens.
--
Aurélien Regat-Barrel
|
|
| Back to top |
|
 |
kanze Guest
|
Posted: Wed Sep 28, 2005 2:25 pm Post subject: Re: Conception modulaire |
|
|
Aurelien Regat-Barrel wrote:
| Quote: | Jusqu'à là, c'est bien. Mais je ne vois pas d'où vient les
#ifdef.
Ils sont définis en fonction des modules à compiler.
|
Mais justement, il faut compiler tout, et seulement linker une
partie.
| Quote: | Ce que je ferais, c'est un map statique des modules,
chargement dynamiquement par le constructeur d'un objet
statique dans chaque module. Ton code
Application::Database() serait alors quelque chose du genre
:
DatabaseMgr*
Application::Database()
{
Map::const_iterator module = ourMap.find( databaseId ) ;
return module == ourMap.end()
? NULL
: static_cast< DatabaseMgr* >( module->second ) ;
}
La « configuration » se ferait lors de l'édition de liens,
où tu incorporeras ou non l'objet static d'initialisation de
tel ou tel module.
Alternativement, tu pourrais faire à peu près pareil avec
des objets dynamiques. Quand la fonction (par exemple
Application::Database) ne trouve pas l'entrée dans le map,
il essaie de charger l'objet dynamique correspondant (qui
s'insérera dans le map) avant de retourner NULL. Dans ce
cas-ci, la « configuration » ne se fait que lors du
packaging : tu inclus plus ou moins d'objets dynamiques dans
le package.
Dans ce cas-là, la fonction devient :
DatabaseMgr*
Application::Database()
{
Map::const_iterator module = ourMap.find( databaseId ) ;
if ( module == ourMap.end() ) {
tryToLoadModule( databaseId ) ;
module = ourMap.find( databaseId ) ;
}
return module == ourMap.end()
? NULL
: static_cast< DatabaseMgr* >( module->second ) ;
}
C'est un peu vers quoi je m'oriente. J'explique un peu ça dans
ma réponse à Patrick:
"Je pars sur une idée d'enregistrement dynamique auprès
de l'Application, et de référencement via un nom sous forme de
chaine de caractères, et le dynamic_cast qui va bien.
class ModuleBase
{
public:
virtual std::string GetName() const = 0;
template<typename T
T * Downcast()
{
return dynamic_cast
}
};
class Application
{
public:
void RegisterModule( ModuleBase *M );
M * GetModule( std::string Name );
};
// *****
class ModuleX
{
void Toto()
{
// on utilise la base de données
DataBaseMgr *db = App()->GetModule( "DataBase"
)->Downcast<DataBaseMgr>();
}
}"
Je compte migrer vers quelque chose comme celà: - pour
chaque module, une classe ModuleUser donne le droit
d'utiliser le module. Il faut dériver de cette classe pour
pouvoir utiliser la fonction membre donnant accès à
l'instance du module.
class DatabaseUser
{
protected:
DatabaseMgr* Database();
};
class X : public DatabaseUser
{
void Toto()
{
this->Database()->...
}
};
C'est moins pratique à l'utilisation (il faut dériver sa
classe utilisatrice...) mais ça me parrait pas trop mal.
Ça me paraît un peu lourd, mais ça pourrait marcher aussi.
N'empèche que je chercherais à différer la décision au plus
tard possible, à l'édition de liens ou au packaging, et non
dans le code.
Oui, c'est ce qui me freine. D'un point de vue conceptuel, je
trouve ça pas mal de se déclarer utilisateur de tel ou tel
module. Je suis en train de voir si c'est pas trop lourd de
devoir hériter à chaque fois.
Dans ton exemple ci-dessus:
static_cast< DatabaseMgr* >( module->second )
tu sembles trouver acceptable mon idée de classe Module de
base qu'on downcast selon le besoin. Ca rend ma classe
centrale Application indépendante de tout module. Car dans ton
exemple, il y a un problème:
DatabaseMgr*
Application::Database()
Application::Database() ne peut pas être implémenté dans
Application, car:
- le module Database (comme tous les modules) utilise la classe
Application
|
Ça, ce n'est pas bien. Il faudrait séparer les concernes : une
classe qui donne accès aux modules, et une autre classe (ou une
module) qui est toujours présente, et qui contient tout ce qui
est commun et qui sert dans les modules.
| Quote: | - la classe Application utilise le module Database (et les autres)
donc le serpent se mord la queue. J'ai donc une classe
abstraite Application qui ne fait que des forward declaration:
class DatabaseMgr;
class Application
{
public:
virtual DatabaseMgr* Database() = 0;
};
et une classe ApplicationCore dans le module principal qui
implémente Application::Database() (et le reste).
|
C'est une solution. J'aurais une tendance à faire un peu
l'opposer ; une classe spéciale qui s'occupe de la
configuration.
| Quote: | C'est à peu près mon design actuel, mais ceci me gêne: si le
module Database n'est pas inclus, il n'est donc pas
utilisable, d'où la question:
- pourquoi proposer la fonction membre Database() ?
Car je ne veux pas renvoyer NULL. Je préfère le principe "si
c'est là tu peux l'utiliser" qu'au principe "ok c'est là mais
attention aucune garantie". Ca simplifie l'utilisation.
|
S'il ne s'agit que de la classe Application... on pourrait assez
facilement le générer automatiquement à partir des paramètres
externes.
| Quote: | Je ne compte pas charger les modules à la demande, mais au
lancement de l'application initialiser tous les modules (pour
une base de données ça peut être long), et alors si tout est
ok, les modules sont présents et utilisables directement, sans
test de NULL.
|
Je ne connais pas assez l'application pour donner les détails,
mais je soupçonne que ton gestion des NULL serait toujours ce
qu'il y a de plus simple. Les #ifdef à droit et à gauche,
surtout dans le code des fonctions (ou la définition des
classes) mènent très vite à quelque chose d'immaintenable.
--
James Kanze GABI Software
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
|
|
| Back to top |
|
 |
Aurelien Regat-Barrel Guest
|
Posted: Mon Oct 03, 2005 7:28 am Post subject: Re: Conception modulaire |
|
|
| Quote: | Jusqu'à là, c'est bien. Mais je ne vois pas d'où vient les
#ifdef.
Ils sont définis en fonction des modules à compiler.
Mais justement, il faut compiler tout, et seulement linker une
partie.
|
J'essaye de m'orienter vers celà.
| Quote: | Dans ton exemple ci-dessus:
static_cast< DatabaseMgr* >( module->second )
tu sembles trouver acceptable mon idée de classe Module de
base qu'on downcast selon le besoin. Ca rend ma classe
centrale Application indépendante de tout module. Car dans ton
exemple, il y a un problème:
DatabaseMgr*
Application::Database()
Application::Database() ne peut pas être implémenté dans
Application, car:
- le module Database (comme tous les modules) utilise la classe
Application
Ça, ce n'est pas bien. Il faudrait séparer les concernes : une
classe qui donne accès aux modules, et une autre classe (ou une
module) qui est toujours présente, et qui contient tout ce qui
est commun et qui sert dans les modules.
|
Mais les modules s'utilisent entre-eux via la classe qui donne accès aux
modules. Et cette dernière utilise tous les modules car elle donne accès
à ces derniers... D'où mon idée qui suit:
| Quote: | C'est à peu près mon design actuel, mais ceci me gêne: si le
module Database n'est pas inclus, il n'est donc pas
utilisable, d'où la question:
- pourquoi proposer la fonction membre Database() ?
Car je ne veux pas renvoyer NULL. Je préfère le principe "si
c'est là tu peux l'utiliser" qu'au principe "ok c'est là mais
attention aucune garantie". Ca simplifie l'utilisation.
S'il ne s'agit que de la classe Application... on pourrait assez
facilement le générer automatiquement à partir des paramètres
externes.
|
J'hésite à implémenter "l'adressage" des modules en les désignant par
une string:
class Application
{
public:
DatabaseMgr* Database()
{
return static_cast<DatabaseMgr>(
this->GetModuleByName( "DatabaseManager" ) );
}
};
Ca, ou une map statique comme tu dis. Ainsi j'évite les #include au
niveau de Application et ce problème de dépendances circulaires
(DatabaseMgr utilise Application qui utilise DatabaseMgr...).
Ma question porte en fait sur la pertinence du système de désignation
d'un module au moyen d'une string / un id, suivi d'un downcast.
Downcast, ça rime avec erreur de conception, d'où mon hésitation.
| Quote: | Je ne compte pas charger les modules à la demande, mais au
lancement de l'application initialiser tous les modules (pour
une base de données ça peut être long), et alors si tout est
ok, les modules sont présents et utilisables directement, sans
test de NULL.
Je ne connais pas assez l'application pour donner les détails,
mais je soupçonne que ton gestion des NULL serait toujours ce
qu'il y a de plus simple. Les #ifdef à droit et à gauche,
surtout dans le code des fonctions (ou la définition des
classes) mènent très vite à quelque chose d'immaintenable.
|
Oui, c'est déjà le cas.
--
Aurélien Regat-Barrel
|
|
| Back to top |
|
 |
kanze Guest
|
Posted: Tue Oct 04, 2005 8:59 am Post subject: Re: Conception modulaire |
|
|
Aurelien Regat-Barrel wrote:
| Quote: | Jusqu'à là, c'est bien. Mais je ne vois pas d'où vient les
#ifdef.
Ils sont définis en fonction des modules à compiler.
Mais justement, il faut compiler tout, et seulement linker
une partie.
J'essaye de m'orienter vers celà.
|
C'est tout à fait possible.
Je ne cite pas la reste de ce que tu as écrit. Dans l'ensemble,
tes observations sont assez bien considérées. La solution que
j'ai proposée (avec le map et des objets dynamiques que je
charge) s'est basé sur un problème concret que j'ai eu, qui
était différent du tien : dans mon cas, il fallait bien pouvoir
ajouter des modules après coup, les modules étaient bien choisis
au moyen d'une chaîne de caractères (c'était une exigeance de
l'application), et surtout, tous les modules implémentaient la
même interface.
Toute reflexion faite, dans ton cas, je ferais plutôt quelque
chose du genre :
-- On définit une interface pour chaque module, c-à-d une
classe abstraite dont toutes les fonctions sont virtuelle
pûre. (Sauf le destructeur, qui est virtuelle, mais inline.)
L'importance, c'est que n'importe qui peut utiliser cette
classe sans générer des références externes dans son fichier
objet. Quelque chose du genre :
class DataBase
{
public:
virtual ~DataBase() {}
virtual void doSomethingWithDataBase() = 0 ;
virtual int doSomethingElse() = 0 ;
// ...
} ;
-- La définition de la classe Application est toujours
complète, avec la déclaration des fonctions usine pour
toutes les modules, qu'ils soient présents ou non. Ça veut
dire que ces utilisateurs doivent pouvoir faire face à une
fonction usine qui renvoie NULL, mais je crois que c'est
toujours plus simple que les alternatifs.
Dans la définition de la classe Application, on doit pouvoir
se servir des déclarations préalables des interfaces des
modules, de façon à ce que l'inclusion de Application.hh
n'impose pas l'inclusion de toutes les ModuleX.hh.
-- La définition de chaque fonction usine dans Application
appartient au module -- il n'y a pas de fichier
d'implémentation de Application. Donc, quelque part dans la
bibliothèque de DataBase, on a :
DataBase*
Application::getDataBase() const
{
return new ConcreteDataBase ;
}
C'est très important : cette fonction se trouve dans la
bibliothèque du module, et NON dans l'implémentation de
Application (où on s'attendrait à le trouver d'habitude).
-- Il existe une bibliothèque « dummy », avec une
implémentation vide de toutes les fonctions usine, du
genre :
DataBase*
Application::getDataBase() const
{
return NULL ;
}
C'est très important : chaque fonction se trouve dans un
fichier source séparé, de façon à ce que l'éditeur de liens
puisse prendre getDataBase de cette bibliothèque, mais
getModuleX d'une autre bibliothèque.
Au fond, on doit pouvoir générer cette bibliothèque
automatiquement, à partir de la définition de Application.
Si ça vaut la peine ou non dépend de combien de modules tu
as.
-- Enfin, quand tu fais l'édition des liens, tu précises les
bibliothèques pour les modules que tu veux avoir ; de cette
façon, les fonctions usines pour ces modules sont
incorporées de leurs bibliothèques. Et en fin de la liste
des bibliothèques, tu précises la bibliothèque « dummy »,
pour résoudre les références à des fonctions usine qui n'ont
pas été résolues par les bibliothèques des modules.
Si tu te sers de GNU make sous Unix, le fichier make
pourrait contenir quelque chose du genre :
define build
$(CC) -o $@ ... application.o $(addprefix -l,$^) -lDummy
endef
complete : DataBase ModuleX
$(build)
simple : ModuleX
$(build)
complete simple : application.o libDummy.a
Pour Windows, évidemment, les séquences de commande et les
suffixes des noms de fichiers seront différents. En
revanche, je conseillerais quand même l'utilisation de GNU
make.
La clé pour que tout ça marche, c'est que l'utilisateur
potentiel de DataBase ne génère jamais de références à quelque
chose de concret dans l'implémentation de DataBase. On aurait
quelque chose du genre :
DataBase* pDB
= Application::instance().getDataBase() ;
if ( pDB == NULL ) {
throw SorryNotImplementedException( "DataBase" ) ;
} else {
pDB->doSomethingWithDataBase() ;
// ...
}
Note que c'est un cas où la déclaration dans le conditionnel
pourrait être utile aussi :
if ( DataBase* pDB = Application::instance().getDataBase() ) {
pDB->doSomethingWithDataBase() ;
// ...
}
--
James Kanze GABI Software
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
|
|
| Back to top |
|
 |
Aurelien Regat-Barrel Guest
|
Posted: Wed Oct 05, 2005 1:40 pm Post subject: Re: Conception modulaire |
|
|
| Quote: | Toute reflexion faite, dans ton cas, je ferais plutôt quelque
chose du genre :
-- On définit une interface pour chaque module, c-à-d une
classe abstraite dont toutes les fonctions sont virtuelle
pûre. (Sauf le destructeur, qui est virtuelle, mais inline.)
L'importance, c'est que n'importe qui peut utiliser cette
classe sans générer des références externes dans son fichier
objet. Quelque chose du genre :
class DataBase
{
public:
virtual ~DataBase() {}
virtual void doSomethingWithDataBase() = 0 ;
virtual int doSomethingElse() = 0 ;
// ...
} ;
|
Ca, j'ai déjà.
| Quote: | -- La définition de la classe Application est toujours
complète, avec la déclaration des fonctions usine pour
toutes les modules, qu'ils soient présents ou non. Ça veut
dire que ces utilisateurs doivent pouvoir faire face à une
fonction usine qui renvoie NULL, mais je crois que c'est
toujours plus simple que les alternatifs.
Dans la définition de la classe Application, on doit pouvoir
se servir des déclarations préalables des interfaces des
modules, de façon à ce que l'inclusion de Application.hh
n'impose pas l'inclusion de toutes les ModuleX.hh.
-- La définition de chaque fonction usine dans Application
appartient au module -- il n'y a pas de fichier
d'implémentation de Application. Donc, quelque part dans la
bibliothèque de DataBase, on a :
DataBase*
Application::getDataBase() const
{
return new ConcreteDataBase ;
}
C'est très important : cette fonction se trouve dans la
bibliothèque du module, et NON dans l'implémentation de
Application (où on s'attendrait à le trouver d'habitude).
|
Ca, c'est la ruse de Sioux qu'il me manquait, d'où mon problème de
dépendance cyclique. Tout s'éclairci... :-)
| Quote: | -- Il existe une bibliothèque « dummy », avec une
implémentation vide de toutes les fonctions usine, du
genre :
DataBase*
Application::getDataBase() const
{
return NULL ;
}
C'est très important : chaque fonction se trouve dans un
fichier source séparé, de façon à ce que l'éditeur de liens
puisse prendre getDataBase de cette bibliothèque, mais
getModuleX d'une autre bibliothèque.
Au fond, on doit pouvoir générer cette bibliothèque
automatiquement, à partir de la définition de Application.
Si ça vaut la peine ou non dépend de combien de modules tu
as.
-- Enfin, quand tu fais l'édition des liens, tu précises les
bibliothèques pour les modules que tu veux avoir ; de cette
façon, les fonctions usines pour ces modules sont
incorporées de leurs bibliothèques. Et en fin de la liste
des bibliothèques, tu précises la bibliothèque « dummy »,
pour résoudre les références à des fonctions usine qui n'ont
pas été résolues par les bibliothèques des modules.
Si tu te sers de GNU make sous Unix, le fichier make
pourrait contenir quelque chose du genre :
define build
$(CC) -o $@ ... application.o $(addprefix -l,$^) -lDummy
endef
complete : DataBase ModuleX
$(build)
simple : ModuleX
$(build)
complete simple : application.o libDummy.a
Pour Windows, évidemment, les séquences de commande et les
suffixes des noms de fichiers seront différents. En
revanche, je conseillerais quand même l'utilisation de GNU
make.
|
J'utilise un IDE (VC++), et je sais que le jour où on va commencer à
compiler/linker le programme en multiples versions à la demande des
clients ça va pas être top. J'ai exploré avec succès la voie de cmake
(un méta-make en quelque sorte), mais j'attends le prochain VC++ dont le
format des fichiers projets de l'IDE est le même que celui de leur
nouveau make (msbuild) avant de choisir quoi utiliser.
Je pense avoir tous les éléments pour faire quelque chose d'assez
élégant. Je m'étonne quand même de ne pas trouver de ressources là
dessus. N'y-a-til pas un design pattern ou quelque chose de ce style qui
existe ? Personne n'a publié sur ce sujet qui me semble courant ?
En tous cas merci beaucoup.
--
Aurélien Regat-Barrel
|
|
| Back to top |
|
 |
|
|
You cannot post new topics in this forum You cannot reply to topics in this forum You cannot edit your posts in this forum You cannot delete your posts in this forum You cannot vote in polls in this forum
|
|