IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)
Navigation

Inscrivez-vous gratuitement
pour pouvoir participer, suivre les réponses en temps réel, voter pour les messages, poser vos propres questions et recevoir la newsletter

Langage C++ Discussion :

Design d'une application : ré-utilisabilité et solution sans singletons


Sujet :

Langage C++

Vue hybride

Message précédent Message précédent   Message suivant Message suivant
  1. #1
    Membre Expert Avatar de Trademark
    Profil pro
    Inscrit en
    Février 2009
    Messages
    762
    Détails du profil
    Informations personnelles :
    Localisation : France

    Informations forums :
    Inscription : Février 2009
    Messages : 762
    Par défaut Design d'une application : ré-utilisabilité et solution sans singletons
    Bonjour à tous,

    Je suis en train de bosser sur un projet assez large (pour une seule personne en tout cas) qui est la création d'un serveur avec Boost.Asio.

    Mon problème est probablement redondant dans beaucoup d'autres projets alors j'aimerais en parler. Quand on est sur un projet assez large on arrive plus ou moins vite à utiliser certaines classes un peu partout, vous les connaissez bien il peut s'agir :

    • Du module de log
    • Du module de statistique
    • De la connexion à la base de donnée
    • De la configuration globale du serveur (chemin vers certains fichiers, ...)
    • ...


    Par exemple pour le logger, j'en ai fait un singleton. Par contre pour la configuration globale du serveur j'ai un objet que je passe partout...
    De plus, on nous rabâche (dans les livres, les blogs, ...) de programmer en essayant de faire des modules les plus indépendants/ré-utilisables possibles. Mais pour moi, rien que d'ajouter des messages de logs, c'est déjà la mauvaise pente de la ré-utilisabilité...

    De plus, j'en arriverais à avoir une classe de ce type :

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    class environment
    {
      server_config cfg;
      database_connection db;
      statistic stat;
      // ...
     
    public:
     
      server_config& get_config();
      database_connection get_db();
      // ...
    };
    Mais je déteste ce genre de classe qui sont à l'opposée d'un esprit de bonne conception...

    Comment est-ce que vous vous en sortez ? Est-ce que vous êtes enclin à faire des singletons pour tout ce qui est global ?

    Merci d'avance pour votre commentaire

    PS : Ne dites pas pourquoi vous n'aimez pas les singletons, on le sait tous. Dites nous quelles sont vos solutions.

  2. #2
    Inactif  


    Homme Profil pro
    Inscrit en
    Novembre 2008
    Messages
    5 288
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 49
    Localisation : France, Rhône (Rhône Alpes)

    Informations professionnelles :
    Secteur : Santé

    Informations forums :
    Inscription : Novembre 2008
    Messages : 5 288
    Par défaut
    Pas de solution miracle

    J'ai rien contre les singletons. Il y a des arguments contre (anti-pattern), mais ils ont aussi des avantages :
    1. accessibilité partout
    2. instanciation unique

    Comment les éviter :
    1. en passant un objet logger plutôt que passer par une variable globale
    2. en créant un seul objet (on n'est pas des devs java, on sais quand même ce que l'on fait avec nos objets, on a pas besoin d'avoir une fonctionnalité qui nous surveille pour savoir si on ne crée pas un objet)

    Le choix singleton aura un impact sur les fonctionnalités :

    * tu veux avoir plusieurs logs différents (par exemple un log public et un log dev. Ou un log pour le serveur, un pour le client, un pour la GUI). Avec un singleton, tu as un seul log, donc il faut prévoir un système d'identification des messages, de dispatching. Sans singleton, tu peux créer autant d'objet que tu veux pour chaque log.

    * tu veux pouvoir activer ou non le log d'une classe (pas en filtrant, mais en n'émettant pas de messages). Avec un singleton, tu pourras par exemple ajouter un bool pour activer ou non. Sans singleton, tu passeras ou non un pointeur sur un logger. Les 2 solutions sont aussi "lourde" à implémenter, la seconde est plus souple.

    A la rigueur, tu peux faire un mix entre les 2 : passer le logger, database, config comme des paramètres pour les objets qui les utilisent. Et utiliser un seul singleton DefauttParameters qui contient un pointeur vers ces objets, dans les cas où tu ne veux pas passer explicitement un objet comme paramètre
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    // sans singleton
    Database db;
    ObjectQuelconque o(&db);
     
    // avec singleton
    ObjectQuelconque o(DefaultParameters::database());
    Remarque : avec cette approche, il n'y a pas de singleton dans les modules "Database" et "ObjectQuelconque" et donc dépendance faible. Tu crées seulement une dépendance (optionnelle) au niveau du module qui utilise ces deux modules, mais pas entre ces modules.

    Conclusion : passer explicitement les objets que l'on a besoin plutôt qu'un singleton

  3. #3
    Membre Expert Avatar de Trademark
    Profil pro
    Inscrit en
    Février 2009
    Messages
    762
    Détails du profil
    Informations personnelles :
    Localisation : France

    Informations forums :
    Inscription : Février 2009
    Messages : 762
    Par défaut
    Merci de ta réponse, je me doutais un peu avoir ce genre de réponse.

    Je sais que les détracteurs du singleton disent souvent "si tu n'en n'as besoin que d'un, n'en crée qu'un". Je suis d'accord mais le soucis c'est que se trimbaler de méthode en méthode, 2 arguments supplémentaires (admettons le logger et la config générale du serveur) c'est lourd (surtout en ajoutant les arguments que la méthode a besoin)... Après effectivement on peut agréger les données dans une seule classe mais on obtient la classe type "environnement" que j'ai mise plus haut.

    Je suis peut-être un peu difficile aussi

  4. #4
    Inactif  


    Homme Profil pro
    Inscrit en
    Novembre 2008
    Messages
    5 288
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 49
    Localisation : France, Rhône (Rhône Alpes)

    Informations professionnelles :
    Secteur : Santé

    Informations forums :
    Inscription : Novembre 2008
    Messages : 5 288
    Par défaut
    En fait, ce qui est problématique, c'est que ton singleton soit intrusif dans tes modules (ou entre tes modules). Mais avoir un singleton qui te permet de récupérer un objet et que tu passes cet objet en paramètre de ton module, ça ira.
    Ton logger, tu n'as pas besoin dans toutes tes classes. Et ça te fais simplement ajouter un variable membre Logger* et une fonction membre setLogger(Logger* logger = 0). C'est pas non plus un drame. Au pire, tu crées un classe de base Loggable qui implémente ça (désolé Liskov)
    Tu peux aussi ne pas avoir un Logger global (entre tes modules), mais avoir un Logger interne à tes modules (Database::Logger), ça éviter un global entre module

  5. #5
    Expert éminent
    Avatar de koala01
    Homme Profil pro
    aucun
    Inscrit en
    Octobre 2004
    Messages
    11 644
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 53
    Localisation : Belgique

    Informations professionnelles :
    Activité : aucun

    Informations forums :
    Inscription : Octobre 2004
    Messages : 11 644
    Par défaut
    Salut,

    Je crois que la première chose à faire, c'est de se méfier des termes trop génériques dans ton développement.

    Je vais prendre un exemple, auquel tu es sans doute confronté, pour te faire comprendre: "Gestionnaire de configuration", que tu auras sans doute implémenté sous une forme proche d'une classe "ConfigManager".

    Si tu pars dans cette direction, tu vas très certainement estimer qu'il est de son ressort de gérer énormément de choses, comme
    1. la configuration des droits d'accès
    2. la configuration de la connexion à la base de données
    3. la configuration de certaines redirections
    4. la configuration des ports
    5. la configuration du nombre d'essais de connexion à effectuer
    6. la configuration du nombre de connexion simultanées acceptées
    7. j'en passe et de meilleures
    Et tu as encore de la chance de nous parler d'un serveur, car, pour l'application client, nous pourrions encore envisager de rajouter quelques configurations particulières, comme, pourquoi pas, la configuration relative à la gestion des couleurs ou à la manière dont certaines informations sont affichées

    Le problème avec un tel "ConfigManager", c'est que tu vas te retrouver très rapidement à utiliser un gros objet monolithique capable de faire le café, la vaisselle, la lessive et de sortir le chien par dessus le marché à peu près partout dans ton projet alors que tu n'as, en définitive, jamais besoin de toutes ces fonctionnalités réunies.

    Bon, d'accord, je mens un tout petit peu car il y aura bel et bien deux endroits où toutes les informations véhiculées par ton ConfigManager devront bel et bien être regroupées: lorsque tu voudra sauvegarder ta configuration et lorsque tu voudras la charger, au démarrage de ton serveur.

    Mais ce qui importe c'est que les différents endroits où tu auras besoin des informations relatives à l'une des spécifications dont j'ai parlé plus haut seront délimités de manière bien précise:

    Tu n'auras, par exemple, besoin de la configuration relatives à la connexion à la base de données que lorsqu'il s'agira d'effectuer des requêtes sur cette base de données, et de la configuration relative aux droits d'accès que lorsqu'il s'agira de répondre à une requête en vue d'accéder à certaines fonctionnalités particulières.

    Il ne sert strictement à rien (et il est dangereux de le faire) de donner accès à l'ensemble de la configuration alors que seules quelques données bien particulières sont, dans une situation donnée, utiles et nécessaires à l'exécution d'une tâche particulière.

    La première chose que je te conseillerais donc de faire est de créer un ensemble de petits gestionnaires de configuration spécifiques à chaque aspect pour lequel une configuration est nécessaire.

    Rien ne t'empêchera bien sur de regrouper le tout dans un "méga gestionnaire de configuration" pour pouvoir charger ou sauvegarder l'ensemble de ta configuration en "une seule fois", mais ce qui importe surtout, c'est de pouvoir poser un cadre à l'utilisation de chaque gestionnaire spécifique, et donc de pouvoir déterminer, si pas les deux ou trois classes qui peuvent accéder à un gestionnaire de configuration spécifique, au moins le module dans lequel l'accès à ce gestionnaire de configuration spécifique est toléré sinon admis.

    Ensuite, il y a le problème du module responsable des logs.

    Il est vrai que c'est, typiquement, le genre de module que l'on se trimbale partout parce que l'on est susceptible, en gros de vouloir logger strictement tout et n'importe quoi.

    Mais ce n'est pas *forcément* une raison pour donner l'accès à l'ensemble du système qui te permet de gérer les différents types de log que tu peux envisager de créer!!!

    Quand on y réfléchit un tout petit peu, en tant qu'utilisateur de ton système de logging, il n'y a que quatre actions susceptibles de t'intéresser et encore, à des moments bien particuliers. Tu dois en effet être capable:
    1. D'ajouter un nouveau fichier de log (uniquement au niveau de la configuration)
    2. D'activer le logging de certains types d'activités (uniquement au niveau de la configuration)
    3. De désactiver le logging de certains types d'activités (uniquement au niveau de la configuration)
    4. De provoquer le logging de certaines activités (ca, ca se fait partout)
    Quand on y réfléchit bien, la seule chose dont tu auras réellement besoin partout, c'est l'accès à une seule et unique fonction "log(/* paramètres éventuels */ ) ", et tu pourrais très bien, au niveau de la configuration, te contenter d'un nombre finalement très restreint d'autres fonctions ("addLogger(name)", "enableLogger(name)", "disableLogger(name)", "enableLevel(name, level)" et "disableLevel(name, level)") au niveau de la configuration, sans même avoir besoin de t'inquiéter de la mécanique interne qui permettra à ces fonctions d'avoir le comportement adéquat.

    N'oublie pas que nous codons en C++ et que C++ est un langage multi paradigmes! Nous serions très mal inspirés de ne pas profiter de cette particularité pour "cacher" à l'utilisateur du système de logging ce que l'on ne veut pas lui montrer (en fait, j'aurais plutôt du écrire "pour permettre à l'utilisateur du système de logging de l'utiliser de manière transparente" )

    Ce que tu pourrais donc faire (je sais, ca peut être surprenant ), c'est:

    Créer deux fichiers d'en-tete spécifiques, et un fichier d'implémentation spécifique:
    1. log.h qui ne déclare qu'une seule fonction: log et
    2. logConfig.h qui déclares les fonctions de configuration du logging ( addLogger, enableLogger, disableLogger, enableLevel et disableLevel dont j'ai parlé plus tot).
    3. log.cpp contiendrait, entre autres, la définition de la classe LogManager, l'implémentation des six fonctions citées qui manipulent LogManager, et de "tout ce qu'il faut" pour que LogManager puisse fonctionner).
    Nous pourrions alors même décider d'avoir une variable globale de type LogManager dans notre fichier d'implémentation, cela ne poserait strictement aucun problème car elle ne serait disponible que pour les fonctions que tu définis au sein de ce fichier d'implémentation.

    Ce qu'il faut comprendre, c'est que l'on a forcément une "double casquette" lorsque l'on est développeur sur un projet (que ce soit un projet simple ou complexe, que l'on travaille seul ou dans un staff de 60 développeurs ne change rien): on n'est le développeur d'une fonctionnalité que le temps nécessaire à son implémentation / test / intégration / correction.

    Tout le reste du temps, on n'est que l'utilisateur de cette fonctionnalité et l'on doit donc l'aborder du point de vue de l'utilisateur.

    A partir du moment où tu deviens capable de garder cette séparation stricte des pouvoirs en tête, il devient beaucoup plus facile de raisonner de manière simple et ordonnée
    A méditer: La solution la plus simple est toujours la moins compliquée
    Ce qui se conçoit bien s'énonce clairement, et les mots pour le dire vous viennent aisément. Nicolas Boileau
    Compiler Gcc sous windows avec MinGW
    Coder efficacement en C++ : dans les bacs le 17 février 2014
    mon tout nouveau blog

  6. #6
    Inactif  


    Homme Profil pro
    Doctorant sécurité informatique — Diplômé master Droit/Économie/Gestion
    Inscrit en
    Décembre 2011
    Messages
    9 026
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 32
    Localisation : France, Loire (Rhône Alpes)

    Informations professionnelles :
    Activité : Doctorant sécurité informatique — Diplômé master Droit/Économie/Gestion
    Secteur : Enseignement

    Informations forums :
    Inscription : Décembre 2011
    Messages : 9 026
    Par défaut
    Bonjour,

    Pour le module de connexion à la base de donnée, normalement, la bibliothèque s'occupe de tout, elle ouvre et ferme les connexions dès qu'elle le juge nécessaire. Pas besoins de singletons donc ni même de donner un même objet à toutes les classes.

    Pour les informations du serveur, je pense que je préférerais transmettre une référence vers une instance, cette même instance pourrait avoir des méthodes pour accéder aux services de logs et de bases de données (?) :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    class Server
    {
          public :
                  LogMachin & logMachin();
                  InfoServer & info();
    };
    Ceci évitera de passer 50 références à chaque méthodes/classes...

    Pour les logs, peut être peut-on encapsuler un flux/des flux et fournir une interface du style de std::clog (?) :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    my::log << LOG_TYPE << "Bonjour" << std::endl;
    my::log << LOG_TYPE2 | LOG_TYPE << "Au revoir" << std::endl;
    my::log << "log par defaut" << std::endl;

  7. #7
    Membre Expert Avatar de Trademark
    Profil pro
    Inscrit en
    Février 2009
    Messages
    762
    Détails du profil
    Informations personnelles :
    Localisation : France

    Informations forums :
    Inscription : Février 2009
    Messages : 762
    Par défaut
    Le problème avec un tel "ConfigManager", c'est que tu vas te retrouver très rapidement à utiliser un gros objet monolithique capable de faire le café, la vaisselle, la lessive et de sortir le chien par dessus le marché à peu près partout dans ton projet alors que tu n'as, en définitive, jamais besoin de toutes ces fonctionnalités réunies.

    Bon, d'accord, je mens un tout petit peu car il y aura bel et bien deux endroits où toutes les informations véhiculées par ton ConfigManager devront bel et bien être regroupées: lorsque tu voudra sauvegarder ta configuration et lorsque tu voudras la charger, au démarrage de ton serveur.
    J'ai effectivement une classe config, qui est générique. Un peu comme le property tree de Boost. Du coup j'ai organisé la configuration dans un fichier comme ceci :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
     
    [server_core]
    	port = "12522"
    	thread = "0"
    	data_dir = "../"
    [/server_core]
    [logging]
    	log_if_greater_or_equal="debug"
    	[cout]
    		level = trace, debug, info
    	[/cout]
    	[cerr]
    		level = warning, error, fatal
    	[/cerr]
    	#[file]
    	#	filename = "info.log"
    	#	level = info
    	#[/file]
    [/logging]
    [database]
    	dns = "serveur_db"
            user = "root"
    	password = "mypassword"
    [/database]
    [protocol]
    	max_header_size = "8192"
    [/protocol]
    Du coup nommons cette config "cfg", il est vrai que je ne peux passer que certaines partie à mes classes de cette façon : logger lg(cfg["logging"]). Soit mais d'une façon général ça ajoute un argument (ou plusieurs) partout où j'ai besoin d'avoir la/les configs. À la limite, si c'est dans le constructeur, c'est peut-être pas si gênant.

    ...
    N'oublie pas que nous codons en C++ et que C++ est un langage multi paradigmes! Nous serions très mal inspirés de ne pas profiter de cette particularité pour "cacher" à l'utilisateur du système de logging ce que l'on ne veut pas lui montrer (en fait, j'aurais plutôt du écrire "pour permettre à l'utilisateur du système de logging de l'utiliser de manière transparente" )
    ...
    Pour ce qui est du loggeur, j'en ai fait un singleton et je l'accède via des macros du style :

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    UMCD_LOG(fatal) << " blah: " << e.user_message;
    Après je dois avouer que je n'ai pas compris l'utilité de séparer la fonction de log des autres fonctions. D'accord elles seront utilisées à différent moment, mais vu que de toutes façon je ne log que via des macros prédéfinies, je ne vois pas trop le soucis.

    Pour le module de connexion à la base de donnée, normalement, la bibliothèque s'occupe de tout, elle ouvre et ferme les connexions dès qu'elle le juge nécessaire. Pas besoins de singletons donc ni même de donner un même objet à toutes les classes.
    Je vais utiliser OTL et je pense qu'il n'y a pas de pool de connexion directement implémenté dans la librairie. En gros il faudra que je le fasse moi-même. Mais je n'ai pas encore regardé ça de très près, donc effectivement il se peut que je puisse m'en passer

    Pour les informations du serveur, je pense que je préférerais transmettre une référence vers une instance, cette même instance pourrait avoir des méthodes pour accéder aux services de logs et de bases de données (?) :
    Du coup un petit peu comme ma classe environnement au dessus ? J'essaye d'éviter au maximum les getters/setters et de toujours avoir des méthodes qui rendent des services. C'est pour ça que j'essaye d'éviter ce genre de classe.

    Enfin, je trouve que c'est quand même la misère, on devrait pouvoir ajouter des sortes de "plugging" aux classes pour rendre des services supplémentaires comme le logging.

    Bon merci beaucoup pour vos commentaires, je vais faire pour un mieux en m'en inspirant

  8. #8
    Expert éminent
    Avatar de koala01
    Homme Profil pro
    aucun
    Inscrit en
    Octobre 2004
    Messages
    11 644
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 53
    Localisation : Belgique

    Informations professionnelles :
    Activité : aucun

    Informations forums :
    Inscription : Octobre 2004
    Messages : 11 644
    Par défaut
    Citation Envoyé par Trademark Voir le message
    J'ai effectivement une classe config, qui est générique. Un peu comme le property tree de Boost. Du coup j'ai organisé la configuration dans un fichier comme ceci :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
     
    [server_core]
    	port = "12522"
    	thread = "0"
    	data_dir = "../"
    [/server_core]
    [logging]
    	log_if_greater_or_equal="debug"
    	[cout]
    		level = trace, debug, info
    	[/cout]
    	[cerr]
    		level = warning, error, fatal
    	[/cerr]
    	#[file]
    	#	filename = "info.log"
    	#	level = info
    	#[/file]
    [/logging]
    [database]
    	dns = "serveur_db"
            user = "root"
    	password = "mypassword"
    [/database]
    [protocol]
    	max_header_size = "8192"
    [/protocol]
    Du coup nommons cette config "cfg", il est vrai que je ne peux passer que certaines partie à mes classes de cette façon : logger lg(cfg["logging"]). Soit mais d'une façon général ça ajoute un argument (ou plusieurs) partout où j'ai besoin d'avoir la/les configs. À la limite, si c'est dans le constructeur, c'est peut-être pas si gênant.
    Je ne te dis pas de ne pas en faire des singleton (quoi que, à choisir, en faire des monostate serait peut etre mieux ), si tu le souhaites, je te dis surtout d'en faire plusieurs classes clairement distinctes.

    Plutôt que d'avoir une grande classe monolithique du genre de
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    class ConfigManager{
        public:
            static ConfigManager & instance(){
                static ConfigManager man;
                return man;
            }
        private:
            /* la configuration du core */
            /* la configuration logging */
            /* la configuration database */
            /* la configuration protocole */
            /*  ... les autres configurations qui viendront en leur temps */
    };
    je te conseillerais d'avoir une classe pour chaque type de configuration particulière, sous une forme (j'utilise la forme monostate, que je préfères personnellement au singleton) de
    CoreConfig.h
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    class CoreConfig{
        friend class ConfigLoader;
        friend class ConfigSaver;
        private:
            static int port;
            static int threadNumber;
            static std::string dataDir;
    };
    logConfiguration.h
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    #include <logLevel.h> // contient l'énumération pour les niveaux de logging
    class LogConfiguration{
        friend class ConfigLoader;
        friend class ConfigSaver;
        private:
            LogLevel level;
    };
    databaseConfig.h
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    class DatabaseConfig{
        friend class ConfigLoader;
        friend class ConfigSaver;
        private:
            static std::string dns;
            static std::string user;
            static std::string password;
    };
    et ainsi de suite. (j'ai du modifier le nom de la configuration pour le logging afin de rester compatible avec les noms de fichiers donnés dans mon intervention précédente... sorry )

    Bien sur, j'ai placé les membres en accessibilité privée par habitude, et il manque surement au moins les accesseurs sur ces données, mais tu organise cela à peu près comme bon te semble

    Tu auras remarqué que j'ai à chaque fois déclaré une amitié envers ConfigLoader et envers ConfigSaver.

    Ces deux classes prendraient une forme proche de
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    class ConfigLoader{
        public:
            void load(){
                 loadCore();
                 loadLogging();
                 loadDataBase();
                 /* ... */
            }
        private:
            loadCore(){
                CoreConfig core;
                /* lecture et mise à jour des informations */
            }
            loadLogging(){
                LogConfiguration log;
                /* lecture et mise à jour des informations */
            }
            loadDatabase(){
                databaseConfig db;
                /* lecture et mise à jour des informations */
            }
            /* ... */
    };
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    class ConfigSaver{
        public:
            void save(){
                 saveCore();
                 saveLogging();
                 saveDataBase();
                 /* ... */
            }
        private:
            saveCore(){
                CoreConfig core;
                /* écriture des informations dans le fichier*/
            }
            saveLogging(){
                LogConfig log;
                /* écriture des informations dans le fichier */
            }
            saveDatabase(){
                databaseConfig db;
                /* écriture des informations dans le fichier */
            }
            /* ... */
    };
    Là, je te mets juste les fonctions pour que tu aies une idée de la manière de faire, je te laisse totalement libre au niveau de l'implémentation

    L'idée, une fois que tu as ces deux classes, c'est que tu pourrais très bien avoir un fonction main qui ressemble à
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    int main(){
        ConfigLoader loader;
        loader.load();
        /* tout le reste */
        ConfigSaver saver;
        saver.save();
        return 0;
    }
    Mais ce qui importe c'est, surtout, que finalement, tu n'auras besoin des différentes configuration qu'à certains moment très particuliers:
    • Tu n'as besoin de CoreConfig qu'au moment de lancer ton serveur, et, éventuellement, lorsqu'il s'agit d'aller chercher les données dans dataDir.
    • Tu n'as besoin de DatabaseConfig qu'au moment de te (re)connecter à la base de donnée
    • Tu n'as besoin de LogConfig que pour pouvoir implémenter correctement une fonction log()
    • ... et ainsi de suite
    En travaillant de la sorte, tu pourrais placer les différentes configurations dans des modules totalement indépendants les uns des autres, et, si certains modules ont des dépendances envers d'autres modules, ce n'est pas au niveau de la configuration, mais bien au niveau de l'utilisation.

    Si tu venais à relancer un autre projet identique, tu pourrais par exemple prendre le module dataBase (ou le module Core), avec, cela va sans doute de soi, le *Config qui est associé au module, faire un grand copier / coller de ce code et la seule chose que tu aurais à faire au niveau de ConfigLoader et de ConfigSaver serait de commenter les parties qui "n'existent pas encore", sans que cela ne te pose strictement le moindre problème
    Pour ce qui est du loggeur, j'en ai fait un singleton et je l'accède via des macros du style :

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    UMCD_LOG(fatal) << " blah: " << e.user_message;
    Après je dois avouer que je n'ai pas compris l'utilité de séparer la fonction de log des autres fonctions. D'accord elles seront utilisées à différent moment, mais vu que de toutes façon je ne log que via des macros prédéfinies, je ne vois pas trop le soucis.
    Moi, ce qui m'inquiète, c'est que du coup, tu soit obligé d'utiliser explicitement ton singleton à l'intérieur de ta macro sous une forme proche de
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    #definie UMCD_LOG(level) LogManager::instance() \
    /* ... */
    alors que, tout ce dont tu as réellement besoin, c'est d'une fonction log(LogLevel )

    Le but de la manœuvre lorsque je te propose de séparer les fonctions permettant d'agir sur les loggers actifs de la fonction log, c'est, tout simplement, d'éviter à l'utilisateur d'avoir l'impression qu'il peut impunément activer ou désactiver un logger particulier n'importe quand, n'importe comment "uniquement" parce qu'il a la permission de demander le logging d'une information.

    En ayant séparé les différentes configurations, il est tout à fait possible de se limiter à une macro "simple" sous une forme proche de
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    #definie UMCD_LOG(level) log(level)
    avec un fonction log dont la signature prendrait la forme de
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    std::ostream & log(LogLevel);
    et dont il n'y aurait que l'implémentation qui aurait connaissance de "toute la mécanique" qui se cache derrière.

    Pour reprendre le nom de fichier que j'utilisais dans mon intervention précédente, le fichier log.cpp serait alors le seul (en dehors des classes ConfigLoader et ConfigSaver, bien sur ) à avoir besoin de la configuration particulière pour le logging et prendrait une forme proche de
    log.cpp
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
     
    #incude <log.h>
    #include <logConfig.h>
    #include <logConfiguration.h> // il n'y a qu'ici que ce soit nécessaire ;)
    class LogManager{
        public:
            LogManager(){
                LogConfiguration conf;
                /* Tout ce sur quoi la configuration a une incidence sur la création 
                 * du LogManager
                 */
            }
        /* implémentation strictement libre */
    };
    LogManager man; // on peut parfaitement avoir une variable globale au niveau de ce fichier 
                    // mais nous pourrions encore une fois recourir au monostate
    void addLogger(std::string const & name){
        /* ... */
    }
    void enableLogger(std::string const & name){
        /* ... */
    }
    void disableLogger(std::string const & name){
        /* ... */
    }
    void enableLevel(std::string const & name, LogLevel){
        /* ... */
    }
    void disableLevel(std::string const & name, LogLevel){
        /* ... */
    }
    std::ostream & log(LogLevel level){
        /* ... */
    }
    Si tu veux quelque chose de flexible, la solution qui consiste à "éclater" les responsabilités et à en "cacher un maximum" à l'utilisateur (même si c'est toi l'utilisateur ) n'aura que des avantages, que ce soit en terme de dépendances ou simplement en terme des possibilités qui sont offertes à l'utilisateur
    A méditer: La solution la plus simple est toujours la moins compliquée
    Ce qui se conçoit bien s'énonce clairement, et les mots pour le dire vous viennent aisément. Nicolas Boileau
    Compiler Gcc sous windows avec MinGW
    Coder efficacement en C++ : dans les bacs le 17 février 2014
    mon tout nouveau blog

+ Répondre à la discussion
Cette discussion est résolue.

Discussions similaires

  1. Réponses: 5
    Dernier message: 09/02/2013, 10h10
  2. Réponses: 5
    Dernier message: 18/04/2010, 15h27
  3. [VB.NET] Forcer le design d'une application .NET
    Par tssi555 dans le forum VB.NET
    Réponses: 3
    Dernier message: 07/01/2009, 12h52
  4. Réponses: 2
    Dernier message: 27/11/2007, 10h07
  5. Tester une application Web designée en Flash
    Par ninox_ dans le forum Test
    Réponses: 2
    Dernier message: 20/07/2007, 10h09

Partager

Partager
  • Envoyer la discussion sur Viadeo
  • Envoyer la discussion sur Twitter
  • Envoyer la discussion sur Google
  • Envoyer la discussion sur Facebook
  • Envoyer la discussion sur Digg
  • Envoyer la discussion sur Delicious
  • Envoyer la discussion sur MySpace
  • Envoyer la discussion sur Yahoo