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

 C++ Discussion :

Factory : factorisation pour la fabrication


Sujet :

C++

  1. #1
    Membre confirmé
    Profil pro
    Inscrit en
    Mars 2012
    Messages
    99
    Détails du profil
    Informations personnelles :
    Localisation : France

    Informations forums :
    Inscription : Mars 2012
    Messages : 99
    Par défaut Factory : factorisation pour la fabrication
    Bonjour,

    d'une manière générale, lorsqu'on a une application conçue sous forme de modules, il semble intéressant d'exploiter le patron de conception Factory pour regrouper la création de ces derniers et gérer leur cycle de vie (destruction par exemple).

    Comme les modules ont la plupart du temps une logique adoptée par le développeur au niveau de leur nom, on est tenté de vouloir factoriser le code de création sous une forme proche de :

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    #define CREATE_MODULE(Name) \
    return new Module##Name();
    L'idée serait donc de profiter de la concaténation par une directive de préprocesseur.
    Or, ceci n'est pas toujours approuvé par les développeurs.
    L'une des raisons pourrait être la possibilité d'utiliser CREATE_MODULE n'importe où (à condition d'inclure le header). Peut être faudrait-il donc garder l'idée en exposant dans le define quelque chose de moins sensible comme juste un GetTypeModule(Name) qui retournerait Module##Name ?

    Qu'en pensez-vous ?

  2. #2
    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,

    En fait, tu parles spécifiquement des modules, mais les modules en eux-même ne sont pas forcément le problème.

    Quand on décide de découper une application en modules, on ne fait que se mettre dans une position idéale pour créer une séparation plus ou moins artificielle (mais tout à fait logique) des différentes fonctionnalités offertes par l'ensemble de l'application.

    Typiquement, un module va :
    1. Etre, a priori, totalement indépendant des autres modules, ou, du moins, être en mesure de fonctionner sans avoir besoin d'autres modules particuliers (exception faite éventuellement du module regroupant les données métier, et encore)
    2. Regrouper un ensemble de fonctionnalités qui "vont bien ensembles" car elles suivent un objectif commun (le rendu sonore, le rendu visuel, la communication par le réseau, la sérialisation, la gestion des données métiers, ...)
    3. Servir de "passerelle" permettant aux autres modules d'accéder aux seules fonctionnalités indispensables de "transmission de l'information" entre les modules
    4. maintenir, de manière transparente pour l'utilisateur (du module) l'ensemble des données nécessaire à l'utilisation des fonctionnalités envisagée
    5. n'exister jamais que sous la forme d'une instance unique afin d'éviter les éventuels problèmes de synchronisation entre les instances
    6. être initialisé "très tôt" dans l'application : généralement, avant même que l'on ne commence à manipuler réellement les données métier ou -- de manière beaucoup plus générale -- à faire communiquer différents modules entre eux.
    Ces différents points méritent pour certains une explication, et, pour d'autres, il est possible d'en tirer certaines conclusions. Voici la manière dont j'envisage les choses :

    Le point (1) est très important parce qu'il impose plusieurs conclusions:

    La plus importante est sans doute le fait qu'il n'y a aucune raison de créer une classe de base -- mettons AbstractModule -- dont hériteraient publiquement les différents modules.

    Si l'on n'a pas de classe de base, on n'a pas besoin du polymorphisme d'inclusion, et, si l'on n'a pas besoin du polymorphisme d'inclusion, on n'a pas besoin de l'allocation dynamique de la mémoire pour la création des différents modules. On pourra (le cas échéant) les transmettre sous la forme de référence (ce sera obligatoire pour assurer l'unicité référentielle), mais on peut parfaitement les créer par valeur.

    On n'a donc pas besoin d'une "fabrique de module". La directive préprocesseur devient alors tout bonnement inutile car un simple
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    int main(){
        RenderModule render; // le module de rendu d'affichage
        SoundModule sound; // le module de rendu sonore
        LanModule lan; // le module de communication "vers l'extérieur"
        BusinessModule business; // le module de gestion des données métier
        /* ... */
    }
    suffit plus qu'amplement

    Les points (2) et (3) méritent une explication conjointe. La première chose à constater, c'est qu'un module ne va très certainement pas exposer l'ensemble des fonctionnalités et des données qu'il manipule "en interne" : il va exposer certains signaux capables de transmettre un message "à qui est intéressé par la teneur du message" (ex: monstre 3542 attaque avec le sort 24), quelques fonctions qui permettront de transmettre des ordres bien particulières (ex : charges les musiques du niveau 2), et c'est tout!

    Cela nous permet de respecter le DIP parce qu'un signal reste un signal, quelle qu'en soit la forme, quelle que soit la teneur du message émis.

    Si l'un des message est "monstreAttaque(id_monstre, id_sort)", le module business, le module son et le module d'affichage pourront s'y connecter afin que, lorsqu'il est émis, le module business crée le sort, avec comme origine la position du monstre, le module d'affichage sélectionne le sprite correspondant au sort pour l'afficher et le module son émet le son spécifique du sort lancé. Notes d'ailleurs que le module réseau pourrait lui aussi s'y connecter afin de transmettre cette information

    Cela permet aussi de respecter le SRP : le sort peut être limité à sa plus simple expression parce que la seule chose commune qui intéresse réellement tous les modules, c'est l'identifiant du sort (le sort numéro 24 dans mon exemple), sa position et éventuellement quelques données connexes comme la direction, l'orientation et la vitesse de déplacement.

    Le module business pourra créer une donnée qui l'intéresse réellement et qui contient la position du sort et les dégats qu'il provoque si on est touché, le module d'affichage pourra y associer un "volume" à afficher ainsi que des textures, le module son pourra y associer les sons lorsque le sort est lancé et lorsqu'il touche quelque chose et le module réseau pourra se contenter des de transmettre les informations relatives à sa position, sa trajectoire et son identifiant

    Le point (4) fera idéalement appel à un mécanisme de fabrique (en plus d'un mécanisme de maintient des données). Mais ce qu'il est important de constater, c'est que ces mécanismes ne sont que de la "popote interne" au module : chaque module peut parfaitement travailler avec ses propres types de données, qui seraient totalement (ou du moins, dans une très large mesure) différents des types de données manipulés par les autres modules. Et pour cause : je viens de t'expliquer que chaque module donnera un sens qui lui est propre au "sort numéro 24".

    Le point (5) et le point (6) correspondent très certainement l'expression du besoin qui a donné naissance à l'anti-pattern désigné sous le terme de "singleton" par le GoF.

    Chaque module ne doit effectivement exister qu'une seule fois. Chaque module peut, effectivement, être considéré comme global dans le sens où il doit être créé "dés le lancement de l'application" et où il doit continuer d'exister "jusqu'à la fin de l'application".

    Mais on se rend compte que ces deux limites ne font au final que définir une portée bien particulière : celle de l'application. Et l'on se rend aussi compte qu'il n'y a qu'à l'intérieur de cette portée particulière qu'il est important de connaître l'ensemble des modules, afin de pouvoir générer les différentes interconnexions qui existent entre les différents modules.

    Quand on sait que le meilleur moyen pour s'assurer de l'unicité d'un objet est, d'abord et avant tout de s'assurer qu'il n'y aura jamais qu'une seule variable correspondant à cet objet particulier, on se rend compte qu'il est particulièrement facile d'éviter le recours au singleton. Cela se fait en trois étapes:
    1. Rendre les types non affectables et non copiables.
    2. Créer une portée spécifique qui contient les différents modules.
    3. Gérer les relations entre les modules au niveau de cette portée générale uniquement
    La première étape est facile à franchir : le mécanisme permettant de rendre une classe non copiable et non affectable est bien connu depuis longtemps.

    Cela peut passer par l'utilisation de boost::non_copyable ou d'une classe équivalente écrite par toi, par l'utilisation de la possibilité offerte par C++11 de déclarer les fonctions delete ou par la technique "à l'ancienne" qui consiste à déclarer sans les définir le constructeur de copie et l'opérateur d'affectation dans l'accessibilité privée.

    La deuxième étape n'est pas plus compliquée. La portée spécifique peut être la fonction main ou une classe spécifique (comme la classe Application (ou nom similaire) proposée par les bibliothèques d'IHM).

    Enfin, la troisième étape est, peut être, celle qui te demandera le plus d'attention : elle nécessite que les différents modules soient, réellement, indépendants les uns des autres

    Je crois avoir fait le tour de ce qu'il faut prendre en considération pour les modules et, par là, avoir démontré l'inutilité de la directive préprocesseur que tu proposes

    Notes enfin que j'ai, à titre purement personnel, horreur des directives préprocesseur. Elle présentent, je dois l'avouer, un avantage majeur qui est de faciliter énormément l'écriture. Mais cet avantage est largement contrebalancé par le fait qu'elles cachent ce qui est réellement fait à celui qui les utilise.

    Du coup, lorsqu'une erreur de compilation survient, avec "un peu de chance", on se retrouve avec une erreur de 2500 lignes qui développe l'ensemble des directives préprocesseurs par lesquelles ont est passé pour obtenir le résultat erroné. Mais c'est difficile à décrypter. Avec "un peu de malchance", on obtient juste une ligne d'erreur qui correspond à l'erreur telle qu'elle se serait présentée si on n'avait pas eu recours à la directive préprocesseur, et il est particulièrement difficile de "refaire tout le chemin" d'évaluation de la dite directive.

    Je tiens à préciser que ce n'est qu'un avis strictement personnel et qu'il n'engage donc bien évidemment que moi. Mais je le partage
    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

  3. #3
    Membre confirmé
    Profil pro
    Inscrit en
    Mars 2012
    Messages
    99
    Détails du profil
    Informations personnelles :
    Localisation : France

    Informations forums :
    Inscription : Mars 2012
    Messages : 99
    Par défaut
    La plus importante est sans doute le fait qu'il n'y a aucune raison de créer une classe de base -- mettons AbstractModule -- dont hériteraient publiquement les différents modules.

    Si l'on n'a pas de classe de base, on n'a pas besoin du polymorphisme d'inclusion, et, si l'on n'a pas besoin du polymorphisme d'inclusion, on n'a pas besoin de l'allocation dynamique de la mémoire pour la création des différents modules. On pourra (le cas échéant) les transmettre sous la forme de référence (ce sera obligatoire pour assurer l'unicité référentielle), mais on peut parfaitement les créer par valeur.

    On n'a donc pas besoin d'une "fabrique de module". La directive préprocesseur devient alors tout bonnement inutile
    Logique imparable .
    En fait, on peut quand même trouver des fonctionnalités communes entre les différents modules comme par exemple une méthode setConfig. On aurait donc une classe de base même si elle resterait très succincte.
    Aussi, peut être que dans ce cas l'appellation fabrique est mal choisie mais j'envisageais de créer un objet de construction qui aurait connaissance de la config et pourrait distribuer les paramètres spécifiques à chaque module lors de leur création.

    lorsqu'une erreur de compilation survient, avec "un peu de chance", on se retrouve avec une erreur de 2500 lignes qui développe l'ensemble des directives préprocesseurs par lesquelles ont est passé pour obtenir le résultat erroné. Mais c'est difficile à décrypter. Avec "un peu de malchance", on obtient juste une ligne d'erreur qui correspond à l'erreur telle qu'elle se serait présentée si on n'avait pas eu recours à la directive préprocesseur, et il est particulièrement difficile de "refaire tout le chemin" d'évaluation de la dite directive.
    C'est vrai que si on utilise de façon abusive les directives, ça peut devenir rapidement illisible et difficile à déboguer. Après, si ça reste très ciblé et explicite, la puissance que ça apporte peut en valoir le détour.

  4. #4
    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 sfarc Voir le message
    Logique imparable .
    En fait, on peut quand même trouver des fonctionnalités communes entre les différents modules comme par exemple une méthode setConfig. On aurait donc une classe de base même si elle resterait très succincte.
    Quelle horreur!!!

    Il y a deux (et même trois, à bien y réfléchir) raisons pour lesquelles ce serait une très mauvaise chose :

    La première, c'est la loi de Déméter : En créant une fonction setConfig, tu obliges l'utilisateur de ton module à connaître un détail d'implémentation de ton module qu'il n'a normalement pas à connaître.

    Au pire, si le module expose la possibilité de modifier des options de configurations, ce seraient des fonctions très spécifiques (modifyXXXconfigValue(Type valeur) ) qui permettront à l'utilisateur du module (ou de la façade au travers de laquelle le module est manipulé) de n'avoir pas à s'inquiéter de la forme que prend la configuration à l'intérieur du module en lui-même.

    La deuxième raison, c'est que la présence d'une fonction similaire dans différentes classes n'implique pas forcément qu'il doit y avoir un lien de sororité ou d'héritage entre deux classes.

    Imagines une classe InventoryItem, qui serait la classe de base pour "tout élément susceptible de prendre place dans l'inventaire" d'une société et une classe Employee qui représenterait les employés de cette société.

    Ces deux classes pourraient avoir comme caractéristique d'être identifiable de manière unique et non ambigüe par une fonction int id() const. Mais ce n'est absolument pas une raison pour envisager la possibilité de créer une collection d'objets qui mélangerait allègrement des employés et des éléments de l'inventaire:

    Ce n'est pas parce que ces deux classes présentent une caractéristique commune que l'héritage public devient cohérent. Dans le meilleur des cas, tu peux considérer que la fonction id() fait partie d'une interface spécifique, mais tu ne peux absolument pas considérer que l'utilisation de cette interface spécifique en vienne à créer une relation telle que, d'une manière ou d'une autre, les employés et les éléments d'inventaires puissent intervenir dans la même hiérarchie de classes.

    Notes qu'il y a tout à fait moyen d'arriver à avoir une interface commune mais sans créer cette relation de hiérarchie de classe en C++, en créant une interface à base de classe template et en profitant du CRTP:
    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
    template <typename T>
    class TIdentifiable{
        public:
            size_t id() const{return id_;}
        protected:
            template<typename ValueType>
            TIdentifiable(ValueType const& idKey):id_(std::ash(idKey)){}
            ~TIdentifiable(){}
        private:
            size_t id_;
    };
    class Employee : public TIdentifiable<Employee>{
        public:
            Employee(std::string const & name):TIdentifiable<Employee>(name){}
    };
    class InventoryItem : public TIdentifiable<InventoryItem>{
        public
            InventoryItem(size_t productNumber): TIdentifiable<InventoryItem>(productNumber){}
    };
    Les deux exposent bel et bien une fonction id(), mais tu ne pourras jamais transmettre un objet d'un type quand c'est un objet de l'autre type qui est attendu.

    Et, comme je l'ai dit, il y a une troisième raison qui devrait t'inciter à éviter la logique que tu envisages : les informations nécessaires à la configuration d'un module sont spécifiques au module en question : pour le module de rendu visuel, ce seront des options de résolution, de couleurs, de transparence ou autres, alors que pour le module son, ce seront des options relatives au volume, au nombre de haut parleurs ou que sais-je et que pour le module réseau, ce seront des options comme le nom d'utilisateur, l'adresse du serveur à contacter ou le mot de passe.

    Cela signifie que tu devrais transmettre une abstraction qui corresponde à "n'importe quel ensemble d'informations de configuration" à ta fonction setConfig afin de pouvoir en profiter.

    Le résultat est que tu finiras invariablement face à la nécessité de "jouer" avec les dynamic_cast pour déterminer si, oui ou non, l'ensemble d'options de configuration auquel tu es confronté est cohérent avec les options dont le module a besoin (sous peine d'essayer de transmettre les options de configuration du module d'affichage au module son ). Et tu perdras d'un seul coup toutes les possibilités d'évolutions que la modularisation te permettait pourtant d'espérer
    Aussi, peut être que dans ce cas l'appellation fabrique est mal choisie mais j'envisageais de créer un objet de construction qui aurait connaissance de la config et pourrait distribuer les paramètres spécifiques à chaque module lors de leur création.
    Les options de configuration sont du seul recours du module lui-même.

    Soit, il charge en interne un fichier dont le nom lui est donné "par défaut ou par ailleurs" (ailleurs étant dans le cas présent une fonction proche de loadConfig(filename) ), soit il expose des fonctions qui permettent de modifier spécifiquement une information de configuration particulière ( modifyConnectionUserName(newUserName) ou modifyScreenResolution(int resX, int resY, int deep) par exemple)

    Quoi qu'il en soit, comme les options de configuration de chaque module sont strictement adaptées au module en question, il n'y a aucun héritage envisageable, et il n'y a pas nécessité ni possibilité de faire en sorte que tous les modules interviennent dans une hiérarchie de classe commune
    C'est vrai que si on utilise de façon abusive les directives, ça peut devenir rapidement illisible et difficile à déboguer. Après, si ça reste très ciblé et explicite, la puissance que ça apporte peut en valoir le détour.
    Attention, je ne nie absolument pas la puissance des possibilités offertes par les directives préprocesseur! Mon avis personnel est juste que la technique présente souvent plus d'inconvénients que d'avantages réels, et que, dans le cas particulier que tu proposes, les inconvénients sont plus importants que les avantages espérés
    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

  5. #5
    Membre confirmé
    Profil pro
    Inscrit en
    Mars 2012
    Messages
    99
    Détails du profil
    Informations personnelles :
    Localisation : France

    Informations forums :
    Inscription : Mars 2012
    Messages : 99
    Par défaut
    La deuxième raison, c'est que la présence d'une fonction similaire dans différentes classes n'implique pas forcément qu'il doit y avoir un lien de sororité ou d'héritage entre deux classes.
    Ce n'est pas parce que ces deux classes présentent une caractéristique commune que l'héritage public devient cohérent.
    Le lien n'est peut être pas assez conséquent mais on aurait quand même des modules qui recevraient une configuration, donc une fonctionnalité commune et on resterait dans une logique de module.

    La première, c'est la loi de Déméter : En créant une fonction setConfig, tu obliges l'utilisateur de ton module à connaître un détail d'implémentation de ton module qu'il n'a normalement pas à connaître.
    En fait non, je voulais surtout créer une classe de fabrique pour lui confier le rôle de distribuer la configuration à chaque module. L'utilisateur aurait, dans mon idée, juste à faire un createModule avec le nom du module. Après c'est vrai que la méthode setConfig qui serait exposée va à l'encontre de mes propos mais dans ce cas, peut être déclarer amie la fabrique auprès de chaque module et rendre priver le setConfig ? C'est peut être un peu tiré par les cheveux remarque, parce que l'intérêt serait vraiment limité à la relation d'amitié et la méthode setConfig rendue privée n'aurait pas d'autre raison d'être.

    Et, comme je l'ai dit, il y a une troisième raison qui devrait t'inciter à éviter la logique que tu envisages : les informations nécessaires à la configuration d'un module sont spécifiques au module en question : pour le module de rendu visuel, ce seront des options de résolution, de couleurs, de transparence ou autres, alors que pour le module son, ce seront des options relatives au volume, au nombre de haut parleurs ou que sais-je et que pour le module réseau, ce seront des options comme le nom d'utilisateur, l'adresse du serveur à contacter ou le mot de passe.

    Cela signifie que tu devrais transmettre une abstraction qui corresponde à "n'importe quel ensemble d'informations de configuration" à ta fonction setConfig afin de pouvoir en profiter.

    Quoi qu'il en soit, comme les options de configuration de chaque module sont strictement adaptées au module en question, il n'y a aucun héritage envisageable, et il n'y a pas nécessité ni possibilité de faire en sorte que tous les modules interviennent dans une hiérarchie de classe commune
    C'est peut être pas terrible mais l'une de mes idées était de passer une map contenant les paramètres et leur valeur à la méthode commune setConfig. Ensuite, à l'intérieur de chaque module, on récupérerait de cette map la configuration spécifique.
    Pour la récupération de la map, on aurait un objet config sur lequel la fabrique ferait un getParams avec en paramètre le nom du module à considérer au moment même de sa création.
    Après je comprends que l'idée d'avoir un objet commun ne soit pas tout à fait logique étant donné que chaque module à sa propre configuration. Peut être que je vais rester finalement sur le chargement d'un fichier par module en interne directement.

  6. #6
    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
    [EDIT] oupps, je m'a trompé de bouton : envoyer au lieu de prévisualiser
    Citation Envoyé par sfarc Voir le message
    Le lien n'est peut être pas assez conséquent mais on aurait quand même des modules qui recevraient une configuration, donc une fonctionnalité commune et on resterait dans une logique de module.
    Cela irait dans "une logique de module", mais cela irait à l'encontre au LSP (Liskov Substitution Principle, ou, si tu préfères, au principe de substitution de Liskov).

    En programmation orientée objet, le meilleur moyen d'obtenir quelque chose de totalement ingérable, d'impossible à faire évoluer "sans tout casser" et de très difficile à débuger est de ne respecter ni la loi de Déméter ni les principes SOLID.

    Le seul service que tu pourrais avoir en commun pour l'ensemble des modules serait un service (setConfig(AbstractConfiguration * config) ), mais cela irait à l'encontre de la loi de Déméter, et je t'ai expliqué pourquoi ce serait une très mauvaise idée d'y recourir. (si tu n'as pas compris mon argumentation, ou si tu n'es pas tout à fait d'accord avec elle, n'hésites pas à me le faire savoir, cela débouche souvent sur un débat passionnant )

    Aux termes même de LSP, il y a énormément de chances pour que tu te retrouves à vouloir déclarer des services à l'intérieur de ta classe de base qui ne seront, quoi qu'il arrive, réellement utilisables qu'au niveau d'un module spécifique. Ce faisant, tu rajoutes un invariant du style "Tout module expose un service XXX" qui n'est absolument pas respecté par les différents modules, vu que cet invariant n'est valable que pour un module bien particulier. La règle de programmation par contrat qui t'impose que les invariants soient respectés ne l'est pas, l'héritage n'a pas lieu d'être
    (c'est, en gros, la même raison qui fait qu'une classe ListeTriee ne peut pas hériter de la clase Liste ou que celle qui fait que la classe Carre ne peut pas hériter de la classe Rectangle )
    En fait non, je voulais surtout créer une classe de fabrique pour lui confier le rôle de distribuer la configuration à chaque module. L'utilisateur aurait, dans mon idée, juste à faire un createModule avec le nom du module.
    Mais pourquoi vouloir passer par une fabrique alors qu'il est si facile de créer une instance de chaque module

    Ne crois tu pas qu'il est plus facile d'avoir quelque chose ressemblant à (c++11 inside)
    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
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    class RenderModule{
    public:
        RenderModule():RenderModule("render.ini"){
        }
        RenderModule(std::string const & filename):configFilename_(filename){
            loadConfig();
        } 
        void loadConfig(std::string const & filename){
           configFilename_=filename;
           loadConfig();
        }
    private :
        void loadConfig(){
            /* ... */ 
        }
        std::string configFilename_;
    }
    class SoundModule{
    public:
        SoundModule():SoundModule("sound.ini"){
        }
        SoundModule(std::string const & filename):configFilename_(filename){
            loadConfig();
        } 
        void loadConfig(std::string const & filename){
           configFilename_=filename;
           loadConfig();
        }
    private :
        void loadConfig(){
            /* ... */ 
        }
        std::string configFilename_;
    }
    class LanModule{
    public:
        LanModule():LanModule("lan.ini"){
        }
        LanModule(std::string const & filename):configFilename_(filename){
            loadConfig();
        } 
        void loadConfig(std::string const & filename){
           configFilename_=filename;
           loadConfig();
        }
    private :
        void loadConfig(){
            /* ... */ 
        }
        std::string configFilename_;
    };
    int main(){
        SoundModule sound;    // ici, on utilise le fichier de configuration par défaut
        LanModule lan("mySpecialLanConfig.ini"); // et ici, un fichier particulier
        RenderModule render;
    }
    que d'avoir quelque chose qui serait 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
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    class AbstractModule{
    public:
        AbstractModule(){
        }
        virtual ~AbstractModule(){}
        void loadConfig(std::string const & filename){
           configFilename_=filename;
           loadConfig();
        }
    protected:
       std::string const & configFilename() const{return configFileName_;}
    private :
        virtual void loadConfig() = 0;
        std::string configFilename_;
    };
     
    class RenderModule : public AbstractModule{
    public:
        RenderModule(){
        }
    private :
        void loadConfig(){
            /* ... */ 
        }
    }
    class SoundModule : public AbstractModule{
    public:
        SoundModule(){
        }
    private :
        void loadConfig(){
            /* ... */ 
        }
    }
    class LanModule : public AbstractModule{
    public:
        LanModule(){
        }
    private :
        void loadConfig(){
            /* ... */ 
        }
    };
    class ModuleFactory{
        public:
        AbstractModule * createSoundModule(){
            return new SoundModule;
        }
        AbstractModule * createRenderModule(){
            return new RenderModule;
        }
        AbstractModule * createLanModule(){
            return new LanModule;
        }
    };
     
     
    int main(){
        ModuleFactory factory;
        std::vector<AbstractModule *> allModules;
        allModules.push_back(factory.createSoundModule);
        allModules.push_back(factory.createRenderModule);
        allModules.push_back(factory.createLanModule);
        /* ... */
       allModules[0]->loadConfig("render.ini"); // Pas de bol : c'est un SoundModule :-S
    }
    Après c'est vrai que la méthode setConfig qui serait exposée va à l'encontre de mes propos mais dans ce cas, peut être déclarer amie la fabrique auprès de chaque module et rendre priver le setConfig ?
    Moi je te propose déjà de te passer de la fabrique, qui n'a purement et simplement pas lieu d'être si tu n'as pas d'héritage.

    L'héritage public est la relation la plus forte qui puisse exister entre deux types d'objets : celle qui permet de faire passer "n'importe quel objet d'un type dérivé" pour un objet du type de base.

    Vu que c'est la relation la plus forte qui existe, c'est celle qui ne doit être envisagée que lorsque l'on est parfaitement sûr qu'elle est utile, intéressante et sensée
    C'est peut être pas terrible mais l'une de mes idées était de passer une map contenant les paramètres et leur valeur à la méthode commune setConfig. Ensuite, à l'intérieur de chaque module, on récupérerait de cette map la configuration spécifique.
    Encore une fois, pourquoi vouloir mélanger les torchons et les serpillières

    La création d'une map <nom de l'option, valeur de l'option> va t'obliger à avoir recours à des mécanismes particulièrement contraignants. Ne serait-ce que pour t'assurer que la valeur que tu veux indiquer pour une clé donnée correspond au type attendu pour cette donnée.
    Pour la récupération de la map, on aurait un objet config sur lequel la fabrique ferait un getParams avec en paramètre le nom du module à considérer au moment même de sa création.
    Ce n'est de toutes manières pas le role de la fabrique.

    Idéalement, nous pourrions même aller plus loin en insistant sur le fait que chaque module devrait être utilisable dés le moment où il a été créé (que ce soit avec new dans une fabrique ou en déclarant simplement une variable du type adéquat comme je te le propose), c'est à dire, qu'il ne faudrait même pas que la fabrique ait besoin d'appeler la moindre fonction (ou toute utilisation équivalente des membres privés du module) du module entre le moment où elle le crée avec new et le moment où elle renvoie le pointeur
    Après je comprends que l'idée d'avoir un objet commun ne soit pas tout à fait logique étant donné que chaque module à sa propre configuration.
    C'est surtout que, à part les éventuels services tout à fait génériques qui seraient getConfig et setConfig, il n'y a strictement aucun service commun entre tes différents modules!

    La fonction getConfig pourrait éventuellement s'envisager afin d'être en mesure de récupérer la configuration propre à chaque module "quelque part" (qui ne soit dans aucun module en particulier) et de créer un fichier de configuration au départ de toutes ces données aggrégées, Mais setConfig n'a strictement aucun sens

    Et, encore une fois, ce n'est absolument pas parce que deux classes exposent un service similaire qu'il faut obligatoirement envisager le fait qu'elles fassent partie d'une hiérarchie de classe commune
    Peut être que je vais rester finalement sur le chargement d'un fichier par module en interne directement.
    Ceci dit, rien ne t'empêche d'avoir un seul et même fichier qui reprendrait l'ensemble des configurations des différents modules, sous 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
    [screen]
       resolution = 1920 x 1200
       color_machin = RRGGBBTT
       color_bidule = RRGGBBTT
       ...
    [sound]
       ambiance = 98%
       ffx =50%
       speech = 100%
       ...
    [lan]
        server = http://
        username = koala01
        ...
    [ ... ]
    et de faire en sorte que chaque module ne s'intéresse qu'à la "section" qui a du sens pour lui

    Un accesseur permettant de récupérer l'ensemble des informations de configuration pourrait, dans de ce cas, faciliter le travail lorsqu'il s'agit de sauvegarder la configuration
    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

  7. #7
    Membre confirmé
    Profil pro
    Inscrit en
    Mars 2012
    Messages
    99
    Détails du profil
    Informations personnelles :
    Localisation : France

    Informations forums :
    Inscription : Mars 2012
    Messages : 99
    Par défaut
    Cela irait dans "une logique de module", mais cela irait à l'encontre au LSP (Liskov Substitution Principle, ou, si tu préfères, au principe de substitution de Liskov).
    La violation du principe viendrait du fait que chaque module à un comportement très différent des autres et ne peut donc pas être représenté par un objet de base c'est ça ?

    Le seul service que tu pourrais avoir en commun pour l'ensemble des modules serait un service (setConfig(AbstractConfiguration * config) ), mais cela irait à l'encontre de la loi de Déméter
    A part dans le cas ou la méthode setConfig serait déclarée privée et une relation d'amitié avec la fabrique serait définie non ?
    L'utilisateur n'aurait donc pas connaissance de son existence.

    J'étais initialement parti sur la première méthode que tu proposes avec un fichier de config pour chaque module mais je me demandais comment gérer le cas ou on aurait un seul fichier de config avec tous les paramètres de tous les modules.
    En fait, le deuxième bout de code ressemblerait plutôt à :

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    int main(){
        Config config("config.xml");
        ModuleFactory factory;
        factory.setConfig(&config);
        // Creation des modules
        factory.createModule("Sound");
        factory.createModule("Render");
        factory.createModule("Lan");
    }
    Avec dans Factory, au moment du createModule, la récupération dans la config de la map correspondant au nom de ce module.

    La création d'une map <nom de l'option, valeur de l'option> va t'obliger à avoir recours à des mécanismes particulièrement contraignants. Ne serait-ce que pour t'assurer que la valeur que tu veux indiquer pour une clé donnée correspond au type attendu pour cette donnée.
    Mais dans la map passée en paramètres on a pour le moment toujours des chaines de caractères pour représenter les valeurs. C'est ensuite à l'objet config spécifique au module de faire la correspondance avec les types de paramètres.
    On se retrouve avec le même problème même si on charge en interne la configuration.

    Idéalement, nous pourrions même aller plus loin en insistant sur le fait que chaque module devrait être utilisable dés le moment où il a été créé (que ce soit avec new dans une fabrique ou en déclarant simplement une variable du type adéquat comme je te le propose), c'est à dire, qu'il ne faudrait même pas que la fabrique ait besoin d'appeler la moindre fonction (ou toute utilisation équivalente des membres privés du module) du module entre le moment où elle le crée avec new et le moment où elle renvoie le pointeur
    Tout à fait d'accord. Ici, au moment de la création, l'objet serait déjà exploitable même si il aurait une configuration par défaut.

    Moi je te propose déjà de te passer de la fabrique, qui n'a purement et simplement pas lieu d'être si tu n'as pas d'héritage.
    Ok je vais suivre tes conseils . L'idée était surtout ici d'avoir un objet permettant de répartir les paramètres de configuration spécifiques dans les modules.

  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 sfarc Voir le message
    La violation du principe viendrait du fait que chaque module à un comportement très différent des autres et ne peut donc pas être représenté par un objet de base c'est ça ?
    En fait, la violation du LSP interviendrait dés que tu voudrais envisager de rajouter à ta classe de base un invariant qui serait spécifique à un module en particulier.

    J'utilise le terme invariant parce que ce n'est pas que le fait de rajouter une fonction publique (bien qu'une fonction publique soit aussi un invariant) et qu'il faut considérer comme invariant n'importe quel élément de la classe :
    1. n'importe quelle fonction, quelle que soit sa visibilité
    2. une donnée membre, quelle que soit sa visibilité.
    3. Certaines conditions, qui font aussi bien office de précondition (l'objet doit être dans tel état avant d'être manipulé) que de postcondition (l'objet est d'office dans tel état après la manipulation). L'exemple classique est un carré a quatre coté égaux
    Sont autant d'invariants d'une classe

    Si tu décides de créer une classe de base pour tes modules, tu finiras forcément par estimer qu'il est préférable d'exposer un service spécifique à un type de module particulier (par exemple : le signal "monstreAttaque(idMonstre, idAttaque) directement au niveau de la classe de base, ou à estimer qu'il est "plus facile" de placer une donnée spécifique à un module particulier directement dans la classe de base.

    La question n'est pas de savoir si cela va arriver, la seul question est de savoir quand tu sera pris de cette envie Et, à ce moment là, toutes les règles de la Ppc en ce qui concerne l'héritage public (elles ne sont que trois, mais quand même ) seront violées
    A part dans le cas ou la méthode setConfig serait déclarée privée et une relation d'amitié avec la fabrique serait définie non ?
    Il est globalement admis que l'amitié utilisée a bon escient ne brise normalement pas l'encapsulation. Mais le degré d'encapsulation d'une classe peut malgré tout diminuer lorsqu'on a recours à l'amitié.

    J'insiste sur le peut et sur le à bon escient parce que ces sont deux face de la même pièce : L'amitié ne brise pas l'encapsulation, à condition que tu ne fasses pas appel au détails d'implémentation de ta classe dans la classe qui a été déclarée comme amie, par contre, cela risque de diminuer le niveau d'encapsulation de ta classe malgré tout.

    Pour faire simple : moins tu devras modifier de fonctions pour prendre une modification des détails d'implémentation en compte, plus l'encapsulation de ta classe est "forte". A ce titre, une classe qui contient 3 fonctions les données membre de la classe sera "mieux encapsulée" qu'une classe qui contient 4 fonction qui utilisent directement les données membres de la classe, et ce, quelle que soit l'accessibilité des fonctions qui manipulent les données membres.

    Le problème de l'amitié, c'est que tu risques d'estimer qu'il est plus facile d'aller directement tripatouiller dans les données membres de la classe que de faire appel aux fonctions membres-- quelle que soit leur visibilité -- qui sont prévue pour modifier ces données. Les fonctions de la classe amie qui seraient dans le cas devront alors être comptées comme "des fonctions qui utilisent directement les données membres", et participeront de facto à une "diminution" de l'encapsulation.

    En effet, si tu décide de modifier le détail d'implémentation que ces fonctions manipulent, tu devra -- forcément -- modifier ces fonctions pour prendre en compte la modification apportée au niveau des données

    C'est dans cette optique que j'insiste lourdement sur le à bon escient en ce qui concerne l'amitié
    L'utilisateur n'aurait donc pas connaissance de son existence.
    Attention, l'utilisateur doit être pris au sens large : Ce n'est pas uniquement le développeur qui décide de créer une instance de la classe! C'est tout ce qui utilise ou qui fait référence à une instance de la classe.

    Autrement dit, ta fabrique n'est rien d'autre qu'un "utilisateur" de ta classe module, et qui plus est un utilisateur qui dispose de droits accrus sur l'instance de ta classe du fait de l'amitié que tu envisages. Tu commences à situer le problème

    Je te l'accorde sans peine : le problème reste très limité, sans doute à quelques fonctions à peine de ta fabrique (si tant est qu'il y en ait déjà plus d'une). Mais, on peut décemment estimer que ton affirmation "L'utilisateur n'aurait donc pas connaissance de son existence" n'est pas tout à fait juste (ni tout à fait fausse selon ton point de vue), et on peut donc en discuter
    J'étais initialement parti sur la première méthode que tu proposes avec un fichier de config pour chaque module mais je me demandais comment gérer le cas ou on aurait un seul fichier de config avec tous les paramètres de tous les modules.
    En fait, le deuxième bout de code ressemblerait plutôt à :

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    int main(){
        Config config("config.xml");
        ModuleFactory factory;
        factory.setConfig(&config);
        // Creation des modules
        factory.createModule("Sound");
        factory.createModule("Render");
        factory.createModule("Lan");
    }
    Avec dans Factory, au moment du createModule, la récupération dans la config de la map correspondant au nom de ce module.
    Apparemment, tu as répondu alors que j'étais en pleine édition de mon intervention précédente, et nos messages se sont croisés

    D'abord, il faut te dire que le format dans lequel la configuration est sauvegardée n'aura qu'une importance "marginale" sur tout le reste.

    Mais, si tu rends la fabrique responsable de la sélection des options à transmettre aux différents modules, c'est le SRP (Single Responsability Principle ou le principe de la responsabilié unique, identifié par le S de SOLID) que tu violes.

    Ce principe est simple : chaque fonction, chaque classe, ne doit s'occuper que d'une chose bien précise, afin de s'assurer qu'elle s'en occupe correctement

    La responsabilité de la fabrique, c'est de créer des objets. La responsabilité qui consiste à "faire le tri des options" afin de les transmettre aux différents modules, c'est une autre responsabilité qui, en vertu du SRP, devrait échoir à "autre chose" qu'à la fabrique (une fonction ou une classe). Quitte à ce que ce soit la fabrique qui délègue le boulot à la fonction ou à la classe en question une fois qu'elle a créé le module .

    Mais, encore une fois, nous ne sommes toujours pas dans un contexte dans lequel le recours à la fabrique est envisageable. Ce qui ne veut, bien sur, absolument pas dire que le recours à cette "autre chose" ne puisse pas être envisagé . C'est cela qui est bien, quand on distingue clairement chaque responsabilité : il est facile d'extraire l'objet qui est responsable d'un aspect particulier d'un contexte (celui de la fabrique, en l'occurrence) pour le placer dans un autre contexte (celui dans lequel nous n'avons pas de fabrique parce que nous n'en avons simplement pas besoin ).

    Tu pourrais (non, tu devrais) donc envisager la création d'une classe -- mettons ConfigLoader -- qui s'occuperait de charger la configuration -- quelque soit le format du fichier, que ce soit dans des fichiers distincts ou dans un seul fichier unique -- et qui exposerait des services permettant de mettre à jour les informations de configuration de l'ensemble de tes modules.

    Cette classe pourrait prendre une forme qui serait 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
    class ConfigLoader{
    public:
        ConfigLoarder(std::string const & filename){
            /* si c'est un fichier unique, on remplit dataMap_ ici
             */
        }
        void updateConfig(SoundModule & sound) const{
            /* ce qui est spécifique au module son */
        }
        void updateConfig(RenderModule & sound) const{
            /* ce qui est spécifique au module d'affichage */
        }
        void updateConfig(LanModule & sound) const{
            /* ce qui est spécifique au module réseau */
        }
        /* ... les autres modules */
    private:
    std::map<std::string, Untype> dataMap_; //(*)
    };
    (*) ou "un type" serait un type adapté permettant de représenter n'importe quelle valeur pour les options de configuration (boost::variant ou similaire )
    qui pourrait donc, étant donné que je m'efforce de te faire renoncer à l'idée de la fabrique, être utilisé sous 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
    int main(){
        SoundModule sound;
        RenderModule render;
        LanModule lan;
        /* tous les autres modules */
        ConfigLoader loader("config.xml");
        loader.updateConfig(sound);
        loader.updateConfig(render);
        loader.updateConfig(lan);
        /* ... tous les autres modules */
    }
    Si, vraiment, tu restes convaincu que la fabrique est la solution (mais bon, je ne le suis personnellement pas), cela pourrait prendre la forme 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 Factory{
        public:
            Factory(std::string const & configFilename):loader_(configFileName){}
            AbstractModule * create(std::string const & what){
                if(what=="sound"){
                    SoundModule * module = new SoundModule;
                    loader_.updateConfig(*module);
                    return module;
                } else if(what=="render"){
                    RenderModule * module = new RenderModule;
                    loader_.updateConfig(*module);
                    return module;
                } else if(what=="lan"){
                    LanModule * module = new LanModule;
                    loader_.updateConfig(*module);
                    return module;
                }
                assert(!"you should never come here");
                return nullptr;
            }
        private:
            ConfigLoader loader_;
    };
    Mais (parce qu'il y a toujours un mais ) c'est l'OCP (Open/Close Principle ou principe ouvert/fermé, représenté par le O de solid) qui serait violé sous cette forme.

    Alors, bien sur, Il y aurait moyen de faire en sorte de respecter l'OCP, par exemple, en travaillant avec une std::map<std::string, AbstractModule *> au sein de la fabrique et en utilisant l'idiome de l'objet clonable, par exemple .

    Mais je ne suis pas particulièrement partisan des objets clonables et puis, cela obligerait à transformer notre classe ConfigLoader en visiteur de AbstractModule, et, par conséquent, à faire de chaque type dérivé de AbstractModule un objet visitable (c'est du moins la première solution qui me vient à l'esprit pour éviter profiter du double-dispatch indispensable dans cette situation).

    Nous finirions donc avec quelque chose qui ressemblerait à
    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
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    class AbstractModule{
        public:
            virtual void updateConfig(ConfigLoader const & ) = 0;
            virtual AbstractModule * clone() const = 0;
            /* tout le reste */
    };
    class SoundModule : public AbstractModule{
        public:
            virtual void updateConfig(ConfigLoader const & loader){
                loader.updateConfig(*this);
            }
            AbstractModule * clone() const{
                return new SoundModule;
            }
           /* tout le reste */
    };
    class RenderModule : public AbstractModule{
        public:
            virtual void updateConfig(ConfigLoader const & loader){
                loader.updateConfig(*this);
            }
            AbstractModule * clone() const{
                return new RenderModule;
            }
           /* tout le reste */
    };
    class LanModule : public AbstractModule{
        public:
            virtual void updateConfig(ConfigLoader const & loader){
                loader.updateConfig(*this);
            }
            AbstractModule * clone() const{
                return new LanModule;
            }
           /* tout le reste */
    };
    class ConfigLoader{
    public:
        ConfigLoarder(std::string const & filename){
            /* si c'est un fichier unique, on remplit dataMap_ ici
             */
        }
        void updateConfig(SoundModule & sound) const{
            /* ce qui est spécifique au module son */
        }
        void updateConfig(RenderModule & sound) const{
            /* ce qui est spécifique au module d'affichage */
        }
        void updateConfig(LanModule & sound) const{
            /* ce qui est spécifique au module réseau */
        }
        /* ... les autres modules */
    private:
    std::map<std::string, Untype> dataMap_; //(*)
    };
    class Factory{
        public:
            Factroy(std::string const & configFileName):loader_(configFilename){}
            AbstractModule * create(std::string const & what){
               auto it & found =modules_.find(what);
                if(found == modules_.end()){
                    assert(!"You should never come here");
                    return nullptr;
                }
                AbstractModule * copy = it.second->clone();
                copy->updateConfig(loader_);
                return copy;
            }
        ConfigLoader loader_;
        std::map<std::string, AbstractModule *> modules_;
    };
    Avoues que cela commencerait à faire beaucoup, non . Et tout cela, simplement parce que tu voudrais forcer une relation forte qui n'a pas forcément lieu d'exister entre tes différents modules

    PS: en plus de tout, je n'ai fait qu'égratigner la surface ici... Il y aura sûrement des conséquences supplémentaires
    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

  9. #9
    Membre confirmé
    Profil pro
    Inscrit en
    Mars 2012
    Messages
    99
    Détails du profil
    Informations personnelles :
    Localisation : France

    Informations forums :
    Inscription : Mars 2012
    Messages : 99
    Par défaut
    En fait, la violation du LSP interviendrait dés que tu voudrais envisager de rajouter à ta classe de base un invariant qui serait spécifique à un module en particulier.

    Si tu décides de créer une classe de base pour tes modules, tu finiras forcément par estimer qu'il est préférable d'exposer un service spécifique à un type de module particulier (par exemple : le signal "monstreAttaque(idMonstre, idAttaque) directement au niveau de la classe de base, ou à estimer qu'il est "plus facile" de placer une donnée spécifique à un module particulier directement dans la classe de base.

    La question n'est pas de savoir si cela va arriver, la seul question est de savoir quand tu sera pris de cette envie Et, à ce moment là, toutes les règles de la Ppc en ce qui concerne l'héritage public (elles ne sont que trois, mais quand même ) seront violées
    En gros, dés que je sens que j'ai besoin de faire un dynamic_cast quelque part pour manipuler un objet dérivé plutôt que l'objet de base, c'est que je viole le LSP c'est ça ?
    Pourtant c'est assez courant de se retrouver dans la situation ou on ajoute un membre spécifique dans une classe dérivée ou une méthode.

    Attention, l'utilisateur doit être pris au sens large : Ce n'est pas uniquement le développeur qui décide de créer une instance de la classe! C'est tout ce qui utilise ou qui fait référence à une instance de la classe.

    Autrement dit, ta fabrique n'est rien d'autre qu'un "utilisateur" de ta classe module, et qui plus est un utilisateur qui dispose de droits accrus sur l'instance de ta classe du fait de l'amitié que tu envisages. Tu commences à situer le problème?
    Ok je comprends mieux, je limitais l'utilisateur à la portée de la classe Application en gros alors que c'est effectivement beaucoup plus large. Toute méthode déclarée publique exposerait alors une nouvelle fonctionnalité à l'utilisateur oui.

    Si, vraiment, tu restes convaincu que la fabrique est la solution (mais bon, je ne le suis personnellement pas), cela pourrait prendre la forme 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 Factory{
        public:
            Factory(std::string const & configFilename):loader_(configFileName){}
            AbstractModule * create(std::string const & what){
                if(what=="sound"){
                    SoundModule * module = new SoundModule;
                    loader_.updateConfig(*module);
                    return module;
                } else if(what=="render"){
                    RenderModule * module = new RenderModule;
                    loader_.updateConfig(*module);
                    return module;
                } else if(what=="lan"){
                    LanModule * module = new LanModule;
                    loader_.updateConfig(*module);
                    return module;
                }
                assert(!"you should never come here");
                return nullptr;
            }
        private:
            ConfigLoader loader_;
    };
    Mais (parce qu'il y a toujours un mais ) c'est l'OCP (Open/Close Principle ou principe ouvert/fermé, représenté par le O de solid) qui serait violé sous cette forme.
    En fait, comme j'envisageais initialement d'utiliser une directive de préprocesseur, j'allais éviter de violer le principe ouvert/fermé . Cela m'aurait permis d'anticiper tout type de module avec une logique de nom déjà connue pour l'application.

    Avoues que cela commencerait à faire beaucoup, non . Et tout cela, simplement parce que tu voudrais forcer une relation forte qui n'a pas forcément lieu d'exister entre tes différents modules
    Vu comme ça, je suis entièrement d'accord .

    Tu pourrais (non, tu devrais) donc envisager la création d'une classe -- mettons ConfigLoader -- qui s'occuperait de charger la configuration -- quelque soit le format du fichier, que ce soit dans des fichiers distincts ou dans un seul fichier unique -- et qui exposerait des services permettant de mettre à jour les informations de configuration de l'ensemble de tes modules.

    Cette classe pourrait prendre une forme qui serait 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
    class ConfigLoader{
    public:
        ConfigLoarder(std::string const & filename){
            /* si c'est un fichier unique, on remplit dataMap_ ici
             */
        }
        void updateConfig(SoundModule & sound) const{
            /* ce qui est spécifique au module son */
        }
        void updateConfig(RenderModule & sound) const{
            /* ce qui est spécifique au module d'affichage */
        }
        void updateConfig(LanModule & sound) const{
            /* ce qui est spécifique au module réseau */
        }
        /* ... les autres modules */
    private:
    std::map<std::string, Untype> dataMap_; //(*)
    };
    L'idée me plait bien. Il faudra juste s'assurer qu'il y ait un niveau d'abstraction pour les modules comme une interface pour éviter que des modifications dans un module aient des répercussions dans configLoader.
    Pour le type qui permettrait de représenter n'importe quelle valeur, comme je développe sous Qt, il n'y a pas de soucis mais j'aurais dans ce cas un ConfigLoader dépendant de ma bibliothèque.
    Je pourrais peut être définir un ConfigLoader abstrait hérité par un QConfigLoader dans lequel je manipule avec la bibliothèque Qt ?

    D'ailleurs le principe OCP n'est pas violé dans ta suggestion ? Pour un nouveau module, on sera obligé d'ajouter une nouvelle méthode de chargement de configuration.

  10. #10
    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 sfarc Voir le message
    En gros, dés que je sens que j'ai besoin de faire un dynamic_cast quelque part pour manipuler un objet dérivé plutôt que l'objet de base, c'est que je viole le LSP c'est ça ?
    Pourtant c'est assez courant de se retrouver dans la situation ou on ajoute un membre spécifique dans une classe dérivée ou une méthode.
    Non, le problème du dynamic_cast n'a rien à voir avec le LSP.

    Ce qui est contraire au LSP serait plutôt d'avoir quelque chose qui ressemble à
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    class AbstractModule{
    public:
        virtual void monsterAttack(/* parametres*/); //spécifique au module "data"
        virtual void drawData(/* parametres*/); //spécifique au module "Render"
        virtual void playFxSound(/* paramètres*/); // spécifique au module son
        virtual void sendInformation(/* paramètres*/); spécifique au modue "lan"
        virtual void loadData(/* paramètres */); // spécifique au module "serialisation"
    };
    Ce sont autant de services qui sont spécifiques à un module bien particulier, mais qui sont directement accessibles depuis le type de base.

    De tels services n'ont strictement rien à faire dans le type de base, parce qu'il ne correspondent purement et simplement à aucun service que l'on serait en droit d'attendre de la part part d'un module particulier : tu n'a besoin de playFxSound que dans le module son et ce service n'a aucun sens dans les autres modules.

    Cette situation se retrouve régulièrement mais va à l'encontre du LSP parce que les services exposés par le type de base ne correspondent à aucun service spécifique dans la grosse majorité des types dérivés.

    Et comme les services exposés par le type de base correspondent à des invariant du type "expose la possibilité de fournir tel service", que les invariants du type de base doivent impérativement être respectés par le type dérivés et que ce n'est pas le cas, LSP n'est pas respecté, et tu fait, pour simplifier, une erreur conceptuelle en agissant de la sorte

    Le problème du dynamic_cast n'est pas forcément le non respect du LSP, mais ouvre grand la porte au non respect de l'OCP dans certaines circonstances.

    Si tu peux te contenter de tester un seul et unique dynamic_cast et donc d'avoir uniquement une alternative : tu fais soit quelque chose si le transtypage réussit, soit tu fait quelque chose si le transtypage échoue, sous la forme de
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    void SomeClass::doSomething(AbstractModule * module){
        if(dynamic_cast<SoundModule*>(module) != nullptr){
            /* ce qui est spécifique à SoundModule */
        }else{
            /* tout ce qui est n'est pas spécifique à SoundModule, quel que soit
             * soit le type réel du module
             */
        }
    }
    Un tel code n'est pas l'idéal, parce qu'il implique que le développeur du type de base doit connaître un type dérivé, mais l'OCP est malgré tout observé : si tu rajoutes un nouveau type de module, ce ne sera forcément pas un nouveau type "SoundModule", et donc, tu passes dans le else. Tout va dans le mieux dans le meilleur des mondes.

    Le problème, c'est lorsque tu en arrive à un code 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
    void SomeClass::doSomething(AbstractModule * module){
        if(dynamic_cast<SoundModule*>(module) != nullptr){
            /* ce qui est spécifique à SoundModule */
        }else if(dynamic_cast<BusinessModule*>(module) != nullptr){
            /* ce qui est spécifique à BusinessModule */
        }else if(dynamic_cast<RendereModule*>(module) != nullptr){
            /* ce qui est spécifique à RenderModule */
        }else if(dynamic_cast<LanModule*>(module) != nullptr){
            /* ce qui est spécifique à LanModule */
        }else{
            assert(!"You should never come here");
        }
    }
    Si, un jour, tu en viens à décider que "tiens, il serait pas mal de rajouter le module Bidule", tu devras rechercher cette logique partout (et elle risque d'apparaître à de nombreux endroits !!!) pour rajouter le nouveau module dans la logique.

    L'OCP n'est pas respecté parce que, pour ajouter une évolution, tu es obligé de modifier en profondeur des comportements qui ont pourtant été validé dans leur état actuel

    La meilleure solution est alors d'utiliser le double dispatch, parce que le pointeur qui "passe pour être de type AbstractModule" pointe sur un objet qui sait pertinemment de quel type particulier il est : l'objet de type SoundModule sait pertinamment que, même s'il est considéré comme étant un AbstractModule, il est, malgré tout, de type SoundModule. Et il en va de même pour tout les autres modules

    Une des solution pour arriver à profiter du double dispatche est le fameux desing pattern "visiteur". mais ce n'est pas la seule possibilité . Mais cela signifie que la logique qui devra permettre d'accéder au double dispatch est un service qui devra se trouver au niveau du type de base et qui devra être "suffisemment imprécis" pour pouvoir être applicable "à l'ensemble des types dé module réel". Et c'est, effectivement, là qu'on se rend compte qu'il y a "quelque chose qui ne va pas forcément" au niveau du LSP (les fonctions dont j'ai parlé plus haut dans cette information ne sont pas des fonction susceptibles de se retrouver dans la classe de base).

    Finalement, les principe SOLID s'imbrique énormément les uns par rapport aux autres. Une mauvaise décision quand à un service exposé va, quasi systématiquement, tôt ou tard choquer soit au niveau de la responsabilité unique, soit au niveau du LSP

    Pour information, je suis sur le point d'éditer[ un bouquin sur les bonnes pratiques de programmation, dont la loi de Déméter, les principes SOLID et les problèmes liés à la programmation par contrat ([EDIT] La parution dans les bacs est prévue pour le 17 février ) . Sur un bouquin qui fait à peu près 400 pages, dont près de 200 correspondent à une étude de cas (c'est un projet complet, pour lequel je pars des spécifications pour terminer par l'implémentation de la fonction main(), avec tout ce qu'il peut y avoir entre les deux ), il y en a environ 40 sont exclusivement dédiées aux principes SOLID, à la loi de Déméter et aux règles de la programmation par contrat. Tu imagine un peu l'importance que ces différents aspects peuvent prendre en termes de conception .
    Ok je comprends mieux, je limitais l'utilisateur à la portée de la classe Application en gros alors que c'est effectivement beaucoup plus large. Toute méthode déclarée publique exposerait alors une nouvelle fonctionnalité à l'utilisateur oui.
    De manière générale, il y a une chose qui sera toujours vraie : n'importe quel fonction ne sera utile que si elle est "utilisée" par autre chose.

    Le fait qu'elle soit utilisée par une fonction membre de la classe elle-même, par une fonction libre ou par une fonction membre d'une autre classe n'a strictement aucune importance: Une fonction ne prend du sens que si elle est utilisée par "autre chose d'autre", et cet "autre chose d'autre" est "forcément" l'utilisateur de la fonction envisagée
    En fait, comme j'envisageais initialement d'utiliser une directive de préprocesseur, j'allais éviter de violer le principe ouvert/fermé . Cela m'aurait permis d'anticiper tout type de module avec une logique de nom déjà connue pour l'application.
    Cela n'aurait strictement rien empêché du tout. Cela n'aurait sans doute eu pour seuls résultats:
    • de rendre le code plus difficile à débugger
    • de "déplacer" l'endroit où un des principe SOLID ne serait pas respecté
    Alors, bien sur, peut être est ce que tu te serais rendu compte que c'est plutôt SRP qui n'est pas respecté, à cause d'un endroit du code où l'OCP ne l'est pas, mais, quoi qu'il en soit, ce serait un emplâtre sur une jambe de bois, tendant de corriger une erreur de conception à la base
    L'idée me plait bien. Il faudra juste s'assurer qu'il y ait un niveau d'abstraction pour les modules comme une interface pour éviter que des modifications dans un module aient des répercussions dans configLoader.
    Idéalement, pour respecter DIP (Dependency Inversion Principle, ou le principe d'inversion des dépendances, le D de solid ), oui, ce serait sans doute "ce qui conviendrait" en termes de conception pure (à bien y réfléchir,j'ai l'impression que j'ai expliqué chacun des principes SOLID au fur et à mesure de mes interventions sur cette discussion ... En manque-t-il un ).

    Il faut cependant éviter de tomber dans ce que l'on appelle "l'over-engeeniering" (ou le fait de vouloir avoir une conception trop compliquée par rapport à tes besoins spécifiques.

    La "conception idéale" voudrait, en effet, que tu sois en mesure de décider "à n'importe quel moment" d'utiliser plutôt le format .ini ou n'importe quel format de ton propre cru pour charger les options de configuration.

    La question est donc de savoir si "cela vaut vraiment la peine". Et la réponse n'est pas forcément affirmative

    On peut en effet se dire que le choix d'utiliser un format XML est, à peu de choses près, ferme et définitif. Créer une interface qui n'apporte strictement rien parce que ta classe serait la seule à spécifier le (les) comportement(s) pour un format spécifique qui serait de toutes manières le seul format utilisé, cela revient vraiment à créer "une interface inutile", et donc à tomber dans l'over-engeeniering

    En plus, l'extraction du (des) services le (les) plus générique(s) comme "updateConfig" afin de créer une interface qui expose ce service reste malgré tout simpliste. On pourrait donc assez facilement te répondre YAGNI (You Ain't Gonna Need It, tu n'en as pas besoin pour l'instant) ou même KISS(Keep It Simple, Stupid, "gardes les choses simples, idiotes") pour justifier le fait de "postposer" la création de l'interface tant que tu n'en as pas vraiment besoin.

    Si, "un jour", tu décides de permettre l'utilisation du format ini ou d'un format maison, il sera toujours temps d'extraire l'interface à ce moment là
    Pour le type qui permettrait de représenter n'importe quelle valeur, comme je développe sous Qt, il n'y a pas de soucis mais j'aurais dans ce cas un ConfigLoader dépendant de ma bibliothèque.
    C'est, d'une certaine manière, le problème de nombreuses bibliothèques IHM: il y a forcément un endroit où il faut que certains types de données propres à la bibliothèque rencontre des données plus "standard".

    Ce qu'il faut, c'est arriver à rendre les différents modules dépendant du "stricte minimum" issu de ta bibliothèque IHM.

    L'objet LoadConfig pourras sans doute utiliser QString et QVariant, et, allez, comptons large, QMap, mais il n'y a aucune raison de le rendre dépendant des types réellement proches de la création d'IHM dont les QWidget ou les QGraphicsXXX (comprend QGraphicsItem, QGraphicsView et QGraphicsScene, pour les principaux ).

    Le module d'affichage aura sans doute des dépendances plus fortes, parce que, selon ce que tu veux afficher, il risque d'avoir besoin aussi bien des classes héritant de QWidget que de QGraphicsXXX.

    L'idéal, pour le module Business (les données métier en elles-même ) serait qu'il puisse se contenter des dépendances envers la SL uniquement (n'utiliser que ce qui est fourni par le standard C++). C'est faisable, mais il est parfois tentant d'utiliser des classes comme QString, QPoint ou autres

    Pour les autres modules (LanModule, SoundModule, ...), ben, je vais te laisser réfléchir à ce qui est vraiment utile/ indispensable comme dépendances envers le framework Qt
    Je pourrais peut être définir un ConfigLoader abstrait hérité par un QConfigLoader dans lequel je manipule avec la bibliothèque Qt ?
    La chance est avec nous : il existe une classe QSettings, mais il n'existe aucune classe proche de QSettingsLoader.

    Du coup, si tu veux réellement créer une interface, tu pourrais créer une interface nommée AbstractSettingsLoader dont une classe dérivée serait spécialisée pour l'utilisation de la classe -- fournie par Qt -- QSettings.

    Mais on en revient aux questions que je posais la tout de suite : est ce que cela vaut réellement la peine n'est-il pas préférable d'attendre d'en avoir réellement besoin pour faire la distinction entre l'interface et la classe dont tu as réellement besoin à l'instant présent

    D'ailleurs le principe OCP n'est pas violé dans ta suggestion ? Pour un nouveau module, on sera obligé d'ajouter une nouvelle méthode de chargement de configuration.
    L'OCP est violé lorsque tu en viens à devoir modifier un comportement existant.

    Lorsque l'on parle de l'OCP on peut préciser "un code doit être ouvert aux évolutions (l'ajout d'un comportement particulier peut être considéré comme une évolution ) mais fermé aux modifications (le fait de devoir modifier un comportement pour prendre une évolution en comte).
    Un code proche de
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    void UnType::doSomething(Module * module){
        if(dynamic_cast<SoundModule *>(module){
            //invocation d'un comportement spécifique pour SoundModule
        }else if(dynamic_cast<RenderModule *>(module){
            //invocation d'un comportement spécifique pour RenderModule
        } else if /* ... */
    }
    viole l'OCP parce que tu devra modifier le comportement de doSomething pour prendre en compte l'ajout du module LanModule, par exemple.

    Par contre, un code proche de
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    class UnType{
        publc:
            void doSomething(SoundModule /* const */ & sound) /* const */{
                /* on sait que nous travaillerons d'office avec un SoundModule ici*/
            }
            void doSomething(RenderModule /* const */ & render) /* const*/ {
                /* on sait que nous travaillerons d'office avec un RenderModule ici*/
            }
            /* ... */
    };
    va respecter l'OCP : nous n'aurons besoin de modifier ni void doSomething(SoundModule /* const */ & sound) ni void doSomething(RenderModule /* const */ & render) pour ajouter le support d'un LanModule: nous allons, "tout simplement" rajouter une surcharge du comportement qui permettra de l'adapter au module en question

    Après, il y a certains aspects qui méritent sans doute d'être pris en compte, comme le fait que l'idéal est de rajouter les comportements requis dans une classe dérivée de celle à laquelle on souhaite rajouter le comportement qui nous manque. C'est parfois faisable, mais c'est parfois impossible

    Dans le cas présent (on est fort proche du patron "visitieur" ici, que les différents modules interviennent ou non dans une hiérarchie commune dont la classe de base serait AbstractModule), cela impliquerait que nous serions en mesure de faire la différence "par ailleurs" entre UnType et le type qui en dérive pour savoir si l'on peut transmettre ou non notre module "LanModule" (celui dont on veut rajouter la gestion).

    Cela implique aussi de se poser la question de savoir s'il entre bel et bien dans la responsabilité de UnType d'être en mesure de gérer le module LanModule, ou si c'est de la seule responsabilité du type qui en dériverait (histoire de respecter les invariants, et donc le LSP ainsi que le SRP )

    Mais, l'un dans l'autre, que l'on ajoute un comportement au type UnType ou que l'on rajoute ce comportement à un type dérivé de UnType, le résultat est le même : on n'a pas à toucher au code existant pour intégrer l'évolution, et l'OCP est donc respecté
    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

  11. #11
    Membre confirmé
    Profil pro
    Inscrit en
    Mars 2012
    Messages
    99
    Détails du profil
    Informations personnelles :
    Localisation : France

    Informations forums :
    Inscription : Mars 2012
    Messages : 99
    Par défaut
    Ce qui est contraire au LSP serait plutôt d'avoir quelque chose qui ressemble à
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    class AbstractModule{
    public:
        virtual void monsterAttack(/* parametres*/); //spécifique au module "data"
        virtual void drawData(/* parametres*/); //spécifique au module "Render"
        virtual void playFxSound(/* paramètres*/); // spécifique au module son
        virtual void sendInformation(/* paramètres*/); spécifique au modue "lan"
        virtual void loadData(/* paramètres */); // spécifique au module "serialisation"
    };
    Présenté comme ça, j'ai l'impression que le principe LSP est le plus simple à respecter. Peut être que je me trompe mais "remonter" une fonctionnalité spécifique dans une classe de base semble illogique et c'est l'une des premières choses qui s'apprend avec la notion d'héritage.

    Le problème, c'est lorsque tu en arrive à un code 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
    void SomeClass::doSomething(AbstractModule * module){
        if(dynamic_cast<SoundModule*>(module) != nullptr){
            /* ce qui est spécifique à SoundModule */
        }else if(dynamic_cast<BusinessModule*>(module) != nullptr){
            /* ce qui est spécifique à BusinessModule */
        }else if(dynamic_cast<RendereModule*>(module) != nullptr){
            /* ce qui est spécifique à RenderModule */
        }else if(dynamic_cast<LanModule*>(module) != nullptr){
            /* ce qui est spécifique à LanModule */
        }else{
            assert(!"You should never come here");
        }
    }
    La meilleure solution est alors d'utiliser le double dispatch, parce que le pointeur qui "passe pour être de type AbstractModule" pointe sur un objet qui sait pertinemment de quel type particulier il est : l'objet de type SoundModule sait pertinamment que, même s'il est considéré comme étant un AbstractModule, il est, malgré tout, de type SoundModule. Et il en va de même pour tout les autres modules.

    Une des solution pour arriver à profiter du double dispatche est le fameux desing pattern "visiteur". mais ce n'est pas la seule possibilité . Mais cela signifie que la logique qui devra permettre d'accéder au double dispatch est un service qui devra se trouver au niveau du type de base et qui devra être "suffisemment imprécis" pour pouvoir être applicable "à l'ensemble des types dé module réel". Et c'est, effectivement, là qu'on se rend compte qu'il y a "quelque chose qui ne va pas forcément" au niveau du LSP (les fonctions dont j'ai parlé plus haut dans cette information ne sont pas des fonction susceptibles de se retrouver dans la classe de base).
    Je ne connaissais pas vraiment le patron visiteur. Je me suis donc renseigné dessus. Par contre, je n'ai pas trouvé de bons liens expliquant le double dispatch, ça reste flou pour moi :S.
    D'ailleurs, je ne vois pas en quoi le fait d'ajouter une méthode abstraite dans la classe de base serait proche d'un patron visiteur.
    Si l'on veut partir là-dessus, on ne pourrait plus présenter ça sous la forme ci-dessus. On devrait définir un visiteur qui implémente une méthode doSomething pour chaque module non?

    Pour information, je suis sur le point d'éditer[ un bouquin sur les bonnes pratiques de programmation, dont la loi de Déméter, les principes SOLID et les problèmes liés à la programmation par contrat ([EDIT] La parution dans les bacs est prévue pour le 17 février ) . Sur un bouquin qui fait à peu près 400 pages, dont près de 200 correspondent à une étude de cas (c'est un projet complet, pour lequel je pars des spécifications pour terminer par l'implémentation de la fonction main(), avec tout ce qu'il peut y avoir entre les deux ), il y en a environ 40 sont exclusivement dédiées aux principes SOLID, à la loi de Déméter et aux règles de la programmation par contrat. Tu imagine un peu l'importance que ces différents aspects peuvent prendre en termes de conception .
    Génial ça! C'est une partie vraiment importante dans la conception et le développement d'un logiciel, partie trop souvent négligée je pense (tu es bien placé pour le savoir xD).
    A te lire en plus tu maîtrises vraiment bien le sujet. D'ailleurs la clarté de tes explications le montre bien ;-)
    J'attends avec impatience ton ouvrage! En plus, amener les bonnes pratiques autour de la réalisation d'un projet est vraiment une bonne idée. C'est tout de suite plus parlant quand on peut appliquer les principes dans des "conditions réelles".

    Il faut cependant éviter de tomber dans ce que l'on appelle "l'over-engeeniering" (ou le fait de vouloir avoir une conception trop compliquée par rapport à tes besoins spécifiques.
    C'est vrai que ça fait un peu chasse aux moustiques au lance-roquettes!
    Après, dans le cadre d'une formation aux bonnes pratiques, c'est toujours bien de le faire :-). Ca permet de développer de bons réflexes dans la conception d'un logiciel.

    Par contre, un code proche de
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    class UnType{
        publc:
            void doSomething(SoundModule /* const */ & sound) /* const */{
                /* on sait que nous travaillerons d'office avec un SoundModule ici*/
            }
            void doSomething(RenderModule /* const */ & render) /* const*/ {
                /* on sait que nous travaillerons d'office avec un RenderModule ici*/
            }
            /* ... */
    };
    va respecter l'OCP : nous n'aurons besoin de modifier ni void doSomething(SoundModule /* const */ & sound) ni void doSomething(RenderModule /* const */ & render) pour ajouter le support d'un LanModule: nous allons, "tout simplement" rajouter une surcharge du comportement qui permettra de l'adapter au module en question
    Ok pour ce point, j'ai compris .

  12. #12
    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 sfarc Voir le message
    Présenté comme ça, j'ai l'impression que le principe LSP est le plus simple à respecter. Peut être que je me trompe mais "remonter" une fonctionnalité spécifique dans une classe de base semble illogique et c'est l'une des premières choses qui s'apprend avec la notion d'héritage.
    Cela n'arrive pas forcément avec des éléments si "caractéristiques" que ce que j'ai exposé ici (ici, c'est vraiment le cas le plus simple et le plus évident.

    Mais une erreur "qui arrive souvent" est de déclarer un comportement dans la classe de base qui s'applique à une grosse majorité des classes dérivées mais qui ne s'applique pas à l'une ou à une autre classe.

    Une fonction addChildrenItem ne devrait pas se trouver dans la classe de base d'un patron de conception "composite", par exemple, parce que les éléments qui se peuvent peuvent être qu'une feuile n'ont absolument pas besoin de ce service.

    On recontre donc beaucoup plus souvent ce genre de violation que ce que tu pourrais le croire
    Je ne connaissais pas vraiment le patron visiteur. Je me suis donc renseigné dessus. Par contre, je n'ai pas trouvé de bons liens expliquant le double dispatch, ça reste flou pour moi :S.
    En fait, le double dispatche implique simplement le fait que tu as un comportement polymorphe dans une classe qui invoquera d'autres comportements spécifiques au type réel de la classe dérivée.

    Cela pourrait parfaitement prendre 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
    class Base{
        public:
           virtual void doSomething() = 0;
    }
    class Derivee1 : public Base{
        public:
            virtual void doSomething(){
                doItwithADerivee1(*this) ;
            }
    };
     
    class Derivee2 : public Base{
        public:
            virtual void doSomething(){
                doItwithADerivee2(*this) ;
            }
    };
    avec les fonctios doItwithADerivee1 et doItwithADerivee2 qui prendraient une forme proche de
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    /* cela peut être une fonction libre, ou une fonction membre d'une autre classe, ou ce que tu veux */
    void doItwithADerivee1(Derivee1 const &){
     
    }
    void doItwithADerivee1(Derivee2 const &){
     
    }
    C'est exactement ce que fait le patron de conception visiteur, à ceci près que la fonction appelée porte systématiquement le même nom et qu'il n'y a jamais que le type de l'objet transmis en paramètre qui change . Mais il n'est même pas obligatoire que le les fonctions appelées portent le même nom
    D'ailleurs, je ne vois pas en quoi le fait d'ajouter une méthode abstraite dans la classe de base serait proche d'un patron visiteur.
    Si elle a pour résultat d'appeler une fonction qui prendra spécifiquement une fonction qui manipule le type réel de l'objet, tu tombes non pas dans le cas du pattern visiteur, mais très certainement dans le cas du double dispatch. Et le patron de conception visiteur n'est qu'un exemple de double dispatch régulièrement utilisé
    Si l'on veut partir là-dessus, on ne pourrait plus présenter ça sous la forme ci-dessus. On devrait définir un visiteur qui implémente une méthode doSomething pour chaque module non?
    La question est alors : Est-ce qu'il y a vraiment du sens à permettre à chaque module d'exposer ce comportement

    Un comportement propre de updateConfiguration(optionName, value) aurait sans doute du sens pour tous les modules. Un comportement updateItemColor en aurait beaucoup moins, car il n'est réellement utile que dans le cas du RenderModule.

    Note encore une fois -- j'ai déjà beaucoup insisté sur ce point -- que ce n'est pas parce que deux classes disposent d'un comportement qui porte le même nom qu'il y a forcément une relation entre ces deux classes .

    Tu peux parfaitement observer un comportement proche de updateConfiguration(optionValue, value) dans tous les modules que tu crées, sans qu'il n'y ait de relation entre les différents modules. Et tu pourrait même envisager de travailler avec une classe template utilisant le CRTP pour créer cette interface en t'assurant qu'il n'y aura pas de rapport entre la classe ConfigurationData<RenderModule> et la classe ConfigurationModule<SoundModule>

    Génial ça! C'est une partie vraiment importante dans la conception et le développement d'un logiciel, partie trop souvent négligée je pense (tu es bien placé pour le savoir xD).
    A te lire en plus tu maîtrises vraiment bien le sujet. D'ailleurs la clarté de tes explications le montre bien ;-)
    Merci

    Disons que certains s'intéressent surtout aux optimisations fines -- voire prématurées -- moi je m'intéresse surtout à l'organisation globale du programme (enfin, entre autres à l'organisation globale des différents éléments afin d'éviter un maximum de copies inutiles )
    J'attends avec impatience ton ouvrage! En plus, amener les bonnes pratiques autour de la réalisation d'un projet est vraiment une bonne idée. C'est tout de suite plus parlant quand on peut appliquer les principes dans des "conditions réelles".
    Plus que trois semaines à attendre
    C'est vrai que ça fait un peu chasse aux moustiques au lance-roquettes!
    Après, dans le cadre d'une formation aux bonnes pratiques, c'est toujours bien de le faire :-). Ca permet de développer de bons réflexes dans la conception d'un logiciel.
    Assez bizarrement, non. Une bonne conception est une conception qui va "aussi loin en fonction des besoins que possible".

    Evidemment, ce "point limite" est spécifique à chaque projet. Et si tu dépasse ce point, tu tombes dans l'over-ingeeniering avec pour conséquence que tu pourras toujours "aller plus loin", sans que cela ne t'apporte le moindre avantage. Au contraire, cela ne fera le plus souvent que t'embrouiller et rendre ton projet impossible à appréhender correctement

    C'est presque une "question de feeling" en sommes
    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

  13. #13
    r0d
    r0d est déconnecté
    Membre expérimenté

    Homme Profil pro
    Développeur informatique
    Inscrit en
    Août 2004
    Messages
    4 293
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Ain (Rhône Alpes)

    Informations professionnelles :
    Activité : Développeur informatique

    Informations forums :
    Inscription : Août 2004
    Messages : 4 293
    Billets dans le blog
    2
    Par défaut
    Bonjour.

    Je ne suis pas sûr de bien tout comprendre, mais pourquoi ne pas passer par du template? Quelque chose comme ça:

    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
    template <typename ConfigurationType>
    class Configurable
    {
    public:
    	virtual void Configure(const ConfigurationType & configuration) {}
    };
     
    struct MySpecificConfiguration
    {
    	MySpecificConfiguration(int serialized_value) :serialized_value(serialized_value){}
    	int serialized_value;
    };
     
    class MySpecificModule : private Configurable<MySpecificConfiguration>
    {
    public:
    	void Configure(const MySpecificConfiguration & configuration) { state = configuration.serialized_value; }
    private:
    	int state;
    };
     
    int main()
    {
    	MySpecificModule a_specific_module;
    	a_specific_module.Configure(MySpecificConfiguration(5));
    	return 0;
    }
    Les templates c'est pouissant!

  14. #14
    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
    Pour toute autre chose qu'une configuration, pour des fonctionnalités qui pourraient être obtenue en ne transmettant que des éléments simples (types primitifis, chaines de caractères, primitifs propre au DSEL envisagé), je n'aurais strictement rien eu contre l'idée d'utiliser les template.

    Le problème, c'est que ton module ne pourra que rarement se contenter d'une seule et unique information de configuration! Il y aura forcément plusieurs couleurs, ou plusieurs épaisseurs de traits, ou plusieurs niveau sonores, ou différentes informations sonores (tremble, bass, tweeter, ...), bref, plusieurs informations -- de type similaire ou non -- qui devront être transmises.

    Le fait que tu appelles la fonction configure au lieu de setConfiguration ne changera pas grand chose : si tu combine toutes ces informations en une seule structure, tu obligeras l'utilisateur du module à manipuler cette structure (qui n'est qu'un détail d'implémentation) pour faire varier la configuration du module alors qu'il devrait être en mesure de faire varier les différents éléments de manière strictement autonome.

    Comprends bien que je n'ai rien contre l'existence d'une telle structure reprenant les différentes informations de configuration. l'existence de cette structure est particulièrement cohérente, normale et logique. Ce qui me chagrine, c'est qu'elle puisse être exposée à la modification à l'extérieur du module.

    Et là, on atteint les limites des template : Pour que cela puisse fonctionner, il faudrait être en mesure de modifier la valeur des effets spéciaux et du son général de manière séparée (ou l'épaisseur d'un trait particulier sans avoir à modifier l'épaisseur de tous les traits, ou le nom de l'utilisateur sans avoir à modifier celui du serveur auquel il se connecte, ou ... )

    A moins de partir dans une structure ConfigurationInfo proche de
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    template <typename CRTP, typename Type, typename Name>
    struct ConfigurationInfo{
        /* ... */
        Type value;
    };
    qui nous obligerait à travailler sous une forme proche de
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    class SoundModule : public ConfigurationInfo<SoundModule, int, fxVolume>,
                                      public ConfigurationInfo<SoundModule, int, globalVolume>,
                                      public /*...*/{
     
    };
    nous ne serions sans doute pas beaucoup plus avancés

    Et comme, par définition, tous les modules ont des données de configuration différentes, nous respecterions peut être d'avantage le DIP, mais nous verserions vraiment dans l'over-engeeniering
    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

  15. #15
    r0d
    r0d est déconnecté
    Membre expérimenté

    Homme Profil pro
    Développeur informatique
    Inscrit en
    Août 2004
    Messages
    4 293
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Ain (Rhône Alpes)

    Informations professionnelles :
    Activité : Développeur informatique

    Informations forums :
    Inscription : Août 2004
    Messages : 4 293
    Billets dans le blog
    2
    Par défaut
    Je ne comprend pas ce que tu dis Koala

    Citation Envoyé par Koala
    Le problème, c'est que ton module ne pourra que rarement se contenter d'une seule et unique information de configuration!
    Dans mon exemple, l'objet MySpecificConfiguration peut contenir toutes les données que tu veux. Il peut être un type composé, etc.

  16. #16
    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 r0d Voir le message
    Je ne comprend pas ce que tu dis Koala

    Dans mon exemple, l'objet MySpecificConfiguration peut contenir toutes les données que tu veux. Il peut être un type composé, etc.
    C'est justement ce que je reproche à cet exemple

    Je m'explique : A moins que je n'aie mal compris,tu envisages d'avoir quelque chose qui ressemblerait à
    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
    template <typename ConfigurationType>
    class Configurable
    {
    public:
    	virtual void Configure(const ConfigurationType & configuration) {}
    };
     
    struct SoundConfiguration
    {
            /* .... */
    	int fxVolume;
            int generalVolume;
            int musicVolume;
            /*...*/
    };
     
    class SoundModule : public Configurable<SoundConfiguration>{
        /*...*/
    };
    (dis moi si je fais une erreur, parce que toute ma logique découle de la situation telle qu'elle apparaît ici ).

    Comme je te l'ai dit dans mon intervention précédente, je n'ai absolument rien contre l'existence de la structure SoundConfiguration (quelle qu'en soit la forme réelle) : c'est une structure qui joue parfaitement son rôle qui est de regrouper des informations qui vont particulièrement bien ensemble.

    Par contre, ce qui me pose un problème, c'est la présence au niveau de SoundModule d'une fonction -- héritée de Configurable, mais ca, ce n'est qu'un détail -- Configure qui prend cette structure comme argument.

    Je vois trois problèmes avec cette fonction :
    1. Le premier, c'est que c'est un détail d'implémentation du module. Elle ne devrait donc pas sortir du module.
    2. Le deuxième, c'est qu'elle oblige l'utilisateur à connaitre la structure SoundConfiguration en entier, même si ce n'est que pour modifier le niveau de la musique d'ambiance (allez, généralisons : un niveau spécifique de volume )
    3. Le troisième, c'est qu'elle oblige l'utilisateur du module à s'assurer lui-même de la validité des valeurs qu'ils placera dans cette structure. Cette fonction n'est, en définitive qu'un setConfiguration() à peine caché. Si l'utilisateur décide de placer la valeur 10000 alors que les valeurs admises sont dans l'intervalle [-100,100], il y aura un petit problème
    D'une certaine manière, on en revient toujours à mon fameux exemple de voiture et de réservoir: SoundConfiguration est un "réservoir" qui n'a pas à être exposé par la voiture SoundModule
    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

  17. #17
    r0d
    r0d est déconnecté
    Membre expérimenté

    Homme Profil pro
    Développeur informatique
    Inscrit en
    Août 2004
    Messages
    4 293
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Ain (Rhône Alpes)

    Informations professionnelles :
    Activité : Développeur informatique

    Informations forums :
    Inscription : Août 2004
    Messages : 4 293
    Billets dans le blog
    2
    Par défaut
    Ok je comprends maintenant. Mais j'ai quelques objections

    Citation Envoyé par koala01 Voir le message
    Le premier, c'est que c'est un détail d'implémentation du module. Elle ne devrait donc pas sortir du module.
    Ok, alors il suffit de déclarer la structure qui contient la config à l'intérieur de la classe sur laquelle elle s'applique. Ça a un nom, mais je ne me souviens plus; c'est ce qui est utilisé dans la STL pour les iterators.

    Citation Envoyé par koala01 Voir le message
    Le deuxième, c'est qu'elle oblige l'utilisateur à connaitre la structure SoundConfiguration en entier, même si ce n'est que pour modifier le niveau de la musique d'ambiance (allez, généralisons : un niveau spécifique de volume )
    Il suffit de fournir les fonctions membres publiques nécessaires pour modifier les paramètres séparément.

    Citation Envoyé par koala01 Voir le message
    Le troisième, c'est qu'elle oblige l'utilisateur du module à s'assurer lui-même de la validité des valeurs qu'ils placera dans cette structure. Cette fonction n'est, en définitive qu'un setConfiguration() à peine caché. Si l'utilisateur décide de placer la valeur 10000 alors que les valeurs admises sont dans l'intervalle [-100,100], il y aura un petit problème
    Effectivement, ça c'est le problème. Mais il suffit alors d'ajouter un visiteur et le tour est joué.

    Oui, finalement ma solution était juste une variante du setConfiguration, mais c'était juste pour introduire l'hypothèse de l'utilisation de templates. Ce que je veux dire, c'est que j'ai fournis un code simpliste, qu'il s'agit ensuite d'adapter aux besoins. L'avantage de la solution que je propose, c'est que c'est simple. Et pour moi c'est extrêmement important

  18. #18
    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 r0d Voir le message
    Ok je comprends maintenant. Mais j'ai quelques objections

    Ok, alors il suffit de déclarer la structure qui contient la config à l'intérieur de la classe sur laquelle elle s'applique. Ça a un nom, mais je ne me souviens plus; c'est ce qui est utilisé dans la STL pour les iterators.
    Il y a de cela et puis, je me suis peut être mal exprimé en disant qu'elle n'avait pas à en sortir.

    Il peut sembler cohérent de permettre un acces en lecture directement sur cette structure, mais l'accès en écriture devrait être interdit
    Il suffit de fournir les fonctions membres publiques nécessaires pour modifier les paramètres séparément.
    C'est bien ce à quoi je veux arriver
    Ce que je veux dire, c'est que j'ai fournis un code simpliste, qu'il s'agit ensuite d'adapter aux besoins. L'avantage de la solution que je propose, c'est que c'est simple. Et pour moi c'est extrêmement important
    Simple, oui, efficace, oui! Je l'applique moi meme assez souvent.

    Mais cette technique n'est pas adaptée pour quelque chose qui englobe un ensemble de données trop "compactes" comme c'est le cas pour la "configuration générale d'un module".

    Que tu renvoies la structure qui contient les options de configuration vers un autre module (typiquement, celui qui se chargera de la sérialisation) est tout à fait compréhensible. Cela peut être "l'un des services" auquel tu es en droit de t'attendre de la part du module qui utilise la configuration.

    Par contre, c'est le fait que tu puisses injecter les options de configuration "en vrac" (fussent-elles sous la forme d'une structure spécialisée), (le setter en très gros) qui pose le réel problème
    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

  19. #19
    Membre confirmé
    Profil pro
    Inscrit en
    Mars 2012
    Messages
    99
    Détails du profil
    Informations personnelles :
    Localisation : France

    Informations forums :
    Inscription : Mars 2012
    Messages : 99
    Par défaut
    Que tu renvoies la structure qui contient les options de configuration vers un autre module (typiquement, celui qui se chargera de la sérialisation) est tout à fait compréhensible. Cela peut être "l'un des services" auquel tu es en droit de t'attendre de la part du module qui utilise la configuration.

    Par contre, c'est le fait que tu puisses injecter les options de configuration "en vrac" (fussent-elles sous la forme d'une structure spécialisée), (le setter en très gros) qui pose le réel problème
    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
    class ConfigLoader{
    public:
      ConfigLoarder(std::string const & filename){
        /* si c'est un fichier unique, on remplit dataMap_ ici
         */
      }
      void updateConfig(SoundModule & sound) const{
        /* ce qui est spécifique au module son */
      }
      void updateConfig(RenderModule & sound) const{
        /* ce qui est spécifique au module d'affichage */
      }
      void updateConfig(LanModule & sound) const{
        /* ce qui est spécifique au module réseau */
      }
    private:
      std::map<std::string, Untype> dataMap_;
    }
    Au final, on pourrait avoir un problème similaire ici.
    configLoader sera vu comme un utilisateur du module. Il faut donc veiller à ce que, en tant qu'utilisateur, il n'ait pas connaissance des config internes aux modules.
    Pour charger la configuration, on aurait alors plusieurs possibilités :
    1) Exposer la config du module (composant interne spécifique) et appeler des méthodes pour mettre à jour chaque paramètre -> lourd.
    2) Définir configLoader comme visiteur de chaque module pour mettre à jour la config interne -> ça revient à passer l'ensemble de la config à chaque module donc pas terrible.

    [** EDIT : en fait, la deuxième solution n'est peut être pas si similaire que celle qui consistait à passer une map à une méthode loadConfig pour chaque module. En effet, avec la map, on pouvait "de l'extérieur" (ici par exemple dans Appli.cpp pour l'initialisation des modules), passer n'importe quelle map, même si les paramètres n'ont rien à voir avec le module auquel on les passe.
    Ici, on garderait une méthode loadConfig mais on passerait le module ConfigLoader à chaque autre module (je fais référence au pattern visiteur mais en fait, c'est simplement un passage par référence ici puisqu'on ne vient pas ajouter des comportements au module). A l'intérieur des modules, on appellerait alors le composant représentant la configuration spécifique ModuleConfig et on lui passerait à nouveau le module ConfigLoader.
    Pour finir, ce sera donc au composant de config d'appeler depuis le ConfigLoader via une méthode getParams les paramètres qui le concernent.

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    void Module::loadConfig(ConfigLoader& configLoader)
    {
      moduleConfig->loadConfig(configLoader);
    }
     
    void ModuleConfig::loadConfig(ConfigLoader& configLoader)
    {
      std::map<std::string, QVariant> params = configLoader.getParams("module");
      // Remplissage depuis la map des paramètres internes
    }
    Comme la récupération spécifique se ferait en interne de chaque composant de configuration des modules, On ne laisse pas vraiment la possibilité à l'utilisateur de passer depuis l'extérieur directement une map qui ne correspond pas aux paramètres attendues. Qu'en pensez-vous ?
    Le bémol serait peut être d'avoir une indirection (utilisation du pimpl idiom) avec le composant interne de configuration de chaque module (nécessaire pour respecter la responsabilité unique). En effet, on aura à faire appel d'abord à loadConfig du module puis à loadConfig du composant de configuration..
    Pour le type qui "englobe" les autres, ça me gène d'utiliser QVariant ici, un type Qt à côté de type standard. Peut être devrais-je utiliser un type standard pour le représenter aussi (utilisation de boost du coup?)
    Boost ne créée pas une dépendance trop importante pour le projet en plus de Qt ? **]

    La meilleur solution d'un point de vue conception resterait de charger en interne dans chaque module un fichier de configuration spécifique mais bon, si on a 20 modules, on aura 20 fichiers, c'est trop.
    Pourtant, j'ai du mal à trouver une solution plaisante pour le chargement d'un unique fichier avec l'ensemble de la configuration.

    Tu peux parfaitement observer un comportement proche de updateConfiguration(optionValue, value) dans tous les modules que tu crées, sans qu'il n'y ait de relation entre les différents modules. Et tu pourrait même envisager de travailler avec une classe template utilisant le CRTP pour créer cette interface en t'assurant qu'il n'y aura pas de rapport entre la classe ConfigurationData<RenderModule> et la classe ConfigurationModule<SoundModule>
    Ce serait similaire au 1) ci-dessus. Et optionValue devra être précisé par le configLoader alors qu'il n'est pas censé connaître les détails d'implémentation non?

    Par ailleurs, j'ai vu dans ce tuto le paragraphe suivant :
    le principe ouvert-fermé (Open/Closed Principle, ou OCP) n'est pas respecté. Ce principe stipule que mes classes doivent être ouvertes à l'extension mais fermées aux modifications, c'est à dire qu'une évolution logicielle doit pouvoir ajouter de nouvelles fonctionnalités sans modifier le code existant. C'est bien évidemment impossible avec le visiteur : si j'ajoute un nouveau type d'objet à visiter, je dois modifier mon visiteur abstrait et toutes les classes dérivées de AbstractVisitor vont s'en trouver impactée.
    Si j'ai bien compris ce que tu as dit plus haut, ce n'est pas une violation de l'OCP. On va simplement ajouter des nouveaux comportements et ne pas modifier le code existant non?

    Plus que trois semaines à attendre
    C'est noté, je lirai ton livre avec une grande attention!
    Ca représente un sacré travail j'imagine!

Discussions similaires

  1. [PR-2010] ms project pour atelier fabrication mecanique
    Par sabeurchebil dans le forum Project
    Réponses: 1
    Dernier message: 18/03/2014, 12h33
  2. [PC fixe] Conseil pour la fabrication d'une tour
    Par DannyM9 dans le forum Ordinateurs
    Réponses: 1
    Dernier message: 10/09/2013, 08h49
  3. Modélisation BDD pour traçabilité fabrication
    Par head059 dans le forum Modélisation
    Réponses: 12
    Dernier message: 15/08/2012, 17h47
  4. Réponses: 0
    Dernier message: 26/02/2009, 22h06
  5. Réponses: 4
    Dernier message: 06/11/2003, 10h37

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