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 :

Problème de polymorphisme ou de conception


Sujet :

C++

  1. #1
    Membre confirmé Avatar de LinuxUser
    Inscrit en
    Avril 2007
    Messages
    857
    Détails du profil
    Informations forums :
    Inscription : Avril 2007
    Messages : 857
    Points : 616
    Points
    616
    Par défaut Problème de polymorphisme ou de conception
    Bonjour,

    J'ai un problème que je vais illustrer ci-dessous, par contre je ne sais pas si c'est moi qui gère mal le polymorphisme ou si c'est un problème de conception, je vais essayé d'être clair:

    On a une classe Param qui gère des paramètres:

    Param.h:
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    class Param
    {
    public:
      Param();
      ...
      double getCommonParam();
    private:
      double m_common;
    Puis deux classes qui la dérivent, Geometric (paramètres géometriques) et Numeric (paramètres numérique):

    Geometric.h
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    class Geometric : Param
    {
    public:
      Geometric();
      Geometric(double& common, double& param);
       ...
       double getGeoParam();
    private:
       double m_param;
    Numeric.h:
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    class Numeric : Param
    {
    public:
      Numeric();
       ...
       double getNumericParam();
    private:
       double m_param1;
       double m_param2;
    Puis une classe Problem (gère différents problèmes: mathématiques, physiques, ...):

    Problem.h
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    class Problem
    {
    public:
      Problem();
      ... 
    private:
      Param* m_param;
    Et une classe Math qui dérive Problem:

    Math.h
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    class Math : Problem
    {
    public:
      Math();
      ...
      void initParam(double& common, double& param);
      void initParam(double& common, double& param1, double& param2);
      void calculGeo();
     
    private:
      double m_resGeo;
      double m_resNum;
    Math.cpp:
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    #include "Math.h"
    ...
    // pas de problème, m_param est bien reconnu comme un objet de type Geo
    void Math::initParam(double& common, double& param)
    {
      m_param = new Geo(common, param);
    }
    // par contre comment faire comprendre que m_param est un Geo ici
    void Math::calcGeo()
    {
      ...
      // et faire par exemple
      m_param.getGeoParam(); // impossible, ne propose que getCommonParam()
      ...
    }
    J'avais pensé modifier void Math::initParam(double& common, double& param) :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    void Math::initParam(double& common, double& param)
    {
      Geo* geo = new Geo(common, param);
      m_param = geo;
    }
    Mais je suis pas sûr que ce soit une bonne idée et de plus je devrai ajouter un constructeur de copie avec un pointeur en argument (je sais pas comment faire cela):
    D'où l'impression que j'ai un problème de conception des classes.

    En espérant avoir été clair.

    Merci de votre aide.

  2. #2
    Expert éminent sénior

    Femme Profil pro
    Ingénieur développement logiciels
    Inscrit en
    Juin 2007
    Messages
    5 189
    Détails du profil
    Informations personnelles :
    Sexe : Femme
    Localisation : France, Essonne (Île de France)

    Informations professionnelles :
    Activité : Ingénieur développement logiciels

    Informations forums :
    Inscription : Juin 2007
    Messages : 5 189
    Points : 17 141
    Points
    17 141
    Par défaut
    ton probleme, c'est "init".

    Tu devrais utiliser des constructeurs, et l'héritage.

    par exemple
    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 Param {
    public:
        explicit Param():m_common(0){}
        explicit Param(double d):m_common(d){}
        double value() const {return m_common;}
        double& value() {return m_common;}
    private:
        double m_common;
    };
     
    class GeoParam {
    public:
        explicit GeoParam():Param(), m_geo(0){}
        explicit GeoParam(double base, double geo):Param(base), m_geo(d){}
        double geo() const {return m_geo;}
        double& geo() {return m_geo;}
    private:
        double m_geo;
    };
    Mes principes de bases du codeur qui veut pouvoir dormir:
    • Une variable de moins est une source d'erreur en moins.
    • Un pointeur de moins est une montagne d'erreurs en moins.
    • Un copier-coller, ça doit se justifier... Deux, c'est un de trop.
    • jamais signifie "sauf si j'ai passé trois jours à prouver que je peux".
    • La plus sotte des questions est celle qu'on ne pose pas.
    Pour faire des graphes, essayez yEd.
    le ter nel est le titre porté par un de mes personnages de jeu de rôle

  3. #3
    r0d
    r0d est déconnecté
    Expert éminent

    Homme Profil pro
    tech lead c++ linux
    Inscrit en
    Août 2004
    Messages
    4 262
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Ain (Rhône Alpes)

    Informations professionnelles :
    Activité : tech lead c++ linux

    Informations forums :
    Inscription : Août 2004
    Messages : 4 262
    Points : 6 680
    Points
    6 680
    Billets dans le blog
    2
    Par défaut
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    m_param.getGeoParam(); // impossible, ne propose que getCommonParam()
    Tu peux faire un dynamic_cast, mais c'est pas terrible.

    En fait, j'ai l'impression que le problème c'est que tu cherche à généraliser trop tôt. Par exemple, la fonction calcGeo() va forcément avoir besoin d'un paramètre de type Geometric, mais les autres fonctions de la classe n'en auront pas forcément besoin. Il serait donc plus logique de passer ce paramètre à calcGeo(), et ne pas l'utiliser en tant que variable membre.

    Je me trompe certainement, c'est pas facile de résoudre un problème de conception avec si peu de détails
    « L'effort par lequel toute chose tend à persévérer dans son être n'est rien de plus que l'essence actuelle de cette chose. »
    Spinoza — Éthique III, Proposition VII

  4. #4
    Membre confirmé Avatar de LinuxUser
    Inscrit en
    Avril 2007
    Messages
    857
    Détails du profil
    Informations forums :
    Inscription : Avril 2007
    Messages : 857
    Points : 616
    Points
    616
    Par défaut
    Citation Envoyé par leternel Voir le message
    ton probleme, c'est "init".

    Tu devrais utiliser des constructeurs, et l'héritage.

    par exemple
    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 Param {
    public:
        explicit Param():m_common(0){}
        explicit Param(double d):m_common(d){}
        double value() const {return m_common;}
        double& value() {return m_common;}
    private:
        double m_common;
    };
     
    class GeoParam {
    public:
        explicit GeoParam():Param(), m_geo(0){}
        explicit GeoParam(double base, double geo):Param(base), m_geo(d){}
        double geo() const {return m_geo;}
        double& geo() {return m_geo;}
    private:
        double m_geo;
    };
    Je connassais pas le mot clef explicit, par contre ça ne change rien, je n'arrive toujours pas à appeler
    m_param est considéré comme Param et non Geo.

    Pour répondre à r0d:
    J'essaye de génraliser pour faire cela propremement, et là j'ai essayer d'exposer le probleme de manière très simplifier, en réalité j'ai plus de paramètres qui sont utilisés dans toute la classe.

    Leternel:
    Y'aurait pas quelquechose à faire du coté de classe Math, du genre:
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    Math()
    {
      m_param = new Geo();
    }

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

    Informations professionnelles :
    Activité : aucun

    Informations forums :
    Inscription : Octobre 2004
    Messages : 11 614
    Points : 30 626
    Points
    30 626
    Par défaut
    Salut,

    Ce qui se passe, surtout, c'est que , comme tu déclare un pointeur sur un objet de type Param dans ta classe problème, le compilateur te fait purement et simplement confiance sur ce point : pour lui, il a bel et bien affaire à un pointeur sur un objet de type Param, même si, en réalité, il a affaire à un pointeur sur un objet dont le type dynamique est Geometric

    (au fait, je viens de voir que tu devrais préciser "public" pour l'héritage, car l'héritage de classes est privé par défaut )

    Ce qu'il te faut faire, c'est (attention, accroches toi, parce que le solutions ne sont pas si simples quand ont débute )

    Soit tu crées carrément deux classes qui héritent de problème :

    Une classe que je vais appeler ProblemeNumerique, qui disposera d'un membre de Numeric et une casse que je vais appeler ProblemeGeo qui disposera d'un membre de type Geometric.

    Dans le même temps, tu rajouterais une fonction virtuelle pure calcule (et non pas calculeGeo ou calculeNum ) que tu définirait dans chaque classe pour que le comportement s'adapte.

    Ainsi, tu pourrais assez facilement mélanger les deux genres de problème, et te contenter d'appeler "calcule()" pour les deux (et là, tu obtiendrais du polymorphisme )

    Soit, tu te dis que, de toutes manières, un problème reste un problème et que la seule chose qui change, c'est le type du paramètre, que tu feras passer pour un "param".

    A ce moment là, ce qu'il te faut, c'est sans doute ce que l'on appelle le "double dispatch".

    Pour faire simple, et pour que tu comprenne le principe, il faut comprendre que si Geometric et Numeric peuvent tous les deux passer pour un Param, ils savent très bien ce qu'ils sont en réalité : le Géométric n'aura jamais l'idée farfelue de croire qu'il est un Numeric, ni vice versa

    Ce qu'il y a de bien en C++, c'est que l'on dispose de la possibilité de surcharger les fonctions.

    C'est à dire que l'on peut créer deux fonction qui ont le même nom, le même type de retour et dont la seule différence tient dans le nombre et / ou le type de leurs arguments.

    Tu pourrais donc très bien avoir une fonction qui prend une référence (constante) sur un objet de type Geometric et, juste à coté, un fonction portant le même nom, renvoyant le même type de valeur mais prenant... une référence (constante) sur un objet de type Numeric.

    Si tu appelle cette fonction depuis une fonction membre de ton type (Géométric ou Numéric), en transmettant "ce qui est pointé par this" (c'est à dire en transmettant l'objet au départ duquel tu as appelé la fonction membre ), le compilateur choisira d'office la bonne fonction : celle qui prend une référence constante sur Geometric si elle est appelée depuis un objet qui est de ce type, ou celle qui prend une référence constante sur Numeric si on est dans l'autre cas

    Evidemment, tu me diras sans doute que
    c'est bien beau tout cela, mais si je ne peux accéder qu'aux fonctions de Param, je fais comment, moi, pour faire en sorte d'appeler une fonction membre spécifique de Numeric ou de Geometric
    Et la réponse tient encore en un mot : polymorphisme.

    Il "suffira" en effet de déclarer une fonction virtuelle pure (par exemple : execute ) dans ta classe param et de la définir (sous une forme proche de /*return */ calculeMoi(*this); en considérant que c'est le nom que tu auras donné à ta fonction surchargée ) aussi bien dans la classe Geometric que dans la classe Numeric, et le tour sera joué

    C'est une conception tellement habituelle que l'on a même créé un patron de conception (on parle de design pattern en anglais) qui met en place ce double dispatch : le pattern visiteur

    Au final, cela pourrait ressembler à quelque chose comme (d'abord sans le patron de conception visiteur
    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
    void calculeMoi(Geometric const & g)
    {
        /* ce qu'il faut faire en utilisant g */
    }
    void calculeMoi(Numeric const & n )
    {
        /* ce qu'il faut faire en utilisant n */
    }
    class Param
    {
        public:
            virtual void execute() const = 0;
            /* le reste de la classe comme avant */
    };
    class Numeric : public Param
    {
        public:
            virtual void execute() const {calculeMoi(*this);}
            /* le reste de la classe comme avant */
    };
     
    class Geometric : public Param
    {
        public:
            virtual void execute() const {calculeMoi(*this);}
            /* le reste de la classe comme avant */
    };
    avec une petite énumération (pour savoir si c'est un problème de math classique ou un problème de géo) proche de
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    enum what
    {
        algebre,
        geometrie
    };
    et une classe problème 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
    class Problem
    {
    public:
      Problem(what w /* , ...*/)
      {
           if(w ==  algebre)
               m_param = new Numeric(/* ...*/);
           else
               m_param = new Geometric(/* ...*/);
      }
      ... 
    private:
      Param* m_param;
    Bon, c'est très clairement encore perfectible, mais cela me ferait partir sur l'écriture d'un roman, et j'en prend déjà le chemin, donc, si tu veux, j'expliquerai comment encore perfectionner cela plus tard

    Par contre, je vais quand meme t'expliquer le principe du pattern visiteur

    L'idée est, tout simplement, d'encapsuler la fonction qui fera effectivement le travail (calculeMoi, dans le cas présent) dans une classe que l'on appelle "visiteur" (par opposition, Generic et Numeric sont donc... les visités )

    Le prototype de execute devient alors virtual void execute(Visiteur const & v) const et l'implémentation (dans Geometric et dans Numeric) devient proche de v.calculeMoi(*this); .

    Tu vas sans doute me demander maintenant quel avantage tu pourrais tirer à faire de la sorte

    Et, encore une fois, la réponse tient, décidément, toujours dans le même mot: polymorphisme

    En effet, si tu rend calculeMoi virtuelle, tu peux décider de faire hériter plusieurs classes de ton visiteur, et, à chaque fois, redéfinir le comportements des deux "versions" de calculeMoi

    Au final, cela t'apporte encore un "point de variation" supplémentaire, car, tu pourras choisir (de par la hiérarchie de classes issue de Param), le type de problème que tu veux résoudre, et de l'autre (de par la hiérarchie de classes issue de visiteur) n'importe quel autre aspect, comme le "niveau de difficulté" ou encore un aspect particulier que tu veux calculer / vérifier

    La seule limite que tu auras pour les différentes classes qui dériveront de visiteur étant, finalement, ton imagination ou les besoins que tu n'as pas
    A méditer: La solution la plus simple est toujours la moins compliquée
    Ce qui se conçoit bien s'énonce clairement, et les mots pour le dire vous viennent aisément. Nicolas Boileau
    Compiler Gcc sous windows avec MinGW
    Coder efficacement en C++ : dans les bacs le 17 février 2014
    mon tout nouveau blog

  6. #6
    Membre confirmé Avatar de LinuxUser
    Inscrit en
    Avril 2007
    Messages
    857
    Détails du profil
    Informations forums :
    Inscription : Avril 2007
    Messages : 857
    Points : 616
    Points
    616
    Par défaut
    Tout d'abord merci pour ton message, mais j'ai ps tout compris.


    Tu as raison désolé.
    Citation Envoyé par koala01 Voir le message
    (au fait, je viens de voir que tu devrais préciser "public" pour l'héritage, car l'héritage de classes est privé par défaut )
    C'est justement ce que je voulais éviter, je voulais faire ça de manière plus sioux.
    Citation Envoyé par koala01 Voir le message
    Soit tu crées carrément deux classes qui héritent de problème :
    Oui voilà.
    Citation Envoyé par koala01 Voir le message
    Soit, tu te dis que, de toutes manières, un problème reste un problème et que la seule chose qui change, c'est le type du paramètre, que tu feras passer pour un "param".
    Déjà là je comprends pas pourquoi le besoin des réferences.
    Citation Envoyé par koala01 Voir le message
    Tu pourrais donc très bien avoir une fonction qui prend une référence (constante) sur un objet de type Geometric et, juste à coté, un fonction portant le même nom, renvoyant le même type de valeur mais prenant... une référence (constante) sur un objet de type Numeric.
    Je vais faire des recherces la dessus.
    Citation Envoyé par koala01 Voir le message
    C'est une conception tellement habituelle que l'on a même créé un patron de conception (on parle de design pattern en anglais) qui met en place ce double dispatch : le pattern visiteur
    Citation Envoyé par koala01 Voir le message
    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
    void calculeMoi(Geometric const & g)
    {
        /* ce qu'il faut faire en utilisant g */
    }
    void calculeMoi(Numeric const & n )
    {
        /* ce qu'il faut faire en utilisant n */
    }
    class Param
    {
        public:
            virtual void execute() const = 0;
            /* le reste de la classe comme avant */
    };
    class Numeric : public Param
    {
        public:
            virtual void execute() const {calculeMoi(*this);}
            /* le reste de la classe comme avant */
    };
     
    class Geometric : public Param
    {
        public:
            virtual void execute() const {calculeMoi(*this);}
            /* le reste de la classe comme avant */
    };
    Si j'applique ça à mon exemple:
    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
     
    class Param
    {
    public:
      Param();
      double getCommonParam();
      virtual void execute() const = 0;
    private:
      double m_common;
    };
    class Geometric : public Param
    {
    public:
      Geometric();
      Geometric(double& common, double& param);
      virtual void execute() const {calculeMoi(*this);}
      double getGeoParam();
    private:
       double m_param;
    };
    class Numeric : public Param
    {
    public:
      Numeric();
      virtual void execute() const {calculeMoi(*this);}
      double getNumericParam();
    private:
       double m_param1;
       double m_param2;
    };
    class Math : Problem
    {
    public:
      Math();
      ...
      void initParam(double& common, double& param);
      void initParam(double& common, double& param1, double& param2);
      void calculGeo();
     
    private:
      double m_resGeo;
      double m_resNum;
    };
    Question 1:
    Où je déclare et implémente les fonctions calculeMoi(*this) ?

    Question 2:
    Ta fonction calculeMoi(*this) , est-ce qu'elle remplace ma focntion calculGeo()?
    Si oui, pourquoi kui passer une référence?

  7. #7
    Expert éminent sénior
    Avatar de koala01
    Homme Profil pro
    aucun
    Inscrit en
    Octobre 2004
    Messages
    11 614
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 52
    Localisation : Belgique

    Informations professionnelles :
    Activité : aucun

    Informations forums :
    Inscription : Octobre 2004
    Messages : 11 614
    Points : 30 626
    Points
    30 626
    Par défaut
    Citation Envoyé par LinuxUser Voir le message
    C'est justement ce que je voulais éviter, je voulais faire ça de manière plus sioux.
    Tu n'as, malheureusement, pas le choix car l'héritage publique et l'héritage privé représentent deux choses différentes

    Si tu cherches à avoir de la substituabilité (à pouvoir passer un objet du type dérivé à une fonction qui attend un pointeur ou une référence sur un objet du type de base), alors, tu dois utiliser l'héritage public, car l'héritage privé aura pour résultat de n'être accessible (comme tout ce qui est privé, d'ailleurs) qu'au départ d'une fonction membre de la classe héritière, et à elle seule.

    On parle alors d'une liaison "est implémenté en termes de"

    Ceci dit, comme je te l'ai indiqué, il y a la possibilité de faire en sorte que ce soit ProblèmeGeo et ProblèmeAlgebre qui héritent de problème.

    A ce moment là, tu peux effectivement avoir un héritage privé de Geometric (ou de Numeric) envers Param si et seulement si
    1. Problème déclare une fonction virtuelle qui sera suffisamment générique pour être appelée depuis ProblèmeGeo ou ProblemeAlgebre comme calcule (au lieu de calculeGeo)
    2. ProblemeGeo et ProblemeAlgebre implémentent la fonction calcule
    3. ProblemeGeo dispose d'un membre de type Geometric (et ProblemeAlgebre dispose d'un membre Numeric), en étant bien conscient que ni ProblemeGeo ni ProblemeAlgebre ne pourront accéder à la fonction value de Param (car elle sera considérée comme privée, à cause de l'héritage privé)
    Déjà là je comprends pas pourquoi le besoin des réferences.
    Le fait est que tu ne peux profiter du polymorphisme que si tu travailles avec un pointeur ou avec une référence.

    On préfère travailler avec des références car il y a garantie d'existence, qu'elles permettent de respecter la même syntaxe que si l'on travaillait directement avec l'objet et surtout parce qu'elles transmettent la const-correctness : quand une référence est constante, tu es sur de ne pouvoir utiliser que des fonction qui se sont engagées à ne pas modifier l'état de l'objet en étant déclarées constantes.



    Je vais faire des recherces la dessus.


    Si j'applique ça à mon exemple:
    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
     
    class Param
    {
    public:
      Param();
      double getCommonParam();
      virtual void execute() const = 0;
    private:
      double m_common;
    };
    class Geometric : public Param
    {
    public:
      Geometric();
      Geometric(double& common, double& param);
      virtual void execute() const {calculeMoi(*this);}
      double getGeoParam();
    private:
       double m_param;
    };
    class Numeric : public Param
    {
    public:
      Numeric();
      virtual void execute() const {calculeMoi(*this);}
      double getNumericParam();
    private:
       double m_param1;
       double m_param2;
    };
    class Math : Problem
    {
    public:
      Math();
      ...
      void initParam(double& common, double& param);
      void initParam(double& common, double& param1, double& param2);
      void calculGeo();
     
    private:
      double m_resGeo;
      double m_resNum;
    };
    Attention, il n'y a aucun sens à déclarer une fonction calculeGeo dans la classe Probleme, surtout si elle n'est pas virtuelle...

    D'abord, parce que tu ne peux réimplémenter dans une classe dérivée que les classes qui sont déclarée virtuelles

    Ensuite parce qu'il n'y a aucun sens à disposer d'une fonction dans la classe ProblèmeAlgebre, or, comme cette fontction est dans l'accessibilité publique, elle sera accessible depuis toutes les classes dérivées (y compris... ProblèmeAlgebre

    Nous sommes donc en violation du principe de substitution de Liskov (LSP de son petit nom) qui dit que toute propriété valide (comme le fait de pouvoir invoquer une fonction comme calculeGeo) de la classe mère doit être valide dans la classe dérivée

    Question 1:
    Où je déclare et implémente les fonctions calculeMoi(*this) ?
    Tu peux parfaitement en faire des fonctions libres

    Tu les déclarerait dans un fichier d'en-tête quelconque (mais idéalement séparé du reste) et tu les implémenterait dans un fichier *.cpp de ton choix (mais idéalement dans le fichier cpp du même nom que celui du fichier d'en-tête dans lequel elles sont déclarées )
    Question 2:
    Ta fonction calculeMoi(*this) , est-ce qu'elle remplace ma focntion calculGeo()?
    Non, ta fonction calculeGeo (qui devrait s'appeler simplement calcule) appelle Param.execute(), et la fonction calculeMoi n'a aucune connaissance de tes classes dérivées de Problème

    Quand Param.execute() est appelée, comme il s'agit d'une fonction virtuelle, le programme saura à l'exécution si elle est appelée depuis un objet de type Geometric ou depuis un objet de type Numeric.

    C'est ce que l'on appelle le RTTI (Run Time Type Information, ou, si tu préfères, les informations de type à l'exécution )

    Le programme s'occupera de lui même d'appeler comme un grand la version de execute qui correspond au type réel (dynamique) de l'objet depuis lequel elle est appelée, même s'il s'agit dans le code d'un pointeur ou d'une référence vers un objet de type Param (à condition toutefois que l'héritage soit public ) et la bonne version de execute (comprend Geometric.execute ou Numeric.execute) se chargera de passer l'objet depuis lequel elle est appelée à la bonne version de calculeMoi.

    Si oui, pourquoi kui passer une référence?
    Pour trois raisons essentiellement dont deux que je viens d'ailleurs de citer:
    1. Les références apportent une garantie supplémentaire par rapport au pointeur : leur garantie de non nullité (une référence doit faire... référence à un objet existant, alors qu'un pointeur peut pointer sur NULL ce qui correspond au final à un objet inexistant)
    2. Les références permettent de garantir la const correctness, et, enfin,
    3. Pour éviter la copie.
    Pour que tu comprennes bien cette troisième raison, il faut savoir que si tu déclares une fonction avec le prototype ayant la forme de void foo(UnType arg), tu transmets l'argument arg qui est de type UnType par valeur, ce qui fait que tu travailleras avec une copie de la variable que tu auras utilisée comme argument.

    Cela implique deux choses :
    1. D'abord, le fait que les modifications que tu pourrait apporter à arg ne seront pas répercutées sur la variable correspondante dans la fonction appelante, mais ca, ca peut aussi bien être un bien qu'un mal
    2. Ensuite cela implique qu'il y a tout un mécanisme de copie de ton objet qui sera effectué au moment de l'appel de la fonction, et tout un mécanisme de destruction de la variable qui sera effectué au moment où tu quittera la fonction

    La copie peut prendre énormément de temps et demander beaucoup de ressources et la destruction peut aussi demander énormément de temps.

    Si c'est une fonction qui est appelée souvent, cela risque d'avoir un impact très sérieux sur les performances, surtout si le code de la fonction fait peut de choses (en terme d'instructions processeur) car le ratio entre le temps nécessaire à la copie de l'objet + celui nécessaire à sa destruction par rapport au temps passé à faire quelque chose "d'utile" (l'exécution du corps de ta fonction) devient d'autant moins favorable que l'exécution du corps de la fonction est rapide

    Ensuite, il faut savoir que si une classe hérite (publiquement) d'une autre, elle a sémantique d'entité : il ne peut jamais exister deux instances différentes d'une classe ayant sémantique d'entité en mémoire représentant la même "chose".

    Prenons l'exemple d'une classe Personne dont chaque instance représente l'un des 7 milliards d'homme vivant sur la terre.

    Chaque instance représente une personne unique et bien déterminée, et tu t'attends à ce que tout ce que tu lui feras "subir" s'applique bel et bien à cette personne clairement identifiée.

    Si tu permet la création d'une copie de cette personne, il se peut que certaines choses que tu ferais subir s'applique sur la personne "originale" et que d'autres s'appliquent sur "la copie", et il te sera très difficile de s'avoir ce qui a été appliqué à qui

    Pour cette raison, il est préférable d'interdire la copie des classes ayant sémantique d'entité (et c'est ce que je te conseillerais très fort de faire pour tes classes Param et dérivées ainsi que pour tes classe Probleme et dérivées).

    Mais, si tu interdits la copie des objet dont le type a sémantique d'entité, il faudra bien trouver un moyen de les passer en paramètres aux fonctions qui en ont besoin

    Pour ce faire, on a deux solutions :
    • Soit on passe un pointeur sur l'objet en question (qui représente l'adresse mémoire à laquelle se trouve l'objet) , en sachant que tu devras tester systématiquement la validité du pointeur car il peut etre nul et en perdant la possibilité de profiter de la const-correctness
    • Soit on passe une référence (constante si la fonction appelée ne doit pas modifier l'objet reçu en argument), et l'on est sur non seulement que l'objet existe bel et bien (grâce à la garantie d'existence que représente une référence), mais, en plus, on est sur que l'on ne pourra appeler que les fonctions membres qui se sont engagées à ne pas modifier l'objet, que ce soit de manière directe ou indirecte, si l'on passe l'objet sous la forme d'une référence constante.
    Des deux solutions, la deuxième est très clairement celle qui nous apporte le plus de facilité et le plus de garanties, à moins, bien sur, que l'on ait une bonne raison de faire autrement, et la seule raison plausible est que l'objet que l'on passe en argument puisse ne pas exister, et qu'il faut veiller à cela au niveau de la fonction (par exemple, un membre "parent" dans un une structure d'arbre : la "racine" est un élément de l'arbre qui n'a pas de parent )
    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

  8. #8
    Membre confirmé Avatar de LinuxUser
    Inscrit en
    Avril 2007
    Messages
    857
    Détails du profil
    Informations forums :
    Inscription : Avril 2007
    Messages : 857
    Points : 616
    Points
    616
    Par défaut
    Je suis complètement perdu .

    Si vous le voulez bien, je vais encore simplifier le problème:
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    classe Mere
    {
    public:
      Mere();
      double getA();
      double getB();
      double getC();
    protected:
      double m_A;
      double m_B;
      double m_C;
    };
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    classe Fille : public Mere
    {
    public:
      Fille();
      getD();
      getE();
      getF();
    private:
      double m_D;
      double m_E;
      double m_F:
    };
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    class Main
    {
    public:
      Main();
      void calcul1():
      void calcul2();
    private:
      Mere* m_femme;
      int m_value1;
      int m_value2;
    };
    Dans Main.cpp
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    Main::Main()
    {
      m_femme = new Fille();
    }
    void Main::calcul1()
    {
      ...
      m_value1 = .... + m_femme->getD(); // marche pas 
    }
    void Main::calcul2()
    {
      ...
      m_value2 = .... + m_femme->getD() + m_femme->getE(); // marche pas
    }
    Comment faire en sorte que la variable membre soit utilisée comme un objet dans toute la classe?

  9. #9
    r0d
    r0d est déconnecté
    Expert éminent

    Homme Profil pro
    tech lead c++ linux
    Inscrit en
    Août 2004
    Messages
    4 262
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Ain (Rhône Alpes)

    Informations professionnelles :
    Activité : tech lead c++ linux

    Informations forums :
    Inscription : Août 2004
    Messages : 4 262
    Points : 6 680
    Points
    6 680
    Billets dans le blog
    2
    Par défaut
    En fait là il y a un problème de concep(tualisa)tion. Si les variables membre m_D, m_E et m_F de Fille doivent pouvoir:
    1. être accessibles par d'autres classes (via un accesseur ici)
    2. non présentes dans la classe mère
    alors Fille ne doit pas hériter de Mere.

    Tu peux voir l'héritage public comme "est un", c'est à dire que
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    class Chat : public Animal
    peut être lu comme: "Chat est un Animal".

    Cependant le c++ offre des possibilités de contourner ce type de problème, mais ça demande des construction parfois complexes.

    Ici par exemple, tu pourrais mettre, au moins en partie, la fonction de calcul à l'intérieur de ta classe fille. 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
    class Calcul : public CalculBase
    {
    public:
       double calcul( double a, double b);
    };
     
    class Fille: public Mere
    {
    public:
         double doCalcul( const Calcul & the_calcul ) { return the_calcul.calcul( m_A, mB ); }
     
    private:
         double m_A, m_B;
    };
    (code à l'arrache, juste pour l'idée)
    « L'effort par lequel toute chose tend à persévérer dans son être n'est rien de plus que l'essence actuelle de cette chose. »
    Spinoza — Éthique III, Proposition VII

  10. #10
    Membre confirmé Avatar de LinuxUser
    Inscrit en
    Avril 2007
    Messages
    857
    Détails du profil
    Informations forums :
    Inscription : Avril 2007
    Messages : 857
    Points : 616
    Points
    616
    Par défaut
    Citation Envoyé par r0d Voir le message
    En fait là il y a un problème de concep(tualisa)tion. Si les variables membre m_D, m_E et m_F de Fille doivent pouvoir:
    1. être accessibles par d'autres classes (via un accesseur ici)
    2. non présentes dans la classe mère
    alors Fille ne doit pas hériter de Mere.

    Tu peux voir l'héritage public comme "est un", c'est à dire que
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    class Chat : public Animal
    peut être lu comme: "Chat est un Animal".
    On peut considérer que la Fille a les attributs de sa mère plus ses propres attributs (m_D, m_E, m_F). Comme un chat a des griffes, ce qui n''est pas le cas de tous les animaux.

    Citation Envoyé par r0d Voir le message
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    class Calcul : public CalculBase
    {
    public:
       double calcul( double a, double b);
    };
     
    class Fille: public Mere
    {
    public:
         double doCalcul( const Calcul & the_calcul ) { return the_calcul.calcul( m_A, mB ); }
     
    private:
         double m_A, m_B;
    };
    (code à l'arrache, juste pour l'idée)
    Je comprends pas le rapport avec l'exemple que j'ai donné plus haut, désolé.
    Je m 'exprime peut être mal, mais ma question est simple, est-il possible d'utiliser une varible membre déclaré d'un type de base sous forme d'un de ses types dérivés?

    Avoir un membre Et l'instancier en Fille
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    m_membre = new Fille(); // ça c'est OK
    Et appeler les méthodes de la classe Fille
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    m_membre->getD(); // la ça coince

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

    Informations professionnelles :
    Activité : aucun

    Informations forums :
    Inscription : Octobre 2004
    Messages : 11 614
    Points : 30 626
    Points
    30 626
    Par défaut
    C'est ce que je t'ai expliqué tout au long de mes deux interventions précédentes:

    C'est normal que cela ne marche pas car m_femme est connue comme étant un pointeur sur un objet de type Mere.

    A ce moment là, tout ce que tu connait (tout ce à quoi tu peux accéder depuis m_femme-> ) de m_femme, ce sont les fonctions publiques de Mere, à savoir
    1. double getA();
    2. double getB();
    3. double getC();
    Et c'est normal : pour le compilateur (au moment où tu compiles ton code), m_felle passe pour être du type Mere.

    Il faut comprendre que la relation d'héritage publique ne va que dans un seul sens : la classe dérivée connait de la classe mère ce que la classe mère veut bien lui montrer (typiquement ce qui se trouve dans l'accessibilité publique et dans l'accessibilité protégée), mais la classe mère ignore tout des classes qui en dérivent, simplement ... parce que ce n'est pas ses affaires !!!

    Si tu décides de fournir ton projet sous la forme d'une bibliothèque, tu ne peux pas être tenu responsable de la classe que j'aurai fait dériver d'une de tes classes, on et d'accord ? Ici, le principe est le même, bien que l'on soit à l'intérieur d'un meme projet

    Pour arriver à utiliser m_femme comme étant une instance de la classe Fille, il faut avant tout... arriver à la faire passer pour ce qu'elle est réellement, c'est à dire... une instance de la classe Fille.

    Le problème, c'est que cela ne pourra se faire qu'à l'exécution (une fois que tu auras compilé ton code et qu'il fonctionnera ).

    Bon, d'accord, ce n'est pas tout à fait vrai, mais on ne va pas s'amuser à ergoter sur ce point, car l'explication serait longue et n'a de toutes manières aucun rapport avec le problème qui nous intéresse

    Pour l'instant, nous en sommes donc au point qu'il faut, à un moment donné, arriver à avoir un code qui connait m_femme (ou "quelque chose" qui correspond à m_femme) comme étant du type Fille.

    Il existe deux grand axes de solutions pour arriver à ce résultat:

    Soit on y arrive par transtypage (le terme anglais te parlera peut etre mieux : cast ) soit on y arrive en utilisant le principe du double dispatch que j'expliquais dans mes interventions précédentes.

    La solution du transtypage consiste "simplement" à dire au compilateur quelque chose comme
    Tu sais, j'ai fait passer m_femme comme étant du type Mere, mais, en réalité, elle est du type Fille
    Pour y arriver, C++ nous fournit les opérateurs de transtypage (je ne nomme ici que ceux qui nous intéresse ) static_cast<Type Desiré * ou &>(variable à transtyper) et dynamic_cast<Type Desiré * ou &>(variable à transtyper) où Type Desiré correspond au type que tu veux récupérer en précisant si tu le veux sous la forme d'une référence (éventuellement constante) ou d'un pointeur et ou variable à transtyper correspond à un pointeur (si tu veux récupérer un pointeur) ou à une référence (si tu veux récupérer une référence) sur la variable qu'il faut transtyper

    La différence entre les deux étant que dynamic_cast est capable de vérifier si la variable indiquée correspond bel et bien au type que tu demande et donc de renvoyer NULL si tu demandes un pointeur ou de lancer une exception de type std::bad_cast si tu demandes une référence, alors que static_cast ne fera pas cette vérification.

    L'un dans l'autre, si tu es sur (comme tu peux l'être ici) que m_femme est bel et bien du type Fille, tu peux parfaitement utiliser static_cast, mais, si as plusieurs classes qui héritent de Mere (ou meme de Fille) et que tu dois vérifier s'il s'agit bien du type que tu espères recevoir, il vaut mieux passer par dynamic_cast, et récupérer un pointeur (pour pouvoir le tester).

    Si je n'ai pas parlé de cette solution dans mes précédentes interventions, c'est parce que cette manière de procéder pose un énorme problème : cela oblige ta classe Main (au passage, tu devrais lui trouver un autre nom, car il risque de porter à confusion avec la fonction main() qui sert de point d'entrée au programme ), à savoir exactement non seulement de quoi est composé le type Mere, mais aussi de quoi est composé le type Fille.

    Or, dans l'absolu, si tu a déclaré m_femme comme étant un pointeur de type Mere, c'est parce que tu n'as pas envie / pas besoin que ta classe Main sache qu'il s'agit en réalité d'un objet dont le type réel est Fille.

    Bon, tu me diras sans doute que tu dois déjà tout connaitre de la classe Fille, vu que tu fait explicitement un new Fille dans ton code

    Mais, une fois que tu auras compris le principe, je t'expliquerai comment faire pour retirer cette épine de ton pied

    Mais pour que tu comprennes toute l'étendue du problème et que ton exemple est juste un peu trop simple pour t'en faire prendre conscience, on va le rendre un poil plus complexe en rajoutant une classe Fille2 et une classe Fille3 qui héritent toutes les deux de Mere, sous 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
    classe Fille2 : public Mere
    {
    public:
      Fille2();
      getG();
      getH();
      getI();
    /*...*/
    };
    classe Fille3 : public Mere
    {
    public:
      Fille3();
      getJ();
      getK();
      getL();
    /*...*/
    };
    Et l'on va juste modifier un tout petit peu le constructeur de Main pour qu'il prenne la forme de
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    Main::Main()
    {
        if(unTestQuelconque)
            m_femme = new Fille();
        else if(unAutreTest)
            m_femme = new Fille2();
        else 
            m_femme = new Fille3();
    }
    Heuu... peut etre commences tu à entrevoir le problème qui va se poser

    Allez, prend un café, un coca ou une cigarette selon tes préférences et prend le temps d'y réfléchir à ton aise

    <un café / clope / coca plus tard ... >
    Ca y est tu as trouvé quel sera le problème

    Mais oui, c'est bien sur! le fait est que, si tu commences avec le transtypage, tu devras systématiquement l'utiliser dans ta classe Main chaque fois que tu voudras utiliser m_femme comme étant du type Fille (ou Fille2 ou Fille3, selon le cas dans lequel se sera trouvé le constructeur) et que cela va obliger ta classe Main à connaitre les trois types dérivés de Mere que sont Fille, Fille2 et Fille3.

    Cela t'obligera à faire systématiquement une série de if... else, dans le meilleur des cas sur un membre numérique que tu auras pris soin de rajouter à ta classe Main (ou auquel tu peux accéder depuis la classe Mere), dans le pire des cas sur le résultat du dynamic_cast

    Et cela occasionnera deux problèmes majeurs :

    Le premier, c'est que cela va complexifier énormément ton code, car soit tu te retrouveras à écrire quelque chose comme
    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
    if( estFille )
    {
        Fille & fille = static_cast<Fille & >(m_femme);
        /* 50 lignes de code pour tout ce qui doit être fait en utilisant fille */
    }
    else if( estFille1 )
    {
        Fille1 & fille1 = static_cast<Fille1 & >(m_femme);
        /* 45 lignes de code pour tout ce qui doit être fait en utilisant fille1 */
    }
    else if( estFille2 )
    {
        Fille2 & fille2 = static_cast<Fille2 & >(m_femme);
        /* 55 lignes de code pour tout ce qui doit être fait en utilisant fille2 */
    }
    Soit tu aura veillé à "factoriser" ton code dans autant de fonctions que nécessaire, mais, quoi qu'il en soit, cela te fera une classe Main gigantesque

    En plus, il existe cinq principes qu'il est des plus importants de respecter lorsque l'on développe en orienté objets. Il sont connus sous l'acronyme SOLID et, en rendant ta classe mère responsable du fait de gérer les différents cas selon que m_femme est du type réel Fille, Fille1 ou Fille2, quelque part, tu contreviens au S mis pour SRP (Single Responsability Principle, ou , si tu préfères, le principe de la responsabilité unique).

    Ce principe est, finalement, tout bête et pourrait s'énoncer en français sous la forme de
    Citation Envoyé par interprétation personnelle
    Tout type ou fonction ne doit faire qu'une chose, mais se doit de le faire bien
    Et donc, si ta classe Main doit s'occuper de gérer les différents cas qui peuvent se poser "simplement" parce que tu te trouves face à différents types de filles possibles, elle ne devrait plus avoir à s'occuper d'autre chose.

    Or, on peut clairement estimer que, dans le cas présent, Main a d'autres choses bien plus intéressantes à faire, non

    En outre, le code tel qu'il est contrevient à un autre principe de SOLID : le O, mis pour OCP (open / close principle, ou, si tu préfères en français "Le principe ouvert / fermé").

    Ce principe pourrait s'énoncer sous la forme de
    Le code existant doit être ouvert aux évolutions mais fermé au modifications
    Autrement dit, les modifications qu'il faudrait apporter à ton code si tu décidais d'ajouter une class Fille4 dérivée de Mere et que Main peut utiliser en tant que m_femme doivent être aussi limitées que possibles.

    Dans l'état actuel des choses, tu serais obligé de repasser par l'ensemble des fonction de Main (ou peu s'en faut)...

    On ne peut vraiment pas dire qu'il s'agisse là de modifications "aussi limitées que possibles", hein

    Bref, tu l'auras compris, la solution à base de transtypage n'est clairement pas une solution ni élégante, ni efficace

    L'autre solution qu'il nous reste, c'est celle à base de double dispatch.

    L'idée est que, au moment de l'exécution, l'objet qui se trouve à l'adresse mémoire connue au travers de m_femme sait très bien s'il est de type Fille, Fille1 ou Fille2, et de profiter de ce fait.

    Tout ce qu'il nous faut, c'est de disposer d'une fonction au niveau de la classe Mere (pour pouvoir l'appeler depuis m_femme), comme la fonction execute que j'avais pris dans mes interventions précédentes, et de faire en sorte que le comportement de cette fonction s'adapte en fonction du type réel (dynamique) de m_femme.

    Pour y arriver, nous allons donc déclarer la fonction execute() comme virtuelle dans la classe Mere (car c'est "tout ce qu'il nous faut" pour signaler au compilateur que le comportement de la fonction va être adapté en fonction du type réel (dynamique) )

    S'il se fait que la classe Mere n'a pas les informations suffisantes pour fournir le comportement que l'on attend de cette fonction (et il vaut mieux que ce soit le cas, sommes toutes), on peut déclarer cette fonction comme virtuelle pure, c'est à dire que c'est une fonction pour laquelle on signale au compilateur et à l'éditeur de liens qu'il ne doit pas s'inquiéter de ne pas en trouver d'implémentation.

    Mais, comme execute se trouve dans l'accessibilité publique de la classe Mere, le compilateur ne s'étonnera pas (au moment de la compilation donc) d'avoi un code du genre de m_femme->execute() car, pour lui, m_femme est... de type Mere

    Nous sommes donc arrivés au point où nous travaillons sur un objet que le compilateur connait comme étant de type Mere, mais dont l'objet lui-même sait à l'exécution s'il est de type Fille, Fille1 ou Fille2.

    Avoue que c'est déjà un grand pas, non

    Tout ce qu'il nous reste à faire, selon SRP dont je t'ai parlé plus haut, c'est d'arriver à déléguer la responsabilité du traitement à une autre fonction, qui prendra une référence (car c'est plus sécurisant), éventuellement constante (pour la même raison) sur l'objet du type réel qui va l'appeler.

    C'est pour cela que l'on parle de "double dispatch": on passe, grâce au principe du polymorphisme, d'un pointeur connu comme étant du type de base à un comportement spécifique au type réel de m_femme, et c'est ce comportement spécifique au type réel qui va appeler la fonction qui s'occupera du traitement, en sachant cette fois ci sur quel type réel il travaille

    L'idée est donc d'avoir au final un code 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
    /* je met tout dans un seul code ici, mais l'idéal serait d'avoir un fichier
     * d'en-tête et un fichier d'implémentation par classe + (au moins)un fichier 
     * d'en-tête et un fichier d'implémentation pour les fonctions ;)
     */
    /* Ceci s'appelle une déclaration anticipée, elle permettra au compilateur de 
     * ne pas s'étonner de voir apparaitre les type Fille, Fille1 et Fille2 dans les
     * différents  prototypes de calculeMoi ;)
     */ 
    class Fille;
    class Fille1;
    class Fille2;
    void calculeMoi(Fille const & f);
    void calculeMoi(Fille1 const & f);
    void calculeMoi(Fille2 const & f);
    class Mere
    {
        public:
            virtual void execute() const = 0;
        /* comme précédemment */
    };
    class Fille : public Mere
    {
        public:
            virtual void execute() const{calculeMoi(*this);} //deuxième dispatch
        /* comme précédemment */
    };
    class Fille1 : public Mere
    {
        public:
            virtual void execute() const{calculeMoi(*this);} //deuxième dispatch
        /* comme précédemment */
    };
    class Fille2 : public Mere
    {
        public:
            virtual void execute() const{calculeMoi(*this);} //deuxième dispatch
        /* comme précédemment */
    };
    void calculeMoi(Fille const & f)
    {
        /* ce qui doit etre fait pour un objet de type Fille */
    }
    void calculeMoi(Fille1 const & f)
    {
        /* ce qui doit etre fait pour un objet de type Fille1 */
    }
    void calculeMoi(Fille2 const & f)
    {
        /* ce qui doit etre fait pour un objet de type Fille2 */
    }
    class Main
    {
        public:
            Main(){/* comme précédemment */}
            void calcule() const {m_femme->execute();} // premier dispatch
        private:
            Mere* m_femme;
    };
    Je vais m'en arrêter là pour l'instant le temps de m'assurer que tu as compris le principe (et pour te donner l'occasion de poser des questions sur ces explications ) l'étape suivante (une fois que tu auras compris, hein ) sera de t'expliquer en quoi le patron de conception peut t'aider

    A noter que l'on pourrait tout à fait avoir des noms différents pour calculeMoi en fonction du fait que l'on travaille avec un objet de type Fille, Fille1 ou Fille2... mais ce n'est en définitive qu'un détail, et cela introduit déjà la notion qui va nous intéresser pour la suite
    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

  12. #12
    Membre confirmé Avatar de LinuxUser
    Inscrit en
    Avril 2007
    Messages
    857
    Détails du profil
    Informations forums :
    Inscription : Avril 2007
    Messages : 857
    Points : 616
    Points
    616
    Par défaut
    Je te remercie beaucoup pour le temps que tu me consacres et la peine que tu prends à bien rédiger.
    Mais, même si je comprends plus ou moins l'idée de ce que tu fait, je ne comprends absolument pas la rapport avec le problème que j'ai énoncé (à savoir comment accéder au méthode de la classe dérivée). Je suis vraiment désolé de te faire répéter plusieurs fois, je suis apparament stupide car je ne comprends pas.
    Pourrais-tu, s'il te plait illustrer en prenant de meilleurs noms que Mere, Fille, calculMoi ..., peut-être que ça irait mieux. Encore désolé de te faire répéter.

  13. #13
    Expert éminent sénior
    Avatar de koala01
    Homme Profil pro
    aucun
    Inscrit en
    Octobre 2004
    Messages
    11 614
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 52
    Localisation : Belgique

    Informations professionnelles :
    Activité : aucun

    Informations forums :
    Inscription : Octobre 2004
    Messages : 11 614
    Points : 30 626
    Points
    30 626
    Par défaut
    En fait, j'espérais que, me basant sur ton propre exemple, tu comprendrait...

    Nous allons donc reprendre dans l'ordre, et je vais te poser à plusieurs endroits une question qui sera toujours la même : ca va jusque là

    je te demanderai de me citer et de répondre à la question afin de pouvoir déterminer à quel endroit tu "perds les pédales" et donc savoir ce qu'il faut peut etre un peu creuser .

    En outre, n'hésites surtout pas à lire les commentaires que je mets dans mes exemples, ils sont pleins d'enseignements et de précieuses précisions

    Bon, d'abord, reprenons le principe de base qui nous occupe, à savoir l'héritage.

    L'héritage est la relation qui existe entre deux classes (mettons Base et Derivee) et qui représente une relation EST-UN.

    Ainsi, dans le code
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    class Base
    {
        public:
            virtual ~Base(); //obligatoire, mais bon, on verra cela plus tard
            void foo(); 
    };
    class Derivee : public Base 
    {
        public:
            void truc();
    };
    Derivee hérite de Base.

    Cela signifie que tout objet de type Derivee EST UN objet de type Base.

    Concrètement, cela veut dire que, si tu as une fonction du genre de
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    void doSomething(Base /* const */ & b )
    {
       /* on se fout de ce qu'elle fait */
    }
    tu peux parfaitement passer un objet de type Derivee, et que cela fonctionnera.

    C'est ce que l'on appelle la substitution, et c'est ce que le LSP (le principe de substitution de liskov (le L de SOLID dont je parlais la tantot) nous dit qu'il faut vérifer.

    On peut rajouter le fait que, comme Derivee hérite de Base, tout ce qui caractérise un objet de type Base caractérise également un objet de type Dérivée.

    Pour faire simple, toutes les fonctions qui sont déclarées public: dans Base sont accessible depuis une instance de Derivee.
    Ainsi, tu pourrais très bien écrire le code
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    int main()
    {
        Derivee d; // je sais que je dispose d'un objet de type Derivee
        d.truc(); // je peux donc appeler une fonction publique de Derivee
        // mais comme Derivee hérite de Base
        d.foo(); // je peux aussi faire appel au fonctions publiques de Base
    }
    Ca va jusque là

    Cela ne répond pas encore à ta question, mais il me semble important de re poser les bases

    Avec le principe de substitution, il y a un phénomène intéressant.

    Considérons trois classes dont tu sera d'accord avec moi que les instances de deux d'entre elles sont bel et bien des instances de la troisième :
    Forme, Rectangle et Cercle.

    Tu seras très certainement d'accord avec moi pour dire que, sémantiquement parlant, un rectangle est une forme, tout comme cercle en est une.

    En vertu du principe que j'ai expliqué la tout de suite, je devrais pouvoir fournir une instance de type Cercle ou de type Rectangle à toute fonction attendant une référence ou un pointeur de type Forme.

    Mais, intéressons nous un peu à la classe Forme...

    Je m'attends à pouvoir, par exemple, calculer la superficie ou le périmetre d'une forme car ce sont deux comportement que l'on peut observer pour "toute forme présente et à venir".

    Seulement, tu l'auras appris tout comme moi en primaire, on ne calcule ni le périmetre ni la superficie d'un rectangle de la même manière qu'on calcule celle d'un cercle. (et je ne parle même pas des autres formes que nous pourrions décider de créer par la suite )

    Pire: il n'est même pas possible de donner le moindre comportement cohérent pour ces deux calculs si on ne sait pas exactement si on a affaire à un rectangle ou à un cercle

    Pourtant, je compte bien sur le fait, si je substitue un cercle (ou un rectangle) dans une fonction où je crois utiliser une forme, que, si je demande le périmetre ou la superficie de cette forme, j'obtienne... cohérent avec le type que j'ai vraiment passé.

    Ainsi, je m'attend à ce qu'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
    double calculePerimetre(Forme const & f)
    {
        return f.perimetre(); // ca sert à rien, mais c'est pour montrer le principe :D
    }
    int main()
    {
       Rectangle rect(/*...*/);
       Cercle cer(/*...*/);
       std::cout<<"Le perimetre du cercle est de "<<calculePerimetre(cer)<<std::endl
                    <<"et celui du rectangle est de  "<<calculePerimetre(rect) <<std::endl;
        return 0;
    }
    me renvoie une valeur correcte aussi bien pour le périmetre de cer que pour celui de rect.

    Ce genre de comportement s'appelle le polymorphisme, que l'on pourrait définir comme le fait que
    quel que soit le type réel (Rectangle ou Cercle) d'une forme (selon mon exemple), le comportement de la fonction perimetre() doit s'adapter au type réel pour fournir un résultat cohérent
    Hé bien, il y a moyen d'arriver à un tel résultat.

    Et si jusqu'à présent je t'ai expliqué "ce que je veux", je vais enfin t'expliquer comment l'obtenir

    Le moyen d'obtenir ce genre de résultat est de déclarer (dans la classe Forme, vu que c'est un service que l'on est en droit d'attendre de "toute forme présente et à venir" les fonctions double perimetre() const et double superficie() const Et cela suffira pour expliquer au compilateur qu'il devra prendre un certain nombre de mesures pour s'assurer que, si on passe n'importe quel type de forme à la fonction calculePerimetre, ce soit bel et bien la logique qui s'applique au bon type de forme qui sera utilisée.

    Je ne vais pas entrer maintenant dans les détails, car cela ne ferait qu'embrouiller les choses, mais saches que le compilateur fera "ce qu'il faut"

    Cependant, comme je l'ai signalé plus haut, il faut garder en tete qu'il n'est pas possible de donner un comportement permettant de calculer la superficie ou le périmetre d'une forme si on ne sait pas quelle formule appliquer

    On va donc devoir indiquer au compilateur que cela ne sert à rien qu'il cherche l'implémentation pour Forme::perimetre() const ni pour Forme::superficie() const en les déclarant virtuelles pures.

    Pour être honnête, il y aurait deux trois trucs à dire sur les fonctions virtuelles pures, mais je ne vais pas en parler maintenant, cela ne ferait que compliquer les choses

    Cela se fait très simplement en rajoutant un =0 juste avant le ";" qui termine la déclaration de la fonction.

    Nous pourrons donc définir notre classe Forme sous une forme proche de
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    class Forme
    {
        public:
            virtual ~Forme(){} // obligatoire ;)
            virtual double perimetre() const = 0;
            virtual double surface() const = 0;
    };
    dont on fera hériter Rectangle et Cercle, pour lesquels nous définirons le comportement de perimetre et surface, 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
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    class Rectangle : public Forme
    {
        public:
            /* un rectangle a besoin de sa longeur et de sa largeur */
            Rectangle(double lon, double larg):long_(lon), larg_(larg){}
            /* le périmetre est égal à deux fois la somme de la longeur et de la largeur */
           virtual double perimetre() const{return (long_+larg_)*2;}
           /* et la superficie est égale à la multiplication des deux */
           virtual double surface() const{return long_* large;}
        private:
            double long_;
            double larg_;
    };
    class Cercle  : public Forme
    {
        public:
            /* ce qui nous intéresse pour le cercle, c'est son rayon */
            Cercle(double rayon):rayon_(rayon){}
            /* son périmetre est égal à 2 PI R */
            virtual double perimetre() const{return 2 * 3.1415926 * rayon_;}
            /* et sa superficie est égale à Pi * R * R */
            virtual double surface() const{return 3.1415926 * rayon_ * rayon_;}
        private:
            double rayon_;
    };
    et, grace à ce mécanisme, si je complete le code d'exemple de la tantot, sous 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
    double calculePerimetre(Forme const & f)
    {
        return f.perimetre(); // ca sert à rien, mais c'est pour montrer le principe :D
    }
    int main()
    {
       Rectangle rect(3.0,4.0);
       Cercle cer(2.0);
       std::cout<<"Le perimetre du cercle est de "<<calculePerimetre(cer)<<std::endl
                    <<"et celui du rectangle est de  "<<calculePerimetre(rect) <<std::endl;
        return 0;
    }
    nous obtiendrons bel et bien la sortie à laquelle nous nous attendons à savoir, quelque chose comme
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    Le perimetre du cercle est de 12.5662 (j'ai pas calculé plus loin :? )
    et celui du rectangle est de 24.0
    Ca va, jusque là

    Cela ne répond sans doute pas encore à ta question, mais, comme tu peux t'en rendre compte, on commence à s'en rapprocher.

    En effet, la fonction calculePerimetre est dans le même cas que la classe Main que tu donnais la tantot : elle travaille avec un objet du type qu'elle connait pour être du type de base, mais le résultat de la fonction qu'elle appelle s'adapte au type réel de l'argument qu'elle a reçu.

    Et tout cela, c'est grâce au fait que, même si la fonction a été "assez bête", au niveau de la compilation uniquement, pour perdre le fait que cerc est un Cercle et que rect est un Rectangle (selon mon exemple actuel), cette information continue d'exister lors de l'exécution.

    Ceci dit, tu auras sans doute remarqué que c'est une possibilité qui nous est offerte de travailler (au niveau des classes dérivées comme Cercle ou Rectangle) avec des données qui n'appartiennent qu'à la classe dérivée.

    La seule restriction (de rigueur conceptuelle) étant qu'il doit être possible de fournir un comportement qui reste valable "pour toute classe héritée de Forme, présente et à venir".

    Pour autant que cette restriction soit respectée, il est très largement préférable d'utiliser cette manière de travailler que d'essayer désespérément de récupérer un accesseur (ou pire, un mutateur) sur l'une des données de l'objet que tu manipules.

    Il existe en effet la loi dite "demeter" qui nous incite (je devrais dire "qui nous enjoint", vu que c'est une loi ) de faire en sorte que si un objet A utilise en interne un objet B qui utilise lui-même en interne un objet C, l'objet A ne devrait pas avoir à connaitre l'objet C pour être en mesure de manipuler l'objet B.

    Pour te faire comprendre le principe, mettons que tu aies une classe Voiture.

    Tu sais bien que ton objet de type Voiture dispose d'un moteur, d'un démarreur, d'une batterie et d'un réservoir à carburant.

    Tu sais tout aussi bien que chaque action que tu vas entreprendre sur la voiture va avoir un impact sur l'un de ces éléments, qu'il s'agisse faire le plein, de tourner le contact dans un sens ou dans l'autre, d'enfoncer la pédale d'accélérateur, ou d'allumer le phares, les clignotants ou encore les essuies glaces.

    Mais, à moins d'être bricoleur, garagiste ou en panne au milieu de nulle part, jamais tu n'ira chipoter par toi même au réservoir, à la batterie ou au moteur.

    Il est possible que tu aies quelques connaissances disparates sur ces différents éléments, comme le fait de savoir brancher des cables de batterie, de savoir où se trouve la trappe du réservoir voire le fait de savoir changer les bougies et faire la vidange du moteur, mais, si tu n'es pas garagiste, tu ne tenteras sans doute jamais de détacher le réservoir pour le remplacer ou de démonter entièrement le moteur.

    Comme tout bon bricoleur, tu sauras sans doute les grands principe du moteur, mais, encore une fois, il y a vraiment peu de chance que tu soies en mesure de démonter et de remonter intégralement le moteur sans avoir "des pièces en trop"

    Et bien, cette situation avec la voiture n'est, au final, qu'une mise en oeuvre de la loi demeter:

    Tu n'est pas obligé de savoir où se planque le réservoir sous le châssis, ou de savoir démonter le moteur pour le remonter, ni même de savoir changer une roue pour utiliser ta voiture : tu as "juste à savoir" que, si tu tournes la clé dans le bon sens, cela mettra le moteur en route, de savoir qu'en appuyant sur la bonne pédale, tu te mettras à rouler de plus en plus vite et qu'en versant de l'essence dans le bon orifice, ca te permettra d'aller plus loin

    En un mot, il te suffit de connaitre les moyens que tu as d'interagir avec la voiture pour que tu n'aies pas à t'inquiéter de détails tels que "mais que peut faire le moteur ou le réservoir pour moi " ou que "mais à quoi sert tel assemblages de pièces dans le moteur" pour pouvoir utiliser ta voiture en "bon père de famille"

    Mais revenons nos classes Forme, Rectangle et Cercle, en améliorant Rectangle et Cercle pour corser un peu les choses.

    Au niveau de Rectangle, il est intéressant de pouvoir récupérer la longueur et la largeur, et au niveau de Cercle, c'est le rayon qu'il est intéressant de pouvoir récupérer.

    Par contre, il n'y aurait aucun sens de vouloir donner un comportement permettant de récupérer l'une de ces trois données (ou l'une des données propres à "toute classe héritant de Forme, présente et à venir", d'ailleurs) dans Forme, car je te rappelle que toutes les fonctions publiques de Forme sont accessibles au départ d'un objet dont le type dériverait de Forme.

    Et comme il n'y a pas plus de sens d'essayer de récupérer la longueur ou la largeur d'un cercle qu'il n'y en a à vouloir récupérer le rayon d'un Rectangle, nous sommes bel et bien face à des fonctions qui ne doivent se trouver que dans les classes "qui leur vont bien"

    Nous pouvons donc modifier légèrement les classes Rectangle et Cercle de la tantot en
    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
    class Rectangle : public Forme
    {
        public:
            /* un rectangle a besoin de sa longeur et de sa largeur */
            Rectangle(double lon, double larg):long_(lon), larg_(larg){}
            /* le périmetre est égal à deux fois la somme de la longeur et de la largeur */
           virtual double perimetre() const{return (long_+larg_)*2;}
           /* et la superficie est égale à la multiplication des deux */
           virtual double surface() const{return long_* large;}
           double longueur() const{return long_;}
           double largeur() const{return larg_;}
        private:
            double long_;
            double larg_;
    };
    class Cercle  : public Forme
    {
        public:
            /* ce qui nous intéresse pour le cercle, c'est son rayon */
            Cercle(double rayon):rayon_(rayon){}
            /* son périmetre est égal à 2 PI R */
            virtual double perimetre() const{return 2 * 3.1415926 * rayon_;}
            /* et sa superficie est égale à Pi * R * R */
            virtual double surface() const{return 3.1415926 * rayon_ * rayon_;}
            double rayon() const{return rayon_;}
        private:
            double rayon_;
    };
    Et nous pourrions même corser un peu les choses en faisant en sorte que ce soit à l'utilisateur de choisir s'il veut créer un rectangle ou un cercle, sous 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
    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
    Rectangle * creerRectangle()
    {
        double longueur;
        double largueur;
        std::cout<<"introduisez la longueur :";
        std::cin>>longueur;
        std::cout<<"introduisez maintenant la largeur :";
        cin>>largeur;
        return new Rectangle(longueur, largeur);
    }
    Cercle * creerCercle()
    {
        double rayon;
        std::cout<<"introduisez le rayon";
        std::cin >> rayon;
        return new Cercle(rayon);
    }
    int afficheChoix()
    {
        int choix;
        std::cout<< "veuillez choisir parmi les option suivantes :"<<std::endl
                 << "1- créer un rectangle "<<std::endl
                 << "2- créer un cercle"<<std::endl;
         std::cin>> choix;
         return choix;
    }
    /* On ne sait pas si on obtiendra un Cercle ou un rectangle...
     * on sait juste que nous obtiendrons une forme :P
     */
    Forme * DemandeQuoiEtCree()
    {
        int choix = afficheChoix();
        if(choix == 1 )
            return creerRectangle();
        return creerCercle();
    }
    int main()
    {
        /* ici non plus, on ne sait pas si nous aurons un cercle ou un rectangle
         */
        Forme * ptr = DenandeQuoiEtCree();
        /* et pourtant, ca fonctionne comme on peut s'y attendre en fonction
         * des réponses données (je n'ai pas changé la fonction calculePerimetre ;)
         */
        std::cout<< calculePerimetre(*ptr);
        delete ptr;
        return 0;
    }
    Le principe est le même que celui que j'ai expliqué la tout de suite, simplement, j'ai décidé (en fait, j'ai du) de travailler avec parce que je ne sais pas, au moment de déclarer ptr, le type d'objet que je vais récupérer à l'exécution.

    Même s'il est possible (et d'ailleurs préférable) de pousser le respect du SRP encore plus loin, tu n'auras pas manqué de remarqué que j'ai découpé mon code de manière à ce que chaque fonction ne soit responsable que d'un chose...

    De cette manière, si "quelque chose" devait ne pas se passer correctement, j'aurais assez facile, en fonction de l'endroit où ca "clocherait", de retrouver et de résoudre le problème

    Et cela fonctionnerait aussi si c'était dans une classe, dont calculerPerimetre pourrait d'ailleurs etre membre, 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
    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
    class UtilisateurDeFormes 
    {
        public:
            void allonsY()
            {
                DenandeQuoiEtCree();
                /* et pourtant, ca fonctionne comme on peut s'y attendre en fonction
                 * des réponses données (je n'ai pas changé la fonction calculePerimetre ;)
                 */
                std::cout<< calculePerimetre();
                delete m_ptr;
            }
        private: 
            /* Ces fonctions peuvent être privées, car "à usage interne" à la classe uniquement ;) */
            void creerRectangle()
            {
                double longueur;
                double largueur;
                std::cout<<"introduisez la longueur :";
                std::cin>>longueur;
                std::cout<<"introduisez maintenant la largeur :";
                cin>>largeur;
                m_ptr =new Rectangle(longueur, largeur);
            }
            void creerCercle()
            {
                double rayon;
                std::cout<<"introduisez le rayon";
                std::cin >> rayon;
                m_ptr = new Cercle(rayon);
            }
            int afficheChoix()
            {
                int choix;
                std::cout<< "veuillez choisir parmi les option suivantes :"<<std::endl
                         << "1- créer un rectangle "<<std::endl
                         << "2- créer un cercle"<<std::endl;
                 std::cin>> choix;
                 return choix;
            }
            void DemandeQuoiEtCree()
            {
                int choix = afficheChoix();
                if(choix == 1 )
                   creerRectangle();
                creerCercle();
            }
            double calculePerimetre() const
            {
                return m_ptr->perimetre();
           }
            /* je sais que j'aurais besoin d'une forme, dont j'ignore encore tout :? */
           Forme * m_ptr;
    };
    et je pourrais limiter ma fonction main à quelque chose comme
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    int main()
    {
        UtilisateurDeFormes travail;
        travail.allonsY();
        return 0;
    }
    Ca va toujours tu arrives à suivre

    AAAhhh, on approche du problème réel...

    En effet, ma classe UtilisateurDeFormes est finalement très proche de la classe Main que tu donnais dans ton exemple

    Il n'y a plus qu'à te montrer comment accéder au contenu spécifique de Rectangle et de Cercle (et de toute classe dérivée de Forme, présente et à venir).

    L'une des solutions possibles, c'est d'utiliser le transtypage, et pourquoi pas une bonne dose de RTTI pour y arriver.

    Mais je te répète que ce n'est pas ma solutions préférée. Reportes toi à mon intervention précédente pour les raisons qui me font dire cela

    Nous allons, pour la facilité de compréhension, rajouter une fonction "utiliseCercle(Cercle const &)" et une autre "utiliserRectangle(Rectangle const &)" à notre classe UtilisateurDeFormes et modifier un tout petit peu allonsY pour appeler soit l'une soit l'autre en fonction du type réel de ptr_.

    Tu auras compris que utiliserCercle est prévue pour ne manipuler que des cercles, alors que utiliserRectangle est prévue pour ne manipuler que... des rectangles (l'inverse aurait sans doute été choquant )

    Je ne vais pas recopier toute la classe UtilisateurDeFormes car je crains de m'approcher dangereusement de la limite des 65 000 caractères , mais, à part les deux fonctions que je rajoute et la fonction allonsY que je modifie, elle ne change pas du tout

    Et donc, cela ressemblerait à quelque chose comme
    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
    class UtilisateurDeFormes 
    {
        public:
            void allonsY()
            {
                DenandeQuoiEtCree();
                /* pour ne pas apporter trop de modifications, j'utilise dynamic_cast */
                /* je sais qu'un des deux pointeur sera valable et que l'autre
                 * pointera sur NULL si j'utilise dynamic_cast...
                 */
                Cercle * cercle = dynamic_cast<Cercle * >(m_ptr);
                Rectangle * rectangle = dynamic_cast<Rectangle*>(m_ptr);
     
                 * il faut juste que je teste lequel ne vaut pas NULL pour décider
                 * de la fonction que je dois appeler
                 */
                 if(cercle)
                     utiliserCercle(*cercle);
                 else if(rectangle)
                     utiliserRectangle(*rectangle);
                 else // et si ce n'est ni l'un ni l'autre, y a un os :aie:
                     throw QueSePasseTIl();
                delete m_ptr;
            }
        private: 
            /* Ces fonctions peuvent être privées, car "à usage interne" à la classe uniquement ;) */
            void utiliserCercle (Cercle const & c) const
            {
                std::cout<<"Nous avons un cercle de "<<c.rayon()<<" de rayon"<<std::endl;
            }
            void utiliserRectangle (Rectangle const & c) const
            {
                std::cout<<"Nous avons un rectangle de "<<c.longueur()<<" de longueur "<<std::endl
                       <<"et de "<<c.largeur()<<" de largeur"<<std::endl;
            }
    };
    Ca va toujours

    Mais bon, je le répète encore une fois, cette solution n'est ni la plus élégante ni la plus efficace, surtout si tu dois utiliser dynamic_cast, car il doit faire une vérification du type réel avant de renvoyer un pointeur du bon type (ou non).

    La meilleure solution, c'est de faire confiance à ptr_ pour savoir, durant l'exécution, de quel type il est réellement, même s'il est connu à la compilation comme étant un pointeur sur Forme, et de recourir au double dispatch.

    Mais, pour cela, nous devons modifier un tout petit peu les classes Forme, Cercle et Rectangle.

    Nous avons en effet besoin d'une fonction qui sera disponible dans la classe Forme et dont le comportement s'adaptera en fonction que nous ayons affaire à un Cercle ou à un Rectangle. Ce sera donc une fonction virtuelle

    Comme je suis inspiré, j'ai décidé de nommer justement cette fonction travailleAvecMoi()

    Evidemment, il ne sert pas à grand chose d'implémenter cette fonction pour la classe Forme, car nous aurions tout aussi bien pu nous contenter d'utiliser du polymorphisme comme nous l'avons fait jusqu'ici...

    L'idée est de pouvoir, d'une manière ou d'une autre, arriver à travailler soit avec une référence sur un objet de type Cercle pour pouvoir utiliser sa fonction rayon, soit avec une référence sur un objet de type Rectangle pour pouvoir utiliser ses fonctions longueur et largeur

    La fonction travailleAvecMoi() sera donc virtuelle pure dans Forme, pour lui indiquer qu'il ne doit pas chercher apres l'implémentation de cette fonction pour forme

    Nous aurions donc 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
    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
    class Forme
    {
        public:
            virtual ~Forme(){} // obligatoire ;)
            virtual double perimetre() const = 0;
            virtual double surface() const = 0;
            virtual void travailleAvecMoi() const = 0;
    };
    class Rectangle : public Forme
    {
        public:
            /* un rectangle a besoin de sa longeur et de sa largeur */
            Rectangle(double lon, double larg):long_(lon), larg_(larg){}
            /* le périmetre est égal à deux fois la somme de la longeur et de la largeur */
           virtual double perimetre() const{return (long_+larg_)*2;}
           /* et la superficie est égale à la multiplication des deux */
           virtual double surface() const{return long_* large;}
            virtual void travailleAvecMoi() const
            {
                /* ici, on sait que this est un pointeur sur un rectangle...
                 * on peut donc appeler n'importe quelle fonction qui demande un
                 * argument sous la forme d'un pointeur ou d'une référence sur un 
                 * objet de type Rectangle en étant sur qu'elle sera correctement 
                 * appelée, même s'il y a une surcharge quelque par de cette 
                 * fonction qui attend un pointeur ou une référence sur un objet
                 * de type Cercle
                 */
               travaille(*this);
     
            }
        private:
            double long_;
            double larg_;
    };
    class Cercle  : public Forme
    {
        public:
            /* ce qui nous intéresse pour le cercle, c'est son rayon */
            Cercle(double rayon):rayon_(rayon){}
            /* son périmetre est égal à 2 PI R */
            virtual double perimetre() const{return 2 * 3.1415926 * rayon_;}
            /* et sa superficie est égale à Pi * R * R */
            virtual double surface() const{return 3.1415926 * rayon_ * rayon_;}
     
            virtual void travailleAvecMoi() const
            {
                /* ici, on sait que this est un pointeur sur un cercle...
                 * on peut donc appeler n'importe quelle fonction qui demande un
                 * argument sous la forme d'un pointeur ou d'une référence sur un 
                 * objet de type Cercle en étant sur qu'elle sera correctement 
                 * appelée, même s'il y a une surcharge quelque par de cette 
                 * fonction qui attend un pointeur ou une référence sur un objet
                 * de type Rectangle
                 */
               travaille(*this);
     
            }
        private:
            double rayon_;
    };
    Comme je l'ai expliqué dans une intervention précédente, C++ nous autorise à avoir autant de fois que l'on veut (ou peu s'en faut) des fonctions dont la seule chose qui change, c'est le nombre ou le type de leur paramètres.

    ici, j'ai décidé de travailler avec des fonctions libres (AKA qui ne font partie d'aucune classe) nommées travaille dont une version prend une référence sur un objet de type Cercle et dont l'autre prend une référence sur un objet de type Rectangle.

    Ils ont beau avoir une classe de base commune, ce sont malgré tout des types différents

    Et, comme je l'ai mis dans les commentaires, comme nous savons bien le type de this, il n'y aura aucun problème, ni à la compilation, ni à l'exécution.

    Les prototypes de fonctions seront proches de
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    void travaille(Rectangle const &);
    void travaille(Cercle const &);
    et leur implémentation pourrait etre proche de
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
            void travaille(Rectangle const & c)
            {
                std::cout<<"Nous avons un rectangle de "<<c.longueur()<<" de longueur "<<std::endl
                       <<"et de "<<c.largeur()<<" de largeur"<<std::endl;
            }
            void travaille(Cercle const & c)
            {
                std::cout<<"Nous avons un cercle de "<<c.rayon()<<" de rayon"<<std::endl;
            }
    Et la fonction allonY de notre classe UtilisateurDeFormes ressemblerait à
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    class UtilisateurDeFormes 
    {
        public:
            void allonsY()
            {
                DenandeQuoiEtCree();
                m_ptr->travailleAvecMoi();
                delete m_ptr;
            }
    };
    Le plus beau de l'histoire, c'est que la classe UtilisateurDeFormes pourrait se contenter de connaitre le type Forme, sans avoir à s'inquiéter des classes qui en héritent, "présentes et à venir" si elle ne devait pas prendre la responsabilité de leur création

    Quand je te dis qu'il est important de respecter SRP, je sais de quoi je parle

    Ca va toujours, tu commences à percuter sur le principe

    Mais, j'y pense tout à coup... (en fait, non cela fait un sérieux bout de temps que j'y pense ) : la fonction membre travailleAvecMoi que je viens d'ajouter à mes classes Forme, Cercle et Rectangle...

    Il n'y a strictement rien qui l'empêche d'accepter des paramètres, non

    Et si...

    Et si je passais en paramètre une référence sur une classe qui expose une fonction publique (mettons travaille) sous la forme de deux versions différentes, l'une acceptant une référence constante sur un objet de type Rectangle et l'autre acceptant une référence constante sur un objet de type Cercle...

    Je pourrais parfaitement appeler cette fonction travaille aussi bien depuis la version de travailleAvecMoi qui s'applique aux cercles qu'à celle qui s'applique aux rectangle... !!!!

    Cela me donnerait donc 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
    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
    class Forme
    {
        public:
            virtual ~Forme(){} // obligatoire ;)
            virtual double perimetre() const = 0;
            virtual double surface() const = 0;
            virtual void travailleAvecMoi(Visiteur const & v) const = 0;
    };
    class Rectangle : public Forme
    {
        public:
            /* un rectangle a besoin de sa longeur et de sa largeur */
            Rectangle(double lon, double larg):long_(lon), larg_(larg){}
            /* le périmetre est égal à deux fois la somme de la longeur et de la largeur */
           virtual double perimetre() const{return (long_+larg_)*2;}
           /* et la superficie est égale à la multiplication des deux */
           virtual double surface() const{return long_* large;}
            virtual void travailleAvecMoi(Visiteur const & v) const
            {
                /* ici, on sait que this est un pointeur sur un rectangle...
                 * on peut donc appeler n'importe quelle fonction qui demande un
                 * argument sous la forme d'un pointeur ou d'une référence sur un 
                 * objet de type Rectangle en étant sur qu'elle sera correctement 
                 * appelée, même s'il y a une surcharge quelque par de cette 
                 * fonction qui attend un pointeur ou une référence sur un objet
                 * de type Cercle
                 */
               v.travaille(*this);
     
            }
        private:
            double long_;
            double larg_;
    };
    class Cercle  : public Forme
    {
        public:
            /* ce qui nous intéresse pour le cercle, c'est son rayon */
            Cercle(double rayon):rayon_(rayon){}
            /* son périmetre est égal à 2 PI R */
            virtual double perimetre() const{return 2 * 3.1415926 * rayon_;}
            /* et sa superficie est égale à Pi * R * R */
            virtual double surface() const{return 3.1415926 * rayon_ * rayon_;}
     
            virtual void travailleAvecMoi(Visiteur const & v) const
            {
                /* ici, on sait que this est un pointeur sur un cercle...
                 * on peut donc appeler n'importe quelle fonction qui demande un
                 * argument sous la forme d'un pointeur ou d'une référence sur un 
                 * objet de type Cercle en étant sur qu'elle sera correctement 
                 * appelée, même s'il y a une surcharge quelque par de cette 
                 * fonction qui attend un pointeur ou une référence sur un objet
                 * de type Rectangle
                 */
               v.travaille(*this);
     
            }
        private:
            double rayon_;
    };
    Hummm...

    Et puis, d'ailleurs, je pourrais profiter du polymorphisme sur cet argument, étant donné qu'il serait transmis sous la forme d'une référence... !!!

    Donc, chaque fois que j'aurai besoin de travailler d'une manière particulière pour un cercle ou pour un rectangle, il me suffit de créer une classe qui hérite de Visiteur et qui fait "ce dont j'ai besoin" dans les deux versions de travaille...

    Et, si en plus de retirer la responsabilité de la création des cercles et des rectangle à ma classe UtilisateurDeFormes, je m'arrange pour qu'il transmette "à quelque chose" les informations concernant le visiteur dont il a besoin pour obtenir un pointeur de type Visiteur, la classe UtilisateurDeFormes a juste à connaitre trois choses : Ce qui va lui créer un objet de type Visiteur, ce qui va lui créer un objet de type Forme, et le type Forme de manière à pouvoir appeler sa fonction travailleAvecMoi... Et encore:

    Comme elle ne manipulera pas directement l'objet visiteur, la classe UtilisateurDeFormes pourra juste se contenter d'une déclaration anticipée de Visiteur.

    Quant aux classes dérivées de la classe Forme, elle ne devront de toutes manière connaitre que... la classe Visiteur pour l'utiliser, sans s'inquiéter de leur type réel

    Et voilà les bases posées pour parler du patron de conception "visiteur" et du patron de conception "fabrique", mais tout cela, c'est une autre histoire
    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

  14. #14
    Expert éminent sénior

    Femme Profil pro
    Ingénieur développement logiciels
    Inscrit en
    Juin 2007
    Messages
    5 189
    Détails du profil
    Informations personnelles :
    Sexe : Femme
    Localisation : France, Essonne (Île de France)

    Informations professionnelles :
    Activité : Ingénieur développement logiciels

    Informations forums :
    Inscription : Juin 2007
    Messages : 5 189
    Points : 17 141
    Points
    17 141
    Par défaut
    koala01, quand le problème sera résolu, récupère tout ca, et fais-en un cours!
    Mes principes de bases du codeur qui veut pouvoir dormir:
    • Une variable de moins est une source d'erreur en moins.
    • Un pointeur de moins est une montagne d'erreurs en moins.
    • Un copier-coller, ça doit se justifier... Deux, c'est un de trop.
    • jamais signifie "sauf si j'ai passé trois jours à prouver que je peux".
    • La plus sotte des questions est celle qu'on ne pose pas.
    Pour faire des graphes, essayez yEd.
    le ter nel est le titre porté par un de mes personnages de jeu de rôle

  15. #15
    Membre confirmé Avatar de LinuxUser
    Inscrit en
    Avril 2007
    Messages
    857
    Détails du profil
    Informations forums :
    Inscription : Avril 2007
    Messages : 857
    Points : 616
    Points
    616
    Par défaut
    Citation Envoyé par koala01 Voir le message
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    class Base
    {
        public:
            virtual ~Base(); //obligatoire, mais bon, on verra cela plus tard
            void foo(); 
    };
    class Derivee : public Base 
    {
        public:
            void truc();
    };
    Derivee hérite de Base.

    Cela signifie que tout objet de type Derivee EST UN objet de type Base.

    Concrètement, cela veut dire que, si tu as une fonction du genre de
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    void doSomething(Base /* const */ & b )
    {
       /* on se fout de ce qu'elle fait */
    }
    tu peux parfaitement passer un objet de type Derivee, et que cela fonctionnera.

    C'est ce que l'on appelle la substitution, et c'est ce que le LSP (le principe de substitution de liskov (le L de SOLID dont je parlais la tantot) nous dit qu'il faut vérifer.

    On peut rajouter le fait que, comme Derivee hérite de Base, tout ce qui caractérise un objet de type Base caractérise également un objet de type Dérivée.

    Pour faire simple, toutes les fonctions qui sont déclarées public: dans Base sont accessible depuis une instance de Derivee.
    Ainsi, tu pourrais très bien écrire le code
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    int main()
    {
        Derivee d; // je sais que je dispose d'un objet de type Derivee
        d.truc(); // je peux donc appeler une fonction publique de Derivee
        // mais comme Derivee hérite de Base
        d.foo(); // je peux aussi faire appel au fonctions publiques de Base
    }
    Ca va jusque là
    Jusqu'ici tout va bas, sauf à un détail près, tu définis souvent des fonctions (libres de plus) prenant en argument une référence du type de base:
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    void doSomething(Base /* const */ & b )
    {
       /* on se fout de ce qu'elle fait */
    }
    Même si je comrpends que cela puisse fonctionner, je pense que c'est là que l'on diverge, je cherche à utiliser l'objet définit du type de base (qui est un membre de la classe qui l'utilise) directement dans les autres méthodes de la classe (qui l'utilise) en choisissant de l'utiliser en temps qu'une de ses classes dérivées:
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    class UtilisateurDesAutresClasses
    {
      void calcul(); // sans prendre de référence à Base& ou Derivee&
    private:
      int m_resultat;
      Base* m_membre;}
    Dans le .cpp
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    UtilisateurDesAutresClasses::UtilisateurDesAutresClasses()
    {
      m_membre = new Derivee();
    }
    void calcul()
    {
      m_resultat = m_membre->fontionDeLaClasseDerivee() + .... ;
    }
    Je crois comprendre, que ce que je veux faire n'est pas possible.

    Citation Envoyé par koala01 Voir le message
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    double calculePerimetre(Forme const & f)
    {
        return f.perimetre(); // ca sert à rien, mais c'est pour montrer le principe :D
    }
    int main()
    {
       Rectangle rect(/*...*/);
       Cercle cer(/*...*/);
       std::cout<<"Le perimetre du cercle est de "<<calculePerimetre(cer)<<std::endl
                    <<"et celui du rectangle est de  "<<calculePerimetre(rect) <<std::endl;
        return 0;
    }
    me renvoie une valeur correcte aussi bien pour le périmetre de cer que pour celui de rect.

    Ce genre de comportement s'appelle le polymorphisme, que l'on pourrait définir comme le fait que Hé bien, il y a moyen d'arriver à un tel résultat.

    Et si jusqu'à présent je t'ai expliqué "ce que je veux", je vais enfin t'expliquer comment l'obtenir

    Le moyen d'obtenir ce genre de résultat est de déclarer (dans la classe Forme, vu que c'est un service que l'on est en droit d'attendre de "toute forme présente et à venir" les fonctions double perimetre() const et double superficie() const Et cela suffira pour expliquer au compilateur qu'il devra prendre un certain nombre de mesures pour s'assurer que, si on passe n'importe quel type de forme à la fonction calculePerimetre, ce soit bel et bien la logique qui s'applique au bon type de forme qui sera utilisée.

    Je ne vais pas entrer maintenant dans les détails, car cela ne ferait qu'embrouiller les choses, mais saches que le compilateur fera "ce qu'il faut"

    Cependant, comme je l'ai signalé plus haut, il faut garder en tete qu'il n'est pas possible de donner un comportement permettant de calculer la superficie ou le périmetre d'une forme si on ne sait pas quelle formule appliquer

    On va donc devoir indiquer au compilateur que cela ne sert à rien qu'il cherche l'implémentation pour Forme::perimetre() const ni pour Forme::superficie() const en les déclarant virtuelles pures.

    Pour être honnête, il y aurait deux trois trucs à dire sur les fonctions virtuelles pures, mais je ne vais pas en parler maintenant, cela ne ferait que compliquer les choses

    Cela se fait très simplement en rajoutant un =0 juste avant le ";" qui termine la déclaration de la fonction.

    Nous pourrons donc définir notre classe Forme sous une forme proche de
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    class Forme
    {
        public:
            virtual ~Forme(){} // obligatoire ;)
            virtual double perimetre() const = 0;
            virtual double surface() const = 0;
    };
    dont on fera hériter Rectangle et Cercle, pour lesquels nous définirons le comportement de perimetre et surface, 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
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    class Rectangle : public Forme
    {
        public:
            /* un rectangle a besoin de sa longeur et de sa largeur */
            Rectangle(double lon, double larg):long_(lon), larg_(larg){}
            /* le périmetre est égal à deux fois la somme de la longeur et de la largeur */
           virtual double perimetre() const{return (long_+larg_)*2;}
           /* et la superficie est égale à la multiplication des deux */
           virtual double surface() const{return long_* large;}
        private:
            double long_;
            double larg_;
    };
    class Cercle  : public Forme
    {
        public:
            /* ce qui nous intéresse pour le cercle, c'est son rayon */
            Cercle(double rayon):rayon_(rayon){}
            /* son périmetre est égal à 2 PI R */
            virtual double perimetre() const{return 2 * 3.1415926 * rayon_;}
            /* et sa superficie est égale à Pi * R * R */
            virtual double surface() const{return 3.1415926 * rayon_ * rayon_;}
        private:
            double rayon_;
    };
    et, grace à ce mécanisme, si je complete le code d'exemple de la tantot, sous 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
    double calculePerimetre(Forme const & f)
    {
        return f.perimetre(); // ca sert à rien, mais c'est pour montrer le principe :D
    }
    int main()
    {
       Rectangle rect(3.0,4.0);
       Cercle cer(2.0);
       std::cout<<"Le perimetre du cercle est de "<<calculePerimetre(cer)<<std::endl
                    <<"et celui du rectangle est de  "<<calculePerimetre(rect) <<std::endl;
        return 0;
    }
    nous obtiendrons bel et bien la sortie à laquelle nous nous attendons à savoir, quelque chose comme
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    Le perimetre du cercle est de 12.5662 (j'ai pas calculé plus loin :? )
    et celui du rectangle est de 24.0
    Ca va, jusque là
    Oui je comprends très bien tout cela, mais là encore pour utiliser le polymorphisme tu utilises une fonction libre et tu prends une référence de la classe de base en paramètre pour tes appels polymorphiques.


    Citation Envoyé par koala01 Voir le message
    Il existe en effet la loi dite "demeter" qui nous incite (je devrais dire "qui nous enjoint", vu que c'est une loi ) de faire en sorte que si un objet A utilise en interne un objet B qui utilise lui-même en interne un objet C, l'objet A ne devrait pas avoir à connaitre l'objet C pour être en mesure de manipuler l'objet B.
    Je ne savais pas qu'une telle loi existait (en plus Demeter, référence mythologique, la classe, j'adore), mais de moi-même j'essaye toujours de respecter ce principe/

    Citation Envoyé par koala01 Voir le message
    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
    class Rectangle : public Forme
    {
        public:
            /* un rectangle a besoin de sa longeur et de sa largeur */
            Rectangle(double lon, double larg):long_(lon), larg_(larg){}
            /* le périmetre est égal à deux fois la somme de la longeur et de la largeur */
           virtual double perimetre() const{return (long_+larg_)*2;}
           /* et la superficie est égale à la multiplication des deux */
           virtual double surface() const{return long_* large;}
           double longueur() const{return long_;}
           double largeur() const{return larg_;}
        private:
            double long_;
            double larg_;
    };
    class Cercle  : public Forme
    {
        public:
            /* ce qui nous intéresse pour le cercle, c'est son rayon */
            Cercle(double rayon):rayon_(rayon){}
            /* son périmetre est égal à 2 PI R */
            virtual double perimetre() const{return 2 * 3.1415926 * rayon_;}
            /* et sa superficie est égale à Pi * R * R */
            virtual double surface() const{return 3.1415926 * rayon_ * rayon_;}
            double rayon() const{return rayon_;}
        private:
            double rayon_;
    };
    Et nous pourrions même corser un peu les choses en faisant en sorte que ce soit à l'utilisateur de choisir s'il veut créer un rectangle ou un cercle, sous 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
    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
    Rectangle * creerRectangle()
    {
        double longueur;
        double largueur;
        std::cout<<"introduisez la longueur :";
        std::cin>>longueur;
        std::cout<<"introduisez maintenant la largeur :";
        cin>>largeur;
        return new Rectangle(longueur, largeur);
    }
    Cercle * creerCercle()
    {
        double rayon;
        std::cout<<"introduisez le rayon";
        std::cin >> rayon;
        return new Cercle(rayon);
    }
    int afficheChoix()
    {
        int choix;
        std::cout<< "veuillez choisir parmi les option suivantes :"<<std::endl
                 << "1- créer un rectangle "<<std::endl
                 << "2- créer un cercle"<<std::endl;
         std::cin>> choix;
         return choix;
    }
    /* On ne sait pas si on obtiendra un Cercle ou un rectangle...
     * on sait juste que nous obtiendrons une forme :P
     */
    Forme * DemandeQuoiEtCree()
    {
        int choix = afficheChoix();
        if(choix == 1 )
            return creerRectangle();
        return creerCercle();
    }
    int main()
    {
        /* ici non plus, on ne sait pas si nous aurons un cercle ou un rectangle
         */
        Forme * ptr = DenandeQuoiEtCree();
        /* et pourtant, ca fonctionne comme on peut s'y attendre en fonction
         * des réponses données (je n'ai pas changé la fonction calculePerimetre ;)
         */
        std::cout<< calculePerimetre(*ptr);
        delete ptr;
        return 0;
    }
    Le principe est le même que celui que j'ai expliqué la tout de suite, simplement, j'ai décidé (en fait, j'ai du) de travailler avec parce que je ne sais pas, au moment de déclarer ptr, le type d'objet que je vais récupérer à l'exécution.

    Même s'il est possible (et d'ailleurs préférable) de pousser le respect du SRP encore plus loin, tu n'auras pas manqué de remarqué que j'ai découpé mon code de manière à ce que chaque fonction ne soit responsable que d'un chose...

    De cette manière, si "quelque chose" devait ne pas se passer correctement, j'aurais assez facile, en fonction de l'endroit où ca "clocherait", de retrouver et de résoudre le problème

    Et cela fonctionnerait aussi si c'était dans une classe, dont calculerPerimetre pourrait d'ailleurs etre membre, 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
    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
    class UtilisateurDeFormes 
    {
        public:
            void allonsY()
            {
                ptr_ = DenandeQuoiEtCree();
                /* et pourtant, ca fonctionne comme on peut s'y attendre en fonction
                 * des réponses données (je n'ai pas changé la fonction calculePerimetre ;)
                 */
                std::cout<< calculePerimetre();// devrait y avoir une référence en argument ?
                delete ptr_;
            }
        private: 
            /* Ces fonctions peuvent être privées, car "à usage interne" à la classe uniquement ;) */
            Rectangle * creerRectangle()
            {
                double longueur;
                double largueur;
                std::cout<<"introduisez la longueur :";
                std::cin>>longueur;
                std::cout<<"introduisez maintenant la largeur :";
                cin>>largeur;
                return new Rectangle(longueur, largeur);
            }
            Cercle * creerCercle()
            {
                double rayon;
                std::cout<<"introduisez le rayon";
                std::cin >> rayon;
                return new Cercle(rayon);
            }
            int afficheChoix()
            {
                int choix;
                std::cout<< "veuillez choisir parmi les option suivantes :"<<std::endl
                         << "1- créer un rectangle "<<std::endl
                         << "2- créer un cercle"<<std::endl;
                 std::cin>> choix;
                 return choix;
            }
            /* On ne sait pas si on obtiendra un Cercle ou un rectangle...
             * on sait juste que nous obtiendrons une forme :P
             */
            Forme * DemandeQuoiEtCree()
            {
                int choix = afficheChoix();
                if(choix == 1 )
                    return creerRectangle();
                return creerCercle();
            }
            double calculePerimetre() const
            {
                return ptr->perimetre();
           }
            /* je sais que j'aurais besoin d'une forme, dont j'ignore encore tout :? */
           Forme * ptr_;
    };
    et je pourrais limiter ma fonction main à quelque chose comme
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    int main()
    {
        UtilisateurDeFormes travail;
        travail.allonsY();
        return 0;
    }
    Oui je comprends ce que tu fais, mais là encore tu manipules les objets avec des pointeurs, j'aurais aimé le même exemple avec un membre de type Forme dans la classe UtilisateurDeFomres.
    Et il me semble qu'il manque un argument quand tu appelles calculPerimetre dans allonsY().


    Citation Envoyé par koala01 Voir le message
    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
    class UtilisateurDeFormes 
    {
        public:
            void allonsY()
            {
                ptr_ = DenandeQuoiEtCree();
                /* pour ne pas apporter trop de modifications, j'utilise dynamic_cast */
                /* je sais qu'un des deux pointeur sera valable et que l'autre
                 * pointera sur NULL si j'utilise dynamic_cast...
                 */
                Cercle * cercle = dynamic_cast<Cercle * >(ptr_);
                Rectangle * rectangle = dynamic_cast<Rectangle*>(ptr);
     
                 * il faut juste que je teste lequel ne vaut pas NULL pour décider
                 * de la fonction que je dois appeler
                 */
                 if(cercle)
                     utiliserCercle(*cercle);
                 else if(rectangle)
                     utiliserRectangle(*rectangle);
                 else // et si ce n'est ni l'un ni l'autre, y a un os :aie:
                     throw QueSePasseTIl();
                delete ptr_;
            }
        private: 
            /* Ces fonctions peuvent être privées, car "à usage interne" à la classe uniquement ;) */
            void utiliserCercle (Cercle const & c) const
            {
                std::cout<<"Nous avons un cercle de "<<c.rayon()<<" de rayon"<<std::endl;
            }
            void utiliserRectangle (Rectangle const & c) const
            {
                std::cout<<"Nous avons un rectangle de "<<c.longueur()<<" de longueur "<<std::endl
                       <<"et de "<<c.largeur()<<" de largeur"<<std::endl;
            }
    };
    Oui mais cela voudrait dire que je devrai à chaque fois faire un cast (et un test de type) pour utiliser le bon objet.

    Citation Envoyé par koala01 Voir le message
    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
    class Forme
    {
        public:
            virtual ~Forme(){} // obligatoire ;)
            virtual double perimetre() const = 0;
            virtual double surface() const = 0;
            virtual void travailleAvecMoi() const = 0;
    };
    class Rectangle : public Forme
    {
        public:
            /* un rectangle a besoin de sa longeur et de sa largeur */
            Rectangle(double lon, double larg):long_(lon), larg_(larg){}
            /* le périmetre est égal à deux fois la somme de la longeur et de la largeur */
           virtual double perimetre() const{return (long_+larg_)*2;}
           /* et la superficie est égale à la multiplication des deux */
           virtual double surface() const{return long_* large;}
            virtual void travailleAvecMoi() const
            {
                /* ici, on sait que this est un pointeur sur un rectangle...
                 * on peut donc appeler n'importe quelle fonction qui demande un
                 * argument sous la forme d'un pointeur ou d'une référence sur un 
                 * objet de type Rectangle en étant sur qu'elle sera correctement 
                 * appelée, même s'il y a une surcharge quelque par de cette 
                 * fonction qui attend un pointeur ou une référence sur un objet
                 * de type Cercle
                 */
               travaille(*this);
     
            }
        private:
            double long_;
            double larg_;
    };
    class Cercle  : public Forme
    {
        public:
            /* ce qui nous intéresse pour le cercle, c'est son rayon */
            Cercle(double rayon):rayon_(rayon){}
            /* son périmetre est égal à 2 PI R */
            virtual double perimetre() const{return 2 * 3.1415926 * rayon_;}
            /* et sa superficie est égale à Pi * R * R */
            virtual double surface() const{return 3.1415926 * rayon_ * rayon_;}
     
            virtual void travailleAvecMoi() const
            {
                /* ici, on sait que this est un pointeur sur un cercle...
                 * on peut donc appeler n'importe quelle fonction qui demande un
                 * argument sous la forme d'un pointeur ou d'une référence sur un 
                 * objet de type Cercle en étant sur qu'elle sera correctement 
                 * appelée, même s'il y a une surcharge quelque par de cette 
                 * fonction qui attend un pointeur ou une référence sur un objet
                 * de type Rectangle
                 */
               travaille(*this);
     
            }
        private:
            double rayon_;
    };
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    void travaille(Rectangle const &);
    void travaille(Cercle const &);
    et leur implémentation pourrait etre proche de
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
            void travaille(Rectangle const & c)
            {
                std::cout<<"Nous avons un rectangle de "<<c.longueur()<<" de longueur "<<std::endl
                       <<"et de "<<c.largeur()<<" de largeur"<<std::endl;
            }
            void travaille(Cercle const & c)
            {
                std::cout<<"Nous avons un cercle de "<<c.rayon()<<" de rayon"<<std::endl;
            }
    Et la fonction allonY de notre classe UtilisateurDeFormes ressemblerait à
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    class UtilisateurDeFormes 
    {
        public:
            void allonsY()
            {
                ptr_ = DenandeQuoiEtCree();
                ptr_->travailleAvecMoi();
                delete ptr_;
            }
    };
    Oui je comprends, même si je n'aurais pas été capable de le faire moi-même, mais désolé de me répéter encore, tu utilises encore des fonctions libres avec référence d'objets de type dérivés en arguments, ce n'est pas ce que je cherche à faire.

    Citation Envoyé par koala01 Voir le message
    Il n'y a strictement rien qui l'empêche d'accepter des paramètres, non

    Et si...

    Et si je passais en paramètre une référence sur une classe qui expose une fonction publique (mettons travaille) sous la forme de deux versions différentes, l'une acceptant une référence constante sur un objet de type Rectangle et l'autre acceptant une référence constante sur un objet de type Cercle...

    Je pourrais parfaitement appeler cette fonction travaille aussi bien depuis la version de travailleAvecMoi qui s'applique aux cercles qu'à celle qui s'applique aux rectangle... !!!!

    Cela me donnerait donc 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
    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
    class Forme
    {
        public:
            virtual ~Forme(){} // obligatoire ;)
            virtual double perimetre() const = 0;
            virtual double surface() const = 0;
            virtual void travailleAvecMoi(Visiteur const & v) const = 0;
    };
    class Rectangle : public Forme
    {
        public:
            /* un rectangle a besoin de sa longeur et de sa largeur */
            Rectangle(double lon, double larg):long_(lon), larg_(larg){}
            /* le périmetre est égal à deux fois la somme de la longeur et de la largeur */
           virtual double perimetre() const{return (long_+larg_)*2;}
           /* et la superficie est égale à la multiplication des deux */
           virtual double surface() const{return long_* large;}
            virtual void travailleAvecMoi(Visiteur const & v) const
            {
                /* ici, on sait que this est un pointeur sur un rectangle...
                 * on peut donc appeler n'importe quelle fonction qui demande un
                 * argument sous la forme d'un pointeur ou d'une référence sur un 
                 * objet de type Rectangle en étant sur qu'elle sera correctement 
                 * appelée, même s'il y a une surcharge quelque par de cette 
                 * fonction qui attend un pointeur ou une référence sur un objet
                 * de type Cercle
                 */
               v.travaille(*this);
     
            }
        private:
            double long_;
            double larg_;
    };
    class Cercle  : public Forme
    {
        public:
            /* ce qui nous intéresse pour le cercle, c'est son rayon */
            Cercle(double rayon):rayon_(rayon){}
            /* son périmetre est égal à 2 PI R */
            virtual double perimetre() const{return 2 * 3.1415926 * rayon_;}
            /* et sa superficie est égale à Pi * R * R */
            virtual double surface() const{return 3.1415926 * rayon_ * rayon_;}
     
            virtual void travailleAvecMoi(Visiteur const & v) const
            {
                /* ici, on sait que this est un pointeur sur un cercle...
                 * on peut donc appeler n'importe quelle fonction qui demande un
                 * argument sous la forme d'un pointeur ou d'une référence sur un 
                 * objet de type Cercle en étant sur qu'elle sera correctement 
                 * appelée, même s'il y a une surcharge quelque par de cette 
                 * fonction qui attend un pointeur ou une référence sur un objet
                 * de type Rectangle
                 */
               v.travaille(*this);
     
            }
        private:
            double rayon_;
    };
    Là, le coup du visiteur, j'ai pas trop compris l'intêret ni même le fonctionnement de la classe Visiteur (existe-t-elle déjà, faut-il la créer?).

    Citation Envoyé par koala01 Voir le message
    Et voilà les bases posées pour parler du patron de conception "visiteur" et du patron de conception "fabrique", mais tout cela, c'est une autre histoire
    Je pense que je vais laisser le design pattern de coté pour l'instant.

    Je te remercie vraiment encore pour ton aide, je crois m'appercevoir que ce que je cherche à faire n'est pas possible, je crois que j'ai donc plutôt fait une erreur de conception UML de mes classes, va falloir que je reprenne tout je pense.

  16. #16
    Expert éminent sénior
    Avatar de koala01
    Homme Profil pro
    aucun
    Inscrit en
    Octobre 2004
    Messages
    11 614
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 52
    Localisation : Belgique

    Informations professionnelles :
    Activité : aucun

    Informations forums :
    Inscription : Octobre 2004
    Messages : 11 614
    Points : 30 626
    Points
    30 626
    Par défaut
    Citation Envoyé par leternel Voir le message
    koala01, quand le problème sera résolu, récupère tout ca, et fais-en un cours!
    J'y pense
    Citation Envoyé par LinuxUser Voir le message
    Jusqu'ici tout va bas, sauf à un détail près, tu définis souvent des fonctions (libres de plus) prenant en argument une référence du type de base:
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    void doSomething(Base /* const */ & b )
    {
       /* on se fout de ce qu'elle fait */
    }
    Même si je comrpends que cela puisse fonctionner, je pense que c'est là que l'on diverge, je cherche à utiliser l'objet définit du type de base (qui est un membre de la classe qui l'utilise) directement dans les autres méthodes de la classe (qui l'utilise) en choisissant de l'utiliser en temps qu'une de ses classes dérivées:
    Ce qu'il faut comprendre, c'est
    1. que je commence par les bases, sans m'inquiéter de ton problème, pour que tu comprenne un mécanisme simple
    2. qu'il n'y a fondamentalement aucune différence à ce niveau-ci entre une fonction libre et une fonction membre, hormis bien sur le fait qu'il faille passer ton membre en paramètre.
    3. Que Si je passe le paramètre par référence (j'aurais pu le passer par pointeur aussi), c'est pour éviter d'avoir une copie de mon objet
    4. Si je passe le paramètre par référence plutot que par pointeur, c'est pour la sécurité qu'apportent les références par rapport au pointeur
    5. Si je passais le paramètre par valeur, j'aurais une copie de l'objet, et donc je devrais dire adieu au polymorphisme : tout objet de base ne serait plus représenté dans la fonction que par sa partie de base

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    class UtilisateurDesAutresClasses
    {
      void calcul(); // sans prendre de référence à Base& ou Derivee&
    private:
      int m_resultat;
      Base* m_membre;}
    Dans le .cpp
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    UtilisateurDesAutresClasses::UtilisateurDesAutresClasses()
    {
      m_membre = new Derivee();
    }
    void calcul()
    {
      m_resultat = m_membre->fontionDeLaClasseDerivee() + .... ;
    }
    Je crois comprendre, que ce que je veux faire n'est pas possible.
    Non, ce n'est pas possible sous cette forme, et je t'ai expliqué pourquoi :
    Au niveau du compilateur, m_membre est vu comme... un pointeur sur un objet de type Base.

    La seule manière de récupérer quelque chose qui fasse partie du type dérivé depuis le type de base passe par l'utilisation d'une fonction polymorphe ou par le transtypage.
    Oui je comprends très bien tout cela, mais là encore pour utiliser le polymorphisme tu utilises une fonction libre et tu prends une référence de la classe de base en paramètre pour tes appels polymorphiques.
    Comme je te l'ai dit plus haut, il n'y a pas de différence entre l'utilisation d'une fonction membre et celle d'une fonction libre hormis le fait qu'il faut passer la variable en argument.

    Tu pourrais très bien appeler une fonction polymorphe de ton membre dans une fonction membre de ta classe, mais tu ne peux, en tout état de cause, appeler que les fonctions qui sont connues pour le type de base
    Je ne savais pas qu'une telle loi existait (en plus Demeter, référence mythologique, la classe, j'adore), mais de moi-même j'essaye toujours de respecter ce principe/
    C'est une bonne chose
    Oui je comprends ce que tu fais, mais là encore tu manipules les objets avec des pointeurs, j'aurais aimé le même exemple avec un membre de type Forme dans la classe UtilisateurDeFomres.
    Et il me semble qu'il manque un argument quand tu appelles calculPerimetre dans allonsY().
    En fait, j'ai un peu foiré mon copié collé dans le sens où creerCercle et creerRectangle pourraient tout de suite initialiser ptr_...

    Je vais le corriger et utiliser tes conventions plutot que les mienne pour représenter les membre, mais je t'en donne la primeur ici
    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
    class UtilisateurDeFormes 
    {
        public:
            void allonsY()
            {
                 DenandeQuoiEtCree();
                /* et pourtant, ca fonctionne comme on peut s'y attendre en fonction
                 * des réponses données (je n'ai pas changé la fonction calculePerimetre ;)
                 */
                std::cout<< calculePerimetre();
                // j'aurais tout aussi bien pu écrire
                std::cout<<m_ptr->perimetre();
                delete m_ptr;
            }
        private: 
            /* Ces fonctions peuvent être privées, car "à usage interne" à la classe uniquement ;) */
            creerRectangle()
            {
                double longueur;
                double largueur;
                std::cout<<"introduisez la longueur :";
                std::cin>>longueur;
                std::cout<<"introduisez maintenant la largeur :";
                cin>>largeur;
                m_ptr = new Rectangle(longueur, largeur);
            }
            void creerCercle()
            {
                double rayon;
                std::cout<<"introduisez le rayon";
                std::cin >> rayon;
                m_ptr = new Cercle(rayon);
            }
            int afficheChoix()
            {
                int choix;
                std::cout<< "veuillez choisir parmi les option suivantes :"<<std::endl
                         << "1- créer un rectangle "<<std::endl
                         << "2- créer un cercle"<<std::endl;
                 std::cin>> choix;
                 return choix;
            }
            /* On ne sait pas si on obtiendra un Cercle ou un rectangle...
             * on sait juste que nous obtiendrons une forme :P
             */
            void DemandeQuoiEtCree()
            {
                int choix = afficheChoix();
                if(choix == 1 )
                    creerRectangle();
                creerCercle();
            }
            double calculePerimetre() const
            {
                return m_ptr->perimetre();
           }
            /* je sais que j'aurais besoin d'une forme, dont j'ignore encore tout :? */
           Forme * m_ptr;
    };
    Cela te permet il de te rendre compte que toutes les fonctions sont des fonctions membres qui utilisent le membre m_ptr

    Ceci dit, si tu y regarde de plus près, ptr_ est un membre de UtilisateurDeFormes et c'est ce membre que j'utilise dans allonsY (j'avais cependant effectivement laissé une erreur au niveau de calculePerimetre

    Oui, n'ayant pas modifié correctement ptr en ptr_ (maitenant en m_ptr) dans calculePerimetre, j'aurais du passer ptr_ (maintenant m_ptr) en paramètre

    Ce n'était qu'une erreur de copier collé foireux (tous les codes ont été écrits "from scratch", et aucun n'a été testé )
    Oui mais cela voudrait dire que je devrai à chaque fois faire un cast (et un test de type) pour utiliser le bon objet.
    Oui, malheureusement
    Oui je comprends, même si je n'aurais pas été capable de le faire moi-même, mais désolé de me répéter encore, tu utilises encore des fonctions libres avec référence d'objets de type dérivés en arguments, ce n'est pas ce que je cherche à faire.
    Encore une fois, c'est pour te faire comprendre le principe, et c'est, finalement, beaucoup plus proche de ce que tu veux faire que ce que tu ne peux l'imaginer...

    Il est vrai que j'ai oublié de parler de cet aspect

    Mais, si je passais un pointeur (ou une référence) sur ma classe UtilisateurDeFormes à la fonction travailleAvecMoi de mes classes formes, et que je faisais de travaille des fonctions membres de UtilisateurDeFormes, je pourrais appeler la fonction "travaille" de UtilisateurDeFormes depuis mes formes... Les classes Forme, Cercle et Rectangle ressembleraient alors à
    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
    class Forme
    {
        public:
            virtual ~Forme(){} // obligatoire ;)
            virtual double perimetre() const = 0;
            virtual double surface() const = 0;
            /* Allez, pour le coup, je le passe par pointeur bien qu'une référence
             * fut sans doute préférable
             */
            virtual void travailleAvecMoi(UtilisateurDeFormes * uf) const = 0;
    };
    class Rectangle : public Forme
    {
        public:
            /* un rectangle a besoin de sa longeur et de sa largeur */
            Rectangle(double lon, double larg):long_(lon), larg_(larg){}
            /* le périmetre est égal à deux fois la somme de la longeur et de la largeur */
           virtual double perimetre() const{return (long_+larg_)*2;}
           /* et la superficie est égale à la multiplication des deux */
           virtual double surface() const{return long_* large;}
           virtual void travailleAvecMoi(UtilisateurDeFormes * uf) const
            {
                /* ici, on sait que this est un pointeur sur un rectangle...
                 * on peut donc appeler n'importe quelle fonction qui demande un
                 * argument sous la forme d'un pointeur ou d'une référence sur un 
                 * objet de type Rectangle en étant sur qu'elle sera correctement 
                 * appelée, même s'il y a une surcharge quelque par de cette 
                 * fonction qui attend un pointeur ou une référence sur un objet
                 * de type Cercle
                 */
               uf->travaille(*this);
     
            }
        private:
            double long_;
            double larg_;
    };
    class Cercle  : public Forme
    {
        public:
            /* ce qui nous intéresse pour le cercle, c'est son rayon */
            Cercle(double rayon):rayon_(rayon){}
            /* son périmetre est égal à 2 PI R */
            virtual double perimetre() const{return 2 * 3.1415926 * rayon_;}
            /* et sa superficie est égale à Pi * R * R */
            virtual double surface() const{return 3.1415926 * rayon_ * rayon_;}
     
            virtual void travailleAvecMoi(UtilisateurDeFormes * uf) const
            {
                /* ici, on sait que this est un pointeur sur un cercle...
                 * on peut donc appeler n'importe quelle fonction qui demande un
                 * argument sous la forme d'un pointeur ou d'une référence sur un 
                 * objet de type Cercle en étant sur qu'elle sera correctement 
                 * appelée, même s'il y a une surcharge quelque par de cette 
                 * fonction qui attend un pointeur ou une référence sur un objet
                 * de type Rectangle
                 */
               uf->travaille(*this);
     
            }
        private:
            double rayon_;
    };
    et ma classe UtilisateurDeFormes pourrait ressembler à
    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
    class UtilisateurDeFormes 
    {
        public:
            /* les fonctions "travaille" doivent être publiques pour pouvoir
             * etre appelées depuis les classes Cercle et Rectangle
             */
            void travaille(Rectangle const & c)
            {
                std::cout<<"Nous avons un rectangle de "<<c.longueur()<<" de longueur "<<std::endl
                       <<"et de "<<c.largeur()<<" de largeur"<<std::endl;
            }
            void travaille(Cercle const & c)
            {
                std::cout<<"Nous avons un cercle de "<<c.rayon()<<" de rayon"<<std::endl;
            }
            void allonsY()
            { 
                Fabrique f;
                m_ptr= f.DenandeQuoiEtCree();
                Visiteur v; 
                m_ptr->travailleAvecMoi(v);
                delete m_ptr;
            }
        /* le reste comme avant */
    };
    Là, le coup du visiteur, j'ai pas trop compris l'intêret ni même le fonctionnement de la classe Visiteur (existe-t-elle déjà, faut-il la créer?).
    Peut etre que le code que je viens te donner te permettra de comprendre l'intéret...

    En tout cas, il est une bonne base pour me permettre de te l'expliquer

    Le gros intérêt, c'est que, comme tu le vois, la classe UtilisateurDeFormes dépend de Frome, de Cercle et de Rectangle, parce qu'il manipule effectivement chacun de ces types.

    De leur coté, les classes Cercle et Rectangle dépendent elles aussi de UtilisateurDeFormes, car elles doivent appeler appeler une de ses fonctions membre.

    Tu te trouves donc face à un phénomène de dépendances circulaires : A dépend de B qui dépend de A, qui dépend de B

    C'est déjà une situation embêtante en soi, parce que, que ce soit dans un seul fichier ou dans plusieurs, lorsque tu déclareras la fonction travailleAvecMoi de Forme, de Cercle et de Rectangle, il faudra que le compilateur sache qu'il existe une classe UtilisateurDeFormes, et, quand tu déclarera les fonctions travaille de UtilisateurDeFormes, il faudra que le compilateur sache qu'il existe des classes Rectangle et Cercle.

    Il y a, bien sur, moyen de s'en sortir avec les déclarations anticipées, mais il y a un deuxième problème:

    C'est que toute modification que tu pourrais apporter à la classe Cercle ou à la classe Rectangle aura un impact irrémédiable sur la classe UtilisateurDeFormes et inversement.

    Le fait de déléguer la responsabilité du traitement à une autre classe (oui, il faut la créer, cette classe Visiteur) permet de briser le cercle viscieux, surtout si tu délègue la création du visiteur à une quatrième classe (et celle de la création des classes dérivées de Forme à une cinquième)

    En effet, si on s'intéresse aux dépendances, pour l'instant, UtilisateurDeFormes dépend
    1. de Forme
    2. de Cercle
    3. de Rectangle
    et chacune de ces classes dépend de UtilisateurDeFormes.

    Le problème, c'est que si je décide de rajouter les forme Losange, Trapeze et Triangle, ma classe UtilisateurDeFormes en dépendra aussi.

    Or, la classe UtilisateurDeFormes est une classe "centrale", dans le sens où tout part d'elle dans le code (c'est la seule classe qui soit utilisée dans la fonction main !!!! )

    Il faut donc qu'elle soit la plus stable possible, et ce n'est vraiment pas le cas.

    Si je décide de rajouter mes classes Losange, Trapeze et Triangle, je vais devoir modifier UtilisateurDeFormes en conséquence, non seulement en ajoutant certaines fonctions (dont des fonctions publiques, ce qui en modifiera l'interface), mais aussi en modifiant le code interne de "afficheChoix" et de "DemandeQuoiEtCree".

    En plus, pour l'instant, tu n'envisage qu'un seul et unique traitement à effectuer, mais dans trois mois, tu peux très bien te dire que "finalement, il faudrait que je prévoies un autre traitement en plus de celui-ci"...

    Que faudra-t-il changer pour pouvoir faire ce traitement

    Soit tu devra reprendre toute la logique que allonsY et rajouter une série de fonctions, soit tu devras faire hériter une classe (mettons AutreUtilisateurDeFormes) de UtilisateurDeFormes et modifier main pour qu'il puisse prendre les deux cas en compte.

    Or, si UtilisateurDeFormes est la classe centrale de ton projet, la fonction main est la fonction principale de celui-ci : tu penses !!! c'est le point d'entrée de ton programme!!!

    A part pour corriger des erreurs, une fois que cette fonction est écrite, tu ne devrais plus avoir à la modifier !!!

    Mais, en plus, pour pouvoir faire hériter AutreUtilisateurDeFormes de UtilisateurDeFormes, tu devrais apporter pas mal de modifications :
    1. rendre les fonctions allonsY et travaille (les deux versions) virtuelles pour profiter du polymorphisme
    2. rendre au minimum la fonction demandeQuoiEtCree protégée
    3. rendre au minimum le pointeur m_ptr protégé

    Cela contrevient énormément au O de SOLID (qui, rappelons le, représente OCP : Open / Close Principle, le fait qu'un code doit etre ouvert aux évolutions mais fermé aux modifications)

    Comme je te l'ai fait remarquer, UtilisateurDeFormes est la "classe centrale" de ton application...

    C'est sans doute celle qui doit être le plus stable dans le temps!!!

    De telles modifications sont INACCEPTABLES !

    Si l'on crée une classe Fabrique qui s'occupera de demander à l'utilisateur de forme quelle forme il veut et de la créer, en renvoyant un pointeur sur Forme, avec 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
    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
    class Fabrique
    {
    public:
        /* On ne sait pas si on obtiendra un Cercle ou un rectangle...
         * on sait juste que nous obtiendrons une forme :P
         */
        Forme * DemandeQuoiEtCree()
        {
            int choix = afficheChoix();
            if(choix == 1 )
                return creerRectangle();
            retrun creerCercle();
        }
    private: 
     
        int afficheChoix()
        {
             int choix;
             std::cout<< "veuillez choisir parmi les option suivantes :"<<std::endl
                      << "1- créer un rectangle "<<std::endl
                      << "2- créer un cercle"<<std::endl;
             std::cin>> choix;
             return choix;
         }
        Rectangle * creerRectangle()
        {
            double longueur;
            double largueur;
            std::cout<<"introduisez la longueur :";
            std::cin>>longueur;
            std::cout<<"introduisez maintenant la largeur :";
            cin>>largeur;
            return new Rectangle(longueur, largeur);
        }
        Cercle * creerCercle()
        {
            double rayon;
            std::cout<<"introduisez le rayon";
            std::cin >> rayon;
            return new Cercle(rayon);
        }
    };
    et que, parallèlement à cela, nous nous créons une classe Visiteur, sous 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
    class Visiteur
    {
        public:
            /* les fonctions "travaille" doivent être publiques pour pouvoir
             * etre appelées depuis les classes Cercle et Rectangle
             */
            void travaille(Rectangle const & c)
            {
                std::cout<<"Nous avons un rectangle de "<<c.longueur()<<" de longueur "<<std::endl
                       <<"et de "<<c.largeur()<<" de largeur"<<std::endl;
            }
            void travaille(Cercle const & c)
            {
                std::cout<<"Nous avons un cercle de "<<c.rayon()<<" de rayon"<<std::endl;
            }
    };
    en retirant toutes les fonctions que j'ai mise dans visiteur et dans fabrique de UtilisateurDeFormes, cette classe pourrait ressembler à
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    class UtilisateurDeFormes 
    {
        public:
            void allonsY()
            { 
                Fabrique f;
                m_ptr= f.DenandeQuoiEtCree();
                Visiteur v; 
                m_ptr->travailleAvecMoi(v);
                delete m_ptr;
            }
        private:
            Forme * m_ptr;
    };
    Sans oublier, bien sur, de faire en sorte que Forme, Cercle et Rectangle manipulent des Visiteur et non plus UtilisateurDeFormes.

    Cela les ferait ressembler à quelque chose comme
    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
    class Forme
    {
        public:
            virtual ~Forme(){} // obligatoire ;)
            virtual double perimetre() const = 0;
            virtual double surface() const = 0;
            /* Allez, pour le coup, je le passe par pointeur bien qu'une référence
             * fut sans doute préférable
             */
           virtual void travailleAvecMoi(visiteur /* const */ & v) const = 0;
    };
    class Rectangle : public Forme
    {
        public:
            /* un rectangle a besoin de sa longeur et de sa largeur */
            Rectangle(double lon, double larg):long_(lon), larg_(larg){}
            /* le périmetre est égal à deux fois la somme de la longeur et de la largeur */
           virtual double perimetre() const{return (long_+larg_)*2;}
           /* et la superficie est égale à la multiplication des deux */
           virtual double surface() const{return long_* large;}
           virtual void travailleAvecMoi(visiteur /* const */ & v) const
            {
                /* ici, on sait que this est un pointeur sur un rectangle...
                 * on peut donc appeler n'importe quelle fonction qui demande un
                 * argument sous la forme d'un pointeur ou d'une référence sur un 
                 * objet de type Rectangle en étant sur qu'elle sera correctement 
                 * appelée, même s'il y a une surcharge quelque par de cette 
                 * fonction qui attend un pointeur ou une référence sur un objet
                 * de type Cercle
                 */
               v.travaille(*this);
     
            }
        private:
            double long_;
            double larg_;
    };
    class Cercle  : public Forme
    {
        public:
            /* ce qui nous intéresse pour le cercle, c'est son rayon */
            Cercle(double rayon):rayon_(rayon){}
            /* son périmetre est égal à 2 PI R */
            virtual double perimetre() const{return 2 * 3.1415926 * rayon_;}
            /* et sa superficie est égale à Pi * R * R */
            virtual double surface() const{return 3.1415926 * rayon_ * rayon_;}
     
            virtual void travailleAvecMoi(visiteur /* const */ & v) const
            {
                /* ici, on sait que this est un pointeur sur un cercle...
                 * on peut donc appeler n'importe quelle fonction qui demande un
                 * argument sous la forme d'un pointeur ou d'une référence sur un 
                 * objet de type Cercle en étant sur qu'elle sera correctement 
                 * appelée, même s'il y a une surcharge quelque par de cette 
                 * fonction qui attend un pointeur ou une référence sur un objet
                 * de type Rectangle
                 */
               v.travaille(*this);
     
            }
        private:
            double rayon_;
    };
    Tu remarqueras au passage, et c'est très important, que toutes les fonctions qui se trouvent maintenant dans Fabrique et dans Visiteur sont simplement sorties de ma classe UtilisateurDeFormes

    Tout ce que l'on fait, et c'est important de le souligner, c'est déléguer certaines responsabilités à certaines classes qui ne font que cela !!!

    Ce n'est donc qu'une application stricte du SRP: chaque besoin a son outil et chaque outil pour un besoin spécifique

    Effectivement, ce n'est pas "tout à fait" ce que tu veux faire, mais, et c'est ce qui importe, le résultat que tu obtiens est celui que tu recherches

    David Wheeler a dit un jour
    Citation Envoyé par David Wheeler
    all problems in computer science can be solved by another level of indirection .
    A partir du moment où ce que tu veux faire est impossible ou que les solutions "directes" pour le faire ne sont pas acceptables, il est bon de mettre cette phrase en pratique

    les dépendances commencent à s'inverser... :
    UtilisateurDeFormes dépend toujours de trois classes, mais c'est maintenant
    1. de Fabrique
    2. de Visiteur
    3. et de Forme (qui est une abstraction)
    L'avantage que l'on en tire C'est que UtilisateurDeFormes verra toutes les formes (aussi bien Cercle que Rectangle ou toute classe dérivée de Forme, présente et à venir) comme... des Formes.

    Nous avons toujours un problème de dépendances, mais il est déjà moins grave : la classe Visiteur dépend (doit connaitre) Cercle et Rectangle et, de leur coté, les classes Cercle et Carré dépendent (doivent connaitre) de Visiteur.

    Mais nous pourrions résoudre ce problème d'une manière qui, en plus, nous permettra de "prévoir les évolutions futures".

    Car, si on met un peu de polymorphisme dans la classe visiteur, nous pourrions partir sur l'idée que Visiteur est une classe de base dans une hiérarchie qui lui est propre, sous la forme de
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    class Visiteur
    {
        public:
            virtual ~Visiteur(){} //obligatoire ;)
            virtual void travaille(Cercle const &) const = 0;
            virtual void travaille(Rectangle const & ) const = 0;
    };
    et nous créerions donc un visiteur concret (nommons le VisiteurReel) qui fait effectivement le travail 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
    16
    class VisiteurConcret
    {
        public:
            /* les fonctions "travaille" doivent être publiques pour pouvoir
             * etre appelées depuis les classes Cercle et Rectangle
             */
            void travaille(Rectangle const & c)
            {
                std::cout<<"Nous avons un rectangle de "<<c.longueur()<<" de longueur "<<std::endl
                       <<"et de "<<c.largeur()<<" de largeur"<<std::endl;
            }
            void travaille(Cercle const & c)
            {
                std::cout<<"Nous avons un cercle de "<<c.rayon()<<" de rayon"<<std::endl;
            }
    };
    Cela nous permettrait de respecter le I de SOLID : il est mis pour ISP
    qui est l'acronyme de Interface Segregation Principle(ou, si tu préfères en français : Principe de Ségrégation des Interfaces).

    Ce principe dit nous apprend en effet que nos objets devraient dépendre d'abstractions (comme le sont les classes Forme et maintenant Visiteur) plutôt que de classes concrètes (comme peuvent l'être Cercle, Rectangle et maintenant VisiteurConcret )
    Et pour éviter que UtilisateurDeFormes ne dépende de VisiteurConcret (ce qui serait contre productif), nous pouvons créer une "fabrique de visiteur" sous 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
    24
    25
    class FabriqueDeVisiteur
    {
    public:
        /* On ne sait pas si on obtiendra un Cercle ou un rectangle...
         * on sait juste que nous obtiendrons une forme :P
         */
        Visiteur* DemandeQuoiEtCree()
        {
            int choix = afficheChoix();
            if(choix == 1 )
                return creerVisiteurConcret();
            retrun NULL
        }
    private: 
     
        int afficheChoix()
        {
             std::cout<<"Pour l'instant, vous ne pouvez utiliser que VisiteurConcret"<<std::endl;
             return 1;
         }
        VisiteurConcret * creerVisiteurConcret()
        {
            return new VisiteurConcret;
        }
    };
    Et, bien sur, il faudrait prendre ces modification en compte dans UtilisateurDeClasses 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
    16
    class UtilisateurDeFormes 
    {
        public:
            void allonsY()
            { 
                Fabrique f;
                m_ptr= f.DenandeQuoiEtCree();
                FabriqueDeVisiteur fvisiteur
                Visiteur *v = fVisiteur.DemandeQuoiEtCree(); 
                m_ptr->travailleAvecMoi(*v);
                delete m_ptr;
                delete v;
            }
        private:
            Forme * m_ptr;
    };
    Et là, nous en arrivons à la solution idéale, parce que, en terme de dépendances, on a:
    1. UtilisateurDeClasses ne dépend que de Fabrique, de FabriqueDeVisiteur, de Visiteur (qui est une abstraction) et de Forme (qui est une abstraction)
    2. FabriqueDeForme ne dépend que de Cercle et de Rectangle, mais uniquement pour les créer
    3. FabriqueDeVisiteur dépend de VisiteurConcret, mais uniquement pour le créer
    4. Forme (qui est une abstraction), Rectangle et Cercle ne dépende que de Visiteur (qui est une abstraction)


    Je pense que je vais laisser le design pattern de coté pour l'instant.
    Et pourtant, je viens de te présenter le pourquoi et le comment de le mettre en oeuvre

    J'en ai même profité pour te parler du patron de conception Fabrique
    Je te remercie vraiment encore pour ton aide, je crois m'appercevoir que ce que je cherche à faire n'est pas possible,
    Pas possible de manière directe, mais tout ce que je viens d'écrire te permet de le faire de manière indirecte
    je crois que j'ai donc plutôt fait une erreur de conception UML de mes classes, va falloir que je reprenne tout je pense.
    Ca, par contre, c'est un possibilité à tres forte probabilité

    Et je suis sur à 99.9% que, si tu revois ta conception, tu en arriveras au final à quelque chose de très proche du patron de conception visiteur (et peut etre meme que tu utilisera le patron de conception Fabrique )

    Tu aurais donc grand tord de "laisser le desing pattern" de coté
    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
    Membre confirmé Avatar de LinuxUser
    Inscrit en
    Avril 2007
    Messages
    857
    Détails du profil
    Informations forums :
    Inscription : Avril 2007
    Messages : 857
    Points : 616
    Points
    616
    Par défaut
    OK, je vais essayer de digérer tout ça, ensuite je posterai le résultat et tu me diras ce que tu en penses.

Discussions similaires

  1. [AC-2003] Problème de formulaire ou de conception
    Par Chris_Dupasquier dans le forum Modélisation
    Réponses: 6
    Dernier message: 11/03/2010, 10h35
  2. Problème d'implementation et de conception
    Par Invité1 dans le forum C#
    Réponses: 7
    Dernier message: 14/05/2008, 00h29
  3. Problème de polymorphisme
    Par Alain Defrance dans le forum C++
    Réponses: 6
    Dernier message: 14/03/2008, 17h03
  4. [Conception] Problème au niveau de la conception d'un projet
    Par Evocatii dans le forum PHP & Base de données
    Réponses: 1
    Dernier message: 26/06/2007, 15h55
  5. Problème d'analyse ou de conception
    Par nanou2002 dans le forum Architecture
    Réponses: 4
    Dernier message: 25/10/2006, 17h27

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