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


Sujet :

C++

Vue hybride

Message précédent Message précédent   Message suivant Message suivant
  1. #1
    Membre averti
    Homme Profil pro
    Ingénieur Etudes
    Inscrit en
    Juillet 2010
    Messages
    54
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Haute Garonne (Midi Pyrénées)

    Informations professionnelles :
    Activité : Ingénieur Etudes

    Informations forums :
    Inscription : Juillet 2010
    Messages : 54
    Par défaut Problème de design ?
    Bonjour,

    J'aimerai réaliser ce code :
    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
     
    class Animal
    {
    public:
      Animal(string name) : m_name(name) {};
      ~Animal(){};
     
      string getName() const {return m_name;};
      virtual int getLengthQueue() const = 0;
     
    protected:
      string m_name;
    };
     
    class Chat : public Animal
    {
    // Constructeurs, destructeurs...
     
    public:
     int getLengthQueue() const {return m_lengthQueue;};
     
    protected:
      int m_lengthQueue;
    };
     
    class Humain: public Animal
    {
    // Constructeurs, destructeurs...
     
    public:
     int getLengthQueue() const {return 0;}; // PROBLEME
     
    };
    Animal est la classe mère, Chat une classe fille avec un paramètre en plus, et Humain avec aucun paramètre en plus.
    J'ai décidé que j'aurai besoin de mettre le getter de la longueur de la queue en virtuel pur, parce que dans le code je ne connais pas précisément à quel type j'ai affaire (si c'est un Chat ou un Humain), dans une fonction quelconque.
    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
     
    int computeLengthQueueAnimal(Animal &ani)
    {
       return (ani->getLengthQueue());
    }
     
    int main()
    {
     
      Chat c("tony", 5); // on imagine le constructeur avec son nom et la longueur de la queue
      cout <<  computeLengthQueueAnimal(c) << endl;
     
      Humain h("Jean")
      cout <<  computeLengthQueueAnimal(h) << endl;
    }

    Le problème c'est que si je peux définir ce getter pour le Chat, c'est difficilement le cas pour l'Humain, vu qu'il n'a pas de paramètre "m_lengthQueue", mais je suis quand même obligé, alors je met un "return 0".

    Je trouve pas ça très propre. Du coup, est ce que j'ai vraiment pas le choix et je dois changer de design ? Mais si oui lequel vous me proposez ? Ou bien on doit laisser ça là comme ça...?

  2. #2
    Rédacteur/Modérateur


    Homme Profil pro
    Network game programmer
    Inscrit en
    Juin 2010
    Messages
    7 147
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 38
    Localisation : Canada

    Informations professionnelles :
    Activité : Network game programmer

    Informations forums :
    Inscription : Juin 2010
    Messages : 7 147
    Billets dans le blog
    4
    Par défaut
    Tu constates toi-même du problème. Ta hiérarchie est mauvaise.
    Est-ce qu'un Humain est un Animal ? Admettons.
    Est-ce que tout Animal a une queue ? Apparemment pas.
    Donc soit Humain ne doit pas hériter de Animal, soit Animal ne doit pas avoir une fonction qui ne sert clairement à rien pour lui puisque seule une partie des classes filles en a utilité.
    Soit tu vis avec le fait que certains n'en ont pas et retournent juste 0.

    Et indépendamment de ça, de nombreux ; sont inutiles, getName devrait retourner const& et le destructeur de Animal devrait être virtuel. Et implémenté =default puisque vide.
    Pensez à consulter la FAQ ou les cours et tutoriels de la section C++.
    Un peu de programmation réseau ?
    Aucune aide via MP ne sera dispensée. Merci d'utiliser les forums prévus à cet effet.

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

    Informations professionnelles :
    Activité : aucun

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

    En fait, le principe de substitution de Liskov est clair: toute propriété (on pourrait simplifier en disant "toute fonction publique", en première analyse) valide pour la classe de base doit être valide pour l'ensemble des classes qui en dérivent.

    Si bien que, si tu as une fonction qui "n'a aucun sens" (comme la fonction getTailLength, car queue se dit tail en anglais ) pour une seule des classes (comme la classe Human, dans le cas présent) qui dérivent de manière directe ou indirecte de ta classe de base (la classe Animal, dans le cas présent), ou bien la classe dérivée (Human) ne peut pas faire partie de la hiérarchie organisée à partir de la classe de base (Animal), ou bien la fonction (getTailLength) ne peut pas faire partie de l'interface de la classe de base (Animal).

    Cela ne t'empêchera pas, le cas échéant, d'avoir une classe "intermédiaire" capable d'exposer cette fonction et dont dériveraient alors l'ensemble des classes pour lesquelles cette fonction auraient du sens, ce qui te donnerait une hiérarchie proche de
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    /* la classe de base */
    class Animal{
    public:
        Animal(std::string name):name_{name}{
        }
        /* construteur de copie et opérateur d'affectation inactivés car sémantique d'entité */
        Animal(Animal const &) = delete;
        Animal & operator=(Animal const &) = delete;
        /* destructeur virtuel, pour pouvoir détruire n'importe quelle instance "passant pour être"
         * du type de la classe de base
         */
        virtual ~Animal() = default;
        /* uniquement les fonctions libres "qui ont du sens" POUR L'ENSEMBLE DES CLASSES DERIVEES */
        std::string const & getName() const{
            return name_;
        }
        virtual void sayHello() const = 0;
    private:
        std::string name_;
    };
    /* certains animaux ont une queue, et tous les animaux qui ont une queue doivent
     * pouvoir en donner la taille
     */
    class AnimalWithTail : public Animal{
    public:
        AnimalWithTail(std::string name, size_t tail tailLength):Animal{name}, tailLength_{tailLength}{
        }
        size_t getTailLength() const{
            return tailLength_;
        }
    private:
       size_t tailLength_;
    };
    /* C'est le cas du chat et du chien */
    class Cat : public AnimalWithTail{
    public:
        Cat():AnimalWithTail{"cat",30}
        void sayHello() const override{
            std::cout<<"miaouw\n";
        }
    };
    class Cat : public AnimalWithTail{
    public:
        Cat():AnimalWithTail{"cat",30}
        void sayHello() const override{
            std::cout<<"wouah\n";
        }
    };
     
    /* Human EST-UN animal (mettons), mais n'a pas de queue */
    class Human : public Animal{
    public 
        Human(size_t size):Animal{"human"}, size_{size}{
        }
        size_t getSize() const{
            return size_;
        }
        void sayHello() const override{
            std::cout<<"hello\n";
        }
    private:
        size_t size_;
    };
    et dont le schéma ressemblerait sans doute à
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
          Animal
             |
        |---------------|
       Human       AnimalWithTail
                        |
                  |-------------|
                 Cat           Dog
    Et qui pourrait être considéré comme "conceptuellement correct", pour autant que l'on admette que la sémantique accepte de considérer l'humain comme étant un animal (ce qu'il est au demeurant, note )

    Cependant, cela présenterait l'énorme inconvénient de nous faire partir sur une hiérarchie "en profondeur", car on se rend assez facilement compte que Human (qui est une classe concrête) n'est pas "au même niveau" que Cat ou que Dog dans cette hiérarchie (on pourrait dire que "Human" est une classe "Tante" de Cat et de Dog, ce qui a "quelque chose" de "choquant" d'un point de vue sémantique, tu ne trouve pas )

    Et c'est d'autant plus vrai que l'on ne s'est, jusqu'à présent, intéressé qu'à la présence d'une queue, alors que ton projet va surement évoluer, et que tu voudras sans doute ajouter "un certain nombre" de comportements (comme le fait de nager ou de voler) qui ne sont pas forcément adaptés à l'ensemble des animaux.

    Une autre solution, si on tient à considérer que Human dérive de Animal -- car on se rend compte que cette décision pose quelques problèmes "sémantiques" -- serait sans doute de mettre en place une série "d'interfaces" ou, plutôt de "politiques" qui permettront d'ajouter (ou non) les comportements spécifiques.
    On pourrait alors avoir des politiques qui prendraient une forme proche de
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    /* politique pour le vol */
    struct Flying{
        void fly() const{
            /* ce qu'il faut faire */
        }
        bool canFly() const{
            return true;
        }
    };
    struct NoFlying{
        /* on peut demander si l'animal peut voler (la réponse est non)*/
        bool canFly() const{
            return false;
        }
        /* on peut pas voler, on ne rajoute pas la fonction fly*/
    };
    template<typename Trait>
    struct FlyingPolicy : public Trait{
        static_assert(std::is_same_v<Trait, Flying> ||
                           std::is_same_v<Trait, NoFlying>,
         "Only Flying and NoFlying are valid traits");
    };
    /* politique pour la nage */
    struct Swimming{
        void swim() const{
            /* ce qu'il faut faire */
        }
        bool canSwim() const{
            return true;
        }
    };
     
    struct NoSwimming{
     
        /* on peut demander si l'animal peut nager (la réponse est non)*/
        bool canSwim() const{
            return false;
        }
        /* on peut pas nager, on ne rajoute pas la fonction */
    };
    template<typename Trait>
    struct SwimmingPolicy : public Trait{
        static_assert(std::is_same_v<Trait, Swimming> ||
                           std::is_same_v<Trait, NoSwimming>,
         "Only Swimming and NoSwimmingare valid traits");
    };
    /* politique pour la gestion des queues */
    struct TailHolder{
    public:
        TailHolder(size_t tailSize): tailSize_{tailSize}{
        }
        size_t tailSize() const{
            return tailSize_;
        }
        bool hasTail() const{
            return true;
        }
    private:
        size_t tailSize_;
    };
    struct NoTail{
        /* on peut demander si l'animal peut a une queue (la réponse est non)*/
        bool hasTail() const{
            return false;
        }
        /* pas de queue, pas de taille :D */
    };
    template <typename Trait>
    struct TailPolicy : public Trait{
     
        static_assert(std::is_same_v<Trait, TailHolder> ||
                           std::is_same_v<Trait, NoTail>,
         "Only TailHolder and NoTail valid traits");
    };
    /* et toutes les autres politiques qui viendront s'ajouter au fil du temps ;) */
    Grace à cela, nous pourrons créer une classe de base AnimalBase qui ne propose que des propriétés valides pour l'ensemble des classes dérivées, 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
    20
    21
    22
    23
    24
    25
    class AnimalBase{
    public:
        AnimalBase(std::string name):name_{name}{
        }
        /* construteur de copie et opérateur d'affectation inactivés car sémantique d'entité */
        AnimalBase(AnimalBaseconst &) = delete;
        AnimalBase& operator=(AnimalBaseconst &) = delete;
        /* destructeur virtuel, pour pouvoir détruire n'importe quelle instance "passant pour être"
         * du type de la classe de base
         */
        virtual ~AnimalBase() = default;
     
        /* uniquement les fonctions libres "qui ont du sens" POUR L'ENSEMBLE DES CLASSES DERIVEES */
        std::string const & getName() const{
            return name_;
        }
        virtual void sayHello() const = 0;
        /* tous les animaux vont pouvoir manger */
        virtual void eat() const = 0;
        /* tous les animaux vont se reproduire */
        virtual void reproduce() const = 0;
        /* et tout ce à quoi je ne pense pas forcément ;) */
    private:
        std::string name_;
    };
    et nous pourrons créer un modèle de classe qui reprend l'ensemble des politiques que nous avons définies, et dont toutes les autres classes pourront dériver:
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    template <typename Fly,
              typename Swim,
              typename Tail>
    class Animal : public AnimalBase, // on se rattache à AnimalBase pour les comportements communs
                       public SwimPolicy<Swim>,    // ainsi qu'à  la politique de nage,
                       public  FlyingPolicy<Fly>,  //       à la politique de vol
                       public TailPolicy<Tail>{    //       et à la politique de queue
     
    };
    Et grâce à cela, nous pouvons créer toutes nos classes dérivées, 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
    /*un humain peut nager, ne peut pas voler, et n'a pas de queue */
    class Human:public Animal<Swimming, NoFlying, NoTail>{
        /* y a des trucs à  rajouter ici :D */
    };
    /* un chat n'aime pas nager (on va dire qu'il ne peut pas), ne sait pas voler, et a une queue */
    class Cat : publiic Animal<NoSwimming, NoFlying, TailHolder>{
        /* y a des trucs à  rajouter ici :D */
    };
    /* un chien aime nager, ne sait pas voler, et a une queue */
    class Dog : public Animal<Swimming, NoFlying, TailHolder>{
        /* y a des trucs à  rajouter ici :D */
    };
    /* un aigle n'aime pas nager (on va dire qu'il ne peut pas le faire), sait voler, et a une queue */
    class Eagle : public Animal<NoSwimming, Flying, TailHolder>{
        /* y a des trucs à  rajouter ici :D */
    };
    class
    Cela nous permettra non seulement d'avoir toutes les classes concrètes au "même niveau" (elles dérivent toutes "directement" de Animal) mais, aussi de pouvoir les substituer en fonction du trait spécifique utilisé dans le cadre d'une politique particulière.

    Par exemple, si tu crées une fonction
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    void doSomethingWhileSwimming(SwimmingPolicy<Swimming> const & animal){
        /* on fait quelque chose en nageant ;) */
    }
    il n'y aura que les animaux qui peuvent effectivement nager qui seront pris en compte ici
    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

  4. #4
    Membre averti
    Homme Profil pro
    Ingénieur Etudes
    Inscrit en
    Juillet 2010
    Messages
    54
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Haute Garonne (Midi Pyrénées)

    Informations professionnelles :
    Activité : Ingénieur Etudes

    Informations forums :
    Inscription : Juillet 2010
    Messages : 54
    Par défaut
    Bonsoir,

    Merci pour vos réponses, et celle de koala01 en particulier !

    En revanche, je trouve cette solution pas hyper simple au niveau du code, il y a beaucoup de membres en templates.
    En fait, mon code original contient déjà des classes avec templates <T>, et je comptais en rajouter un de plus (vous vous doutez que mon code ne concerne pas les animaux pour de vrai ).
    Je suis pas un grand expert du C++. Pour être franc, la solution de koala01, je l'aurai pas trouvé tout seul (les static_assert par exemple, et le côté "politique").
    Du coup je sais pas si rajouter des templates comme ça peut influer sur les performances du code, ou sa lisibilité ou encore sa maintenabilité.

    J'ai eu le temps de réfléchir de mon côté et voilà ce que je propose :

    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
     
    #include <iostream>
    #include <vector>
     
    using namespace std;
     
    class Animal{
    public:
        Animal() = default;
        Animal(int a) : mA(a) {};
        ~Animal() = default;
     
        int const & getmA() const
        {
            return mA;
        }
     
    protected:
        int mA;
     
    };
     
    class AnimalWithQueue : public Animal{
    public:
        AnimalWithQueue () = default;
        AnimalWithQueue (int a, int b) : Animal(a), mB(b) {};
        ~AnimalWithQueue () = default;
     
        int const & getmB() const
        {
            return mB;
        }
     
    protected:
        int mB;
     
    };
    Du coup dans une fonction où j'ai besoin de "getter" les classes d'un vecteur par exemple (c'est le cas dans mon code original), je suis obligé de faire un static_cast.
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
     
    void readClass(const vector<Animal*> &vec, int i, Animal* val)
    {
     
        if (i == 0) //Indice où je suis certain d'avoir un Animal
            val = new Animal(*(vec.at(i)));
        if (i == 1) //Indice où je suis certain d'avoir un Animal avec une queue  
            val = new AnimalWithQueue(*(static_cast<AnimalWithQueue*>(vec.at(i))));
    }
    Et dans le main :
    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
     
    int main()
    {
        Animal insta(5);
        AnimalWithQueue instb(6,10);
     
        vector<Animal *> vecC;
        vecC.push_back(&insta);
        vecC.push_back(&instb);
     
        Animal  *readInstA = new Animal ;
        AnimalWithQueue *readInstB = new AnimalWithQueue ;
     
        readClass(vecC, 0, readInstA);
        cout << readInstA << endl;
        readClass(vecC, 1, readInstB);
     
     
        cout << readInstA->getmA() << endl;
        cout << readInstB->getmA() << endl;
        cout << readInstB->getmB() << endl;
     
        delete readInstA;
        delete readInstB;
     
        return 0;
    }
    J'ai mis 0 et 1 dans cet exemple pour un vecteur qui a une taille de deux (je mets pas tout non plus). Dans le code original c'est prévu d'être beaucoup plus complexe, mais je veux vérifier si mon idée marche.
    Du coup j'ai testé et ça a donné ce que je voulais.
    Mais ce qui m'inquiète, c'est le static_cast...j'ai cru comprendre que ça pouvait être un peu trop bourrin.

  5. #5
    Membre averti
    Homme Profil pro
    Ingénieur Etudes
    Inscrit en
    Juillet 2010
    Messages
    54
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Haute Garonne (Midi Pyrénées)

    Informations professionnelles :
    Activité : Ingénieur Etudes

    Informations forums :
    Inscription : Juillet 2010
    Messages : 54
    Par défaut
    Citation Envoyé par koala01 Voir le message
    Salut,

    Par exemple, si tu crées une fonction
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    void doSomethingWhileSwimming(SwimmingPolicy<Swimming> const & animal){
        /* on fait quelque chose en nageant ;) */
    }
    il n'y aura que les animaux qui peuvent effectivement nager qui seront pris en compte ici
    Une question sur ce passage : je comprends pas pourquoi le type du paramètre en entrée est du type de la structure et non de la classe de l'animal pouvant nager...

    D'ailleurs dans le design proposé, je comprends pas l'utilité de la classe Animal.

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

    Informations professionnelles :
    Activité : aucun

    Informations forums :
    Inscription : Octobre 2004
    Messages : 11 644
    Par défaut
    L'astuce, c'est que si tu as deux spécialisations d'une même classe template avec deux types différents, tu n'a aucun lien de substituabilité entre ces deux spécialisations.

    Pour te faire comprendre cette phase, imaginons une classe template proche de
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    template <typename T>
    class MaClasse{
    public:
        void foo();
    private:
        T data_;
    }
    et deux spécialisations de cette classe:
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
     
    /* soient A et B sont des types de données qui existent */
    MaClasse<A> objA;
    maClasse<B> objB;
    Eh bien, il n'y a rien qui relie MaClasse<A> à MaClasse<B> et tu ne pourras pas faire passer l'un pour l'autre.

    Bien sur, objA et objB vont tous les deux exposer la fonction foo.

    Bien sur, ils vont tous les deux disposer d'une donnée nommée data_.

    Mais la comparaison va s'arrêter là, ne serait-ce que parce que objA va disposer d'une donnée nommée data_ qui est de type A et objB va disposer d'une donnée nommée data_ qui est ... de type B.

    Je peux, si je le souhaite, "laisser planer le doute" quant au type qui sera utilisé pour la spécialisation de ma classe, en créant une fonction template proche de
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    template <typeame T>
    void doSomething(MaClasse<T> /* const &*/ obj){
        /* je peux utiliser obj qu'il soit une instance de MaClasse<A> ou une instance de MaClasse<B> */
    }
    Mais, si je veux pouvoir travailler spécifiquement avec une instance de MaClasse<A> (ou avec une instance de MaClasse<B>) à l'exclusion de toute instance une autre spécialisation de MaClasse, alors, je dois indiquer spécifiquement que je veux une instance de MaClasse<A>, mais rien ne m'empêche de créer une surcharge de la fonction
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
     
    void doSomething(MaClasse<A>/* const & */obj){
        /* je sais que je travaille ici avec une instance de MaClasse<A> */
    }
    void doSomething(MaClasse<B>/* const & */obj){
        /* je sais que je travaille ici avec une instance de MaClasse<B> */
    }
    int main(){
        MaClasse<A> objA;
        MaClasse<B> objB;
        doSomething(objA); /* appele la surcharge utilisant une instance de MaClasse<A> */
        doSomething(objB); /* appele la surcharge utilisant une instance de MaClasse<B> */
    }
    Dans mon intervention précédente, je te présente le code
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    template <typename Fly,
              typename Swim,
              typename Tail>
    class Animal : public AnimalBase, // on se rattache à AnimalBase pour les comportements communs
                       public SwimPolicy<Swim>,    // ainsi qu'à  la politique de nage,
                       public  FlyingPolicy<Fly>,  //       à la politique de vol
                       public TailPolicy<Tail>{    //       et à la politique de queue
     
    };
    Cela signifie que Animal dérive de AnimalBase, qui sera la "base commune" à tous les animaux. je peux donc faire passer une instance de "n'importe quel animal" pour une instance de AnimalBase.

    Avec les classes concrètes que je t'ai présentées plus haut, cela veut dire que, si je peux manipuler "n'importe quel type d'animal" à partir du moment où je le considère "uniquement" comme étant un AnimalBase, pour autant que je prenne en compte le fait que je ne pourrai appeler que les fonctions publiques exposées par cette classe:
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    void doIt(AnimalBase /* const &*/ obj){
        /* je ne dispose ici que de l'interface issue de AnimalBase */
    }
    int Main(){
        Cat myCat;
        Dog myDog;
        Eagle myEagle;
        Humain mySelf;
        doIt(myCat); // ca marche: Cat dérive de manière indirecte de AnimalBase
        doIt(myDog); // ca marche: Dog dérive de manière indirecte de AnimalBase
        doIt(myEagle); // ca marche: Eagle dérive de manière indirecte de AnimalBase
        doIt(mySelf); // ca marche: Human dérive de manière indirecte de AnimalBase
    }
    Par contre, dés que l'on arrive au niveau de la classe Animal, les choses se corsent, car on dispose en réalité de toutes les combinaisons possibles que les différentes politiques vont nous permettre de créer. On peut en effet avoir
    1. un animal sans queue, qui ne nage pas et qui ne vole pas
    2. un animal sans queue, qui ne nage pas et qui vole
    3. un animal sans queue qui nage, et qui ne vole pas
    4. un animal sans queue qui nage et qui vole
    5. un animal avec queue qui ne nage pas et qui ne vole pas
    6. un animal avec queue qui ne nage pas et qui vole
    7. un animal avec queue qui nage et qui ne vole pas
    8. un animal avec queue qui nage et qui vole
    9. (pas sur de n'avoir pas oublié une possibilité :-$ )

    Eh bien, tu peux te dire que toutes ces possibilités vont être considérées comme des types différents et indépendants entre eux, ou, du moins, qu'ils le seraient s'il n'y avait pas la base commune fournie par AnimalBase.

    Tu ne peux, en effet, pas faire passer un animal sans queue qui ne nage pas et qui vole pour un animal avec queue qui nage et qui ne vole pas, vu qu'il n'y a aucun point commun entre les deux. Et si tu veux manipuler ces deux animaux en même temps, par exemple, en les placant dans une collection (par exemple, dans un std::vector), alors, tu dois travailler en n'utilisant la seule chose qui leur soit réellement commune: leur héritage (indirecte) de la classe AnimalBase.

    J'aurais -- effectivement -- pu te créer une fonction prenne directement un animal pouvant nager. Les vraies questions auraient alors été "Que fais-je de la possibilité d'avoir une queue?" et "Que fais-je de la possibilité de voler?" parce que je vais devoir fournir une spécialisation totale au compilateur, ou, à tout le moins, lui fournir le moyen de déduire cette spécialisation totale.

    J'aurais donc eu trois possibilités:
    La plus facile (pour moi, du moins) aurait sans doute de créer une fonction template proche de
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    template <typename Fly, // la politique de vol
              typename Tail> // la politique de queue
    void doSomething(Animal<Swimming, Fly, Tail> const & animal){
       /* je sais que j'ai un animal,
        * je sais qu'il peut nager,
        * je ne sais pas (et ne veux pas m'en inquiéter) s'il sait voler,
        * je ne sais pas (et ne veux pas m'en inquiéter) s'il a une queue
        */
    }
    La plus spécifique aurait pu être de fournir une surcharge pour les différentes possibilités qui nous restent en partant du principe qu'il sait nager ... Il faut juste savoir s'il y a du sens à le faire:
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    void doSomething(Animal<Swimming, Flying, TailHolder>  const & animal){
        /* animal qui sait nager, sait voler et a une queue*/
    }
    void doSomething(Animal<Swimming, NoFlying, TailHolder>  const & animal){
        /* animal qui sait nager, ne sait pas voler et a une queue*/
    }
    void doSomething(Animal<Swimming, NoFlying, NoTail>  const & animal){
        /* animal qui sait nager, ne sait pas voler et n'a pas de queue*/
    }
    /* et les autres combinaisons, tu  auras compris l'idée ;) */
    Enfin, je peux choisir l'option que j'ai utilisée dans mon intervention précédente et me dire que, en fait, tout ce qui m'intéresse, c'est que l'animal puisse nager, et que je n'ai -- en définitive -- même pas besoin de savoir qu'il s'agit d'un animal (on pourrait dire qu'il s'agit "c'est quelque chose" qui sait nager).

    Cela signifie que je ne pourrais accéder, depuis mon objet, qu'aux fonctions exposées par SwimmingPolicy<Swimming> (canSwim et swim), mais en fait, je m'en fous parce qu'il n'y a vraiment que cette caractéristique de "quelque chose qui peut nager" qui m'intéresse.

    Tout le reste venant de la classe Animal (et de sa classe de base AnimalBase) ne m'intéresse ... tout simplement pas. Je n'ai donc absolument aucun besoin de récupérer ces informations


    Citation Envoyé par fanzilan Voir le message
    En revanche, je trouve cette solution pas hyper simple au niveau du code, il y a beaucoup de membres en templates.
    Il est vrai qu'il faut "une certaine tournure d'esprit" pour arriver à se sentir à l'aise avec les template. Mais ne t'en fais pas, cela viendra en temps utiles
    Je suis pas un grand expert du C++. Pour être franc, la solution de koala01, je l'aurai pas trouvé tout seul (les static_assert par exemple, et le côté "politique").
    A vrai dire, static_assert (qui permet, comme le nom l'indique, de faire des assertions au moment de la compilation) n'est apparu "qu'en" 2011 avec C++11.

    Cela fait déjà dix ans, nous sommes bien d'accord, mais il y a encore énormément de cours qui n'en parlent absolument pas, et c'est bien dommage
    Du coup je sais pas si rajouter des templates comme ça peut influer sur les performances du code, ou sa lisibilité ou encore sa maintenabilité.
    En termes de performances pures, tu peux te dire que les templates n'auront à tout le moins aucune incidence négatives, vu que tout est évalué à la compilation (donc, bien avant que le programme ne soit lancé, au final )

    Il n'est -- par contre -- pas "tout à fait exclu" que l'exécutable soit, au final, plus rapide pour la raison bien simple que le compilateur -- qui connait la solution final du raisonnement à suivre -- aura sans doute remplacé "tout le raisonnement" permettant d'obtenir la solution par ... la solution elle-même, et qu'elle ne devra donc plus être "calculée" à l'exécution.

    Mais, de toutes manières, tu dois t'intéresser en priorité à l'obtention d'un programme qui fonctionne et qui fournit des résultat corrects et cohérents, car cela ne sert à rien d'avoir un programme rapide si c'est pour ... finir rapidement dans un mur

    Comme disait l'autre
    premature optimization is root for all evil
    Restent les problème de la maintenance et de la lisibilité du code, et là, ben, disons que les avis seront partagés...

    Parce que, d'un coté, nous aurons des gens qui ne s'y retrouvent absolument pas dans les template et qui n'en veulent à aucun prix, et que leur point de vue mérite d'être pris en considération, et, de l'autre, il y aura ceux qui comprennent "assez facilement" le code template que pour arriver à le comprendre, qui argueront du fait que cela nous incite beaucoup plus à respecter le SRP pour dire qu'un (bon) code template facilitera grandement les choses.

    Et, bien sur, il y a toutes les variations possibles entre les deux . Je ne sais pas trop où tu te situes personnellement dans cette mesure. Pour ma part, je suis plutôt du coté de la deuxième solution

    J'ai eu le temps de réfléchir de mon côté et voilà ce que je propose :

    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
     
    #include <iostream>
    #include <vector>
     
    using namespace std;
     
    class Animal{
    public:
        Animal() = default;
        Animal(int a) : mA(a) {};
        ~Animal() = default;
     
        int const & getmA() const
        {
            return mA;
        }
     
    protected:
        int mA;
     
    };
     
    class AnimalWithQueue : public Animal{
    public:
        AnimalWithQueue () = default;
        AnimalWithQueue (int a, int b) : Animal(a), mB(b) {};
        ~AnimalWithQueue () = default;
     
        int const & getmB() const
        {
            return mB;
        }
     
    protected:
        int mB;
     
    };
    Du coup dans une fonction où j'ai besoin de "getter" les classes d'un vecteur par exemple (c'est le cas dans mon code original), je suis obligé de faire un static_cast.
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
     
    void readClass(const vector<Animal*> &vec, int i, Animal* val)
    {
     
        if (i == 0) //Indice où je suis certain d'avoir un Animal
            val = new Animal(*(vec.at(i)));
        if (i == 1) //Indice où je suis certain d'avoir un Animal avec une queue  
            val = new AnimalWithQueue(*(static_cast<AnimalWithQueue*>(vec.at(i))));
    }
    Et dans le main :
    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
     
    int main()
    {
        Animal insta(5);
        AnimalWithQueue instb(6,10);
     
        vector<Animal *> vecC;
        vecC.push_back(&insta);
        vecC.push_back(&instb);
     
        Animal  *readInstA = new Animal ;
        AnimalWithQueue *readInstB = new AnimalWithQueue ;
     
        readClass(vecC, 0, readInstA);
        cout << readInstA << endl;
        readClass(vecC, 1, readInstB);
     
     
        cout << readInstA->getmA() << endl;
        cout << readInstB->getmA() << endl;
        cout << readInstB->getmB() << endl;
     
        delete readInstA;
        delete readInstB;
     
        return 0;
    }
    Non, crois moi, tu ne veux surtout pas devoir travailler de la sorte...

    Tu vas, déjà, commencer par me virer cet odieuse directive using namespace std; que je ne saurais voir dans un code qui date d'après 1998

    Ensuite, tu serais sans doute bien inspiré d'utiliser les pointeurs intelligents (std::unique_ptr ou std::shared_ptr, avec une nette préférence pour std::unique_ptr) pour placer tes pointeurs sur Animal dans ta collection.

    Enfin, tu ne veux surtout pas utiliser de static_cast parce que cela revient à briser l'OCP (Open / Close Principle, deuxième principe SOLID), du moins, de la manière dont tu envisages de le faire ici.
    Mais ce qui m'inquiète, c'est le static_cast...j'ai cru comprendre que ça pouvait être un peu trop bourrin.
    Tout à fait, car, tu l'as dit toi-même
    Dans le code original c'est prévu d'être beaucoup plus complexe
    Et le gros problème, c'est que tu risques très rapidement de devoir suivre ce genre de "logique" à dix, quinze ou vingt endroits de ton code (et peut-être même d'avantage).

    Du coup, si tu décides "un jour" de rajouter une nouvelle classe dérivée, sans doute "simplement" parce que les besoins ont évolué "au fil du temps", ce n'est pas un seul endroit de ton code qu'il faudrait modifier pour prendre cet ajout en compte, mais bien dix, quinze ou vingt (et sans doute d'avantage).

    Or, il y a ... 99.99% de chances pour que tu oublies de modifier au moins un de ces endroits (et peut-être plus) pour toute une série de raisons. Et ca, ca va introduire des bugs et des comportements bizarres dans ton programme .

    La solution pour éviter cela serait d'utiliser le principe connu sous le nom de "double dispatch".

    En gros, on va partir du principe que toute donnée sait parfaitement de quel type réelle elle est, et ce, même si elle "passe pour être" du type de sa classe de base, et que nous pourrons donc utiliser le polymorphisme afin de demander à la donnée en question de se transmettre sous la forme d'une référence (ou d'un pointeur) sur une instance de son type réel à une fonction "clairement définie" (et surchargée avec les différents types réels qui sont utilisables).

    L'une des mises en œuvre les plus connues de cette manière de faire est sans doute le patron de conception visitor.

    Dans le cas présent, nous créerions donc -- par exemple -- d'un coté une classe visiteur qui pourrait être proche de
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    /* il s'agit de "l'interface" de toutes les classes qui seront considérées comme des visiteur.
     * C'est donc une classe abstraite qui ne fait qu'exposer une série de fonctions pour lesquelles nous
     * ne pouvons pas, pour l'instant, fournir de comportement spécifique
     */
    class Visitor{
    public:
        virtual void visit(Cat /* const*/ & ) = 0; // comportement spécifique pour les chats
        virtual void visit(Dog /* const*/ & ) = 0; // comportement spécifique pour les chiens
        void visit(Eagle /* const*/ & ) = 0; // comportement spécifique pour les aigles
        void visit(Human /* const*/ & ) = 0; // comportement spécifique pour les humains
        /* et tous les autres */
    };
    Et, de l'autre, nous ajouterions une fonction (virtuelle pure) à l'interface de notre classe Animal, qui lui permette ... d'accepter "n'importe quel type de visiteur" sous une forme proche de
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    class Animal{
    public:
        virtual void accept(Visitor & ) = 0;
    };
    et que je définirai de la même manière pour chacune des classes concrètes héritant de Animal 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
    class Dog : public AnimalWithTail{
    public :
        void accept(Visitor & v) /* const*/ final override;
    };
    void Dog::accept(Visitor & v) /* const{
        v.visit(*this);
    }
    class Cat : public AnimalWithTail{
    public :
        void accept(Visitor & v) /* const*/ final override;
    };
    void Cat::accept(Visitor & v) /* const{
        v.visit(*this);
    }
    class Human : public Animal{
    public :
        void accept(Visitor & v) /* const*/ final override;
    };
    void Human::accept(Visitor & v) /* const{
        v.visit(*this);
    }
    Et, de cette manière, chaque fois que je voudrai manipuler "des animaux" d'une manière particulière, je "n'aurai qu'à" créer une nouvelle classe dérivée de Visitor et fournir l'implémentation de la fonction accepte "qui va bien" pour les différentes classes concrètes qui existent:
    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
    class Greeter : public Visitor{
    public:
        void visit(Cat const & c) final override{
            std:cout<<"miaouw\n";
        }
        void visit(Dog const & d) final override{
            std:cout<<"Wouah\n";
        }
        void visit(Human const & h) final override{
            std:cout<<"miaouw\n";
        }
        /* et tous les autres */
    };
    /* bien sur, le visiteur a accès à toutes les fonctions publiques de 
     * la classe concrète
     */
    class SomethingDoer : public Visitor{
    public:
     
        void visit(Cat const & c) final override{
            c.doSomeCatSpecificBehaviour();
        }
        void visit(Dog const & d) final override{
            d.doSomeDogSpecificBehaviour();
        }
        void visit(Human const & h) final override{
            h.doSomeHumaSpecificBehaviour();
        }
        /* et tous les autres */
    };
    L'énorme avantage de cette méthode, c'est que, du coup, lorsque tu "a été assez bête" pour perdre le type réel de tes objets, tu n'as plus qu'à créer une instance d'un visiteur concret (ex: Greeter) et à l'utiliser avec tous les objets dont tu disposes, vu qu'il adaptera la fonction visit au type réel de l'objet:
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    void doSomething(std::vector<Animal *> const & vec){ // j'aurais vraiment préféré std::vector<std::unique_ptr<Animal>> ;-)
        Greeter greet;
        for(auto * an : vec)
            an->accept(greet);
    }
    De plus, si tu en viens -- un jour -- à devoir ajouter une classe Squale ou une classe Tortuga, si tu oublies de définir la fonction accept pour l'une de ces classes, le compilateur t'engueulera lorsque tu essayeras d'en créer une instance au motif que "Squale (ou Tortuga) est une classe abstraite" pour te rappeler que la fonction accept est toujours virtuelle pure.

    Et, de même, si tu ne rajoutes pas la fonction virtuelle (pure) visit(Squale /* const*/ &) (ou la fonction virtuelle (pure) visit(Tortuga /* const*/ &) ) à la classe Visiteur, le compilateur t'engueulera au motif que "il n'y a aucune fonction visit(Squale /* const */ &) dans la classe Visiteur lorsque tu feras appel à an->accept(monVisiteurConcret);.

    Enfin, et ce n'est pas le moindre, si tu viens à oublier de rajouter l'implémentation spécifique de la fonction visit(Squale /* const*/ &) pour l'un des visiteurs concrets qui existent, le compilateur se plaindra ... de nouveau que ce visiteur particulier est une classe abstraite, et qu'il ne peut donc pas en créer d'instance.

    Au final, le compilateur sera donc ton meilleur allié car il t'empêchera d'oublier (pour une raison ou une autre) de faire les "ajouts nécessaires" (et n'ayant aucun impact sur le code existant) qui te permettront d'ajouter une nouvelle classe.

    C'est cool, non
    A méditer: La solution la plus simple est toujours la moins compliquée
    Ce qui se conçoit bien s'énonce clairement, et les mots pour le dire vous viennent aisément. Nicolas Boileau
    Compiler Gcc sous windows avec MinGW
    Coder efficacement en C++ : dans les bacs le 17 février 2014
    mon tout nouveau blog

  7. #7
    Membre averti
    Homme Profil pro
    Ingénieur Etudes
    Inscrit en
    Juillet 2010
    Messages
    54
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Haute Garonne (Midi Pyrénées)

    Informations professionnelles :
    Activité : Ingénieur Etudes

    Informations forums :
    Inscription : Juillet 2010
    Messages : 54
    Par défaut
    Un grand merci à toi koala ! Un véritable cours, j'ai appris pas mal de choses !

    Sinon tu m'as au final présenté deux façons de faire, une avec template et une sans.
    J'ai l'impression que la 2e méthode, avec le visiteur, est plus simple à implémenter. Où est le piège ?

    Sinon oui, j'utilise les unique_ptr dans mon code. Mais je veux pas non plus oublier les réflexes des pointeurs à l'ancienne, car il n'est pas 100% certain que je puisse faire du C++14 (avec les fameux make_unique) ou pire encore, du C++11...ça ne dépend pas de moi.

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

Discussions similaires

  1. problème éditeur design Visual studio
    Par xx_FiFty_xx dans le forum Windows Forms
    Réponses: 3
    Dernier message: 10/06/2008, 13h21
  2. problème mode design Asp.net[2.0]
    Par dev-man dans le forum ASP.NET
    Réponses: 6
    Dernier message: 07/02/2007, 14h05
  3. [GRIDBAGLAYOUT] problème entre design et execution
    Par Lambrosx dans le forum NetBeans
    Réponses: 5
    Dernier message: 04/12/2006, 15h35
  4. Problème de design !
    Par franck.thibault dans le forum Balisage (X)HTML et validation W3C
    Réponses: 1
    Dernier message: 25/08/2006, 14h23
  5. Problème de design
    Par b4u dans le forum C++
    Réponses: 4
    Dernier message: 10/03/2006, 00h50

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