OVH Cloud OVH Cloud

Conception modulaire

16 réponses
Avatar
Aurelien Regat-Barrel
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

10 réponses

1 2
Avatar
Stan
"Aurelien Regat-Barrel" a écrit dans le message
de news: 43395255$0$22383$
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

Avatar
Aurelien Regat-Barrel
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

Avatar
Stan
"Aurelien Regat-Barrel" a écrit dans le message
de news: 43395c50$0$23813$
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


Avatar
Patrick 'Zener' Brunet
Bonjour.

Je réponds à Aurelien Regat-Barrel
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
***************************************/

Avatar
kanze
Aurelien Regat-Barrel wrote:

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 ) ;
}

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.

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

Avatar
Aurelien Regat-Barrel
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...).

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.

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

Avatar
Aurelien Regat-Barrel
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

Avatar
Stan
"Aurelien Regat-Barrel" a écrit dans le message
de news:433a5411$0$4337$
| > 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
Avatar
Aurelien Regat-Barrel
Jusqu'à là, c'est bien. Mais je ne vois pas d'où vient les
#ifdef.
Ils sont définis en fonction des modules à compiler.


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>();
}
}"

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
- 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


Avatar
Aurelien Regat-Barrel
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.

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

1 2