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

6 réponses

1 2
Avatar
kanze
Aurelien Regat-Barrel wrote:
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.

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


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

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

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.

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



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.



Mais justement, il faut compiler tout, et seulement linker une
partie.


J'essaye de m'orienter vers celà.

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:

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.

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



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




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

-- 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... :-)

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

Avatar
kanze
Aurelien Regat-Barrel wrote:

[...]
-- 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.


A priori, tu dois pouvoir créer de nouveaux cibles dans ton
fichier de make. Strictement parlant, c'est tout ce qu'il te
faut. L'avantage de GNU make, ici, c'est dans la possibilité de
définir des séquences prédéfinies, avec des fonctions
($(addprefix...)) pour générer de texte à partir des dépendances
($^). Avec un autre make, il y a beaucoup plus à tapper à la
main. Mais en principe, ça doit être faisable quand même.

Une autre possibilité serait d'utiliser le IDE pour le
développement et la génération de la verion « complète », avec
tous les modules, et d'écrire un petit script qui génère le
makefile voulu en fonction de la ligne de commande, puis
l'exécute. Il serait même assez simple d'écrire un petit
programme GUI qui présente les modules avec des checkbox, puis
génère le makefile voulu et l'exécute.

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 ?


Pas à ce que je sache. À vrai dire, je ne sais pas si c'est
aussi courant que ça, comme problème. Mais de toute façon, je ne
l'ai jamais vu abordé dans la littérature.

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



A priori, tu dois pouvoir créer de nouveaux cibles dans ton
fichier de make. Strictement parlant, c'est tout ce qu'il te
faut. L'avantage de GNU make, ici, c'est dans la possibilité de
définir des séquences prédéfinies, avec des fonctions
($(addprefix...)) pour générer de texte à partir des dépendances
($^). Avec un autre make, il y a beaucoup plus à tapper à la
main. Mais en principe, ça doit être faisable quand même.

Une autre possibilité serait d'utiliser le IDE pour le
développement et la génération de la verion « complète », avec
tous les modules, et d'écrire un petit script qui génère le
makefile voulu en fonction de la ligne de commande, puis
l'exécute. Il serait même assez simple d'écrire un petit
programme GUI qui présente les modules avec des checkbox, puis
génère le makefile voulu et l'exécute.


C'est ce que fait cmake:
http://www.taylors.org/cim/observed/030930/CMake/CMakeGUI.gif

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 ?



Pas à ce que je sache. À vrai dire, je ne sais pas si c'est
aussi courant que ça, comme problème. Mais de toute façon, je ne
l'ai jamais vu abordé dans la littérature.


Dommage, y'a un manque à mon avis. En tous cas merci beaucoup.

--
Aurélien Regat-Barrel


1 2