Partage
  • Partager sur Facebook
  • Partager sur Twitter

Le "L" de SOLID

Coontravariance des argument / covariance du type de retour

Sujet résolu
    23 août 2019 à 17:12:58

    Bonjour a tous ,

    Mercredi prochain je dois présenter les principe SOLID et Demeter a mon boulot.
    J'aimerais avoir des precision sur 2 petit point sur la substitution de Liskov.

    Il est dit :

    Le principe de Liskov impose des restrictions sur les signatures sur la définition des sous-types :

    • Contravariance des arguments de méthode dans le sous-type.
    • Covariance du type de retour dans le sous-type.
    • Aucune nouvelle exception ne doit être générée par la méthode du sous-type, sauf si celles-ci sont elles-mêmes des sous-types des exceptions levées par la méthode du supertype.


    Pour le dernier point c'est assez clair , cependant pour les deux autres, même si je sais faire des classes substituable, je ne suis pas sur de completement comprendre ce qu'ils veulent dire ( surtout la contravariance des arguments ).

    Généralement je m'assure de :
    - Meme type de retour
    - Memes arguments de methodes
    - Si besoin d'autre variable, c'est au constructeur que je delegue

    Si c'est possible de m'expliquer dans vos mot et si possible un exemple simple en code pour illustrer.

    Merci d'avance !

    -
    Edité par CrevetteMagique 23 août 2019 à 17:18:57

    • Partager sur Facebook
    • Partager sur Twitter
      23 août 2019 à 19:04:18

      Bonsoir,

      << Je suis un débutant en POO, alors si je dit une erreur, ... ne frapper pas trop fort! >>

      Pour moi je comprends le LSP, comme ça: Tu ne fais hérite une classe d'une autre que si tu peux mettre un lien "Est un" entre les deux classes. (la classe héritée "est un" la classe de base).

      En ce sens, toutes les "propriétés" de la classe de base, doivent être remplis par la classe héritée. Par propriétés, je penses aux fonctions publiques et interfaces des fonctions membres, mais aussi les invariants/pre/post conditions.

      << C'est pas une trop grosse erreur ? >>

      Bien cordialement.

      -
      Edité par Dedeun 23 août 2019 à 19:05:16

      • Partager sur Facebook
      • Partager sur Twitter
        23 août 2019 à 22:40:01

        Salut,

        Pourrais tu m'indiquer où tu as trouvé cette description de Liskov?

        Car, sans dire qu'elle est fausse, il faut avouer que ce n'est pas la description la plus classique (ni la plus simple)...

        De plus, cette description laisse "un gout trop peu", car elle semble au minimum incomplète : comme elle parle de restrictions, il aurait été au moins poli d'indiquer lesquelles :D

        Pour répondre à ta question, disons que la covariance représente, pour une fonction redéfinie dans une classe dérivée, la possibilité de renvoyer un type "plus spécialisé" que le type de la donnée renvoyée par cette fonction dans le type de base.

        C'est -- pour te donner un exemple précis -- ce que l'on fait généralement lorsque l'on créer une fonction "clone":

        #include <iostream>
        /* tu sais bien sur qu'en C++, la seule différence entre
         * le mot clé struct et le mot clé classe tient dans
         * l'accessibilité appliquée par défaut 
         */
        struct Base{
            virtual Base* clone() const{
                return new Base;
            }
            virtual void howami() const{
                std::cout<<"I'm a Base\n";
            }
        };
        struct Derived : Base{
            Derived * clone() const override{
                return new Derived;
            }
            virtual void howami() const{
                std::cout<<"I'm a Derived\n";
            }
        };
        int main(){
            Base b;
            Base * cloneB = b.clone();
            Derived d;
            Base * cloneD = d.clone();
            cloneB->howami(); //I'm a Base
            cloneD->howami();  //I'm a Derived
            Derived * other = d.clone(); // Ca marche aussi
            delete cloneB;
            delete cloneD;
            delete other;
        }

        Cette possiblité pourrait aussi parfaitement s'adapter à une fabrique:

        Base * createBase(){
            return new Base;
        }
        Derived * createDerived(){
            return new Derived;
        }
        /* on crée des base ou des dérivées selon le besoin*/

        Elle est particulièrement utile lorsque tu t'arrange pour ne pas perdre "tout de suite" le type réel de l'élément créé; par exemple, si, dans le cas de la fabrique, tu souhaite traiter "pendant un certain temps" tes objets de type dérivé comme ... étant du type Derived (au lieu de les considérer comme étant du type Base).

        La contravariance est illégale dans l'approche orientée objets du C++, mais, dans le principe, elle permettrait de redéfinir une fonction qui s'attend à recevoir un objet du type de base dans une classe dérivée en lui transmettant explicitement un objet de type dérivé, par exemple
        struct Truc{
            virtual void foo(Base /* const */ &);
        };
        
        struct Machin : Truc{
            /* !!! NE COMPILERA PAS !!! */
            void foo(Derived /* const*/ &) override;
        }
        Par contre, on peut obtenir un résultat très similaire (et légal !!! ) avec les templates:
        template <typnename T>
        struct Truc{
            void foo(T /* const */ & t){
                /* ...*/
            }
        };
        int main(){
            Base b;
            Derived d;
            Truc<Base>tb;
            tb.foo(b); // d'office de type Base
            tb.foo(d); // d est considéré comme étant de type Base
            Truc<Derived> td;
            td.foo(d); // d est considéré comme étant du type Dérived;
        }
        Une description bien plus facile à comprendre est celle donnée par wikipedia: Après avoir exprimé le principe

        Si q ( x ) est une propriété démontrable pour tout objet x de type T, alors q ( y ) est vraie pour tout objet y de type S tel que S est un sous-type deT .

        Wikipedia évalue les possibilités qui nous sont offertes du point de vue de la programmation par contrat en nous expliquant que:

        • les préconditions ne peuvent pas être renforcées dans la sous classe par rapport à la classe de base.  C'est la raison pour laquelle Carre (précondition : longueur == largeur) ne peut pas dériver de Rectangle (précondition: aucune)
        • les postconditinos ne peuvent pas être affaiblies dans la sous classe: un aurait pu croire intéressant de faire hériter Rectangle de carré, mais non, on ne peut pas le faire
        • Tous les invariants de la classe de base doivent être respectés dans la sous classe; par exemple:
        1. l'accessibilité des fonctions et des données doit être respecté (tu ne peux pas avoir une fonction publique dans la classe de base et protégée dans la classe dérivée, ni l'inverse)
        2. les fonctions et les données qui existent dans la classe de base doivent exister dans la classe dérivée
        3. si une fonction est constante (ou non)  dans la classe de base, elle doit être constante (ou non) dans la classe dérivée
        4. si tu t'arrange pour ne pas permettre la modification d'une donnée dan la classe de base, tu ne peux pas permettre à la classe dérivée de la modifier

        <mode autopromo="ON"> Au fait, sais tu qu'il existe un excellent bouquin qui traite de SOLID, et de bien d'autres choses encore ??? :D </mode>

        • Partager sur Facebook
        • Partager sur Twitter
        Ce qui se conçoit bien s'énonce clairement. Et les mots pour le dire viennent aisément.Mon nouveau livre : Coder efficacement - Bonnes pratiques et erreurs  à éviter (en C++)Avant de faire ce que tu ne pourras défaire, penses à tout ce que tu ne pourras plus faire une fois que tu l'auras fait
          24 août 2019 à 12:22:11

          Lu'!

          Le LSP est une règle dérivée de la règle de conséquence en logique de Hoare. La règle en question est la suivante :

          APre ==> WPre     { WPre } command { SPost }     SPost ==> APost
          ----------------------------------------------------------------
                            { APre } command { APost }

          Elle te dit en gros que si tu as un moment dans un programme une commande qui a certaines pre et post conditions (APre, APost), mais que ta commande peut fonctionner avec des pré-conditions plus faibles (WPre, qui est plus faible car APre ==> WPre) et fournir une post-condition plus fortes (SPost, qui est plus forte car SPost ==> APost), alors tu as le droit de faire ce raisonnement pour montrer que la commande respecte bien les conditions d'origine (APre, Apost).

          La subsitution de Liskov va quelque part un peu plus loin disant que si dans le programme d'origine tu as une commande C qui a des conditions APre, Apost, tu peux remplacer cette commande C par une commande C2 si elle respecte les mêmes propriétés que celle que j'ai expliquée au dessus.

          Modulo le fait que le typage de C++ n'est pas assez riche pour exprimer toutes ces propriétés, c'est ce que doivent exprimer la contravariance des arguments (affaiblissement de la précondition) et la covariance du type de retour (renforcement de la post-condition). En l'occurrence, ça veut aussi dire que le typage de C++ est insuffisant pour respecter le LSP et ne permet même pas toutes les libertés que le LSP est censé pouvoir offrir :) .

          -
          Edité par Ksass`Peuk 24 août 2019 à 16:54:56

          • Partager sur Facebook
          • Partager sur Twitter

          Posez vos questions ou discutez informatique, sur le Discord NaN | Tuto : Preuve de programmes C

            26 août 2019 à 16:57:15

            Merci a tous pour vos reponses !

            Pourrais tu m'indiquer où tu as trouvé cette description de Liskov?


            Wikipedia lol.

            Donc si je comprend bien , quand ils parlent de covariance du type de retour et de contravariance des parametre (celui la est encore flou ) , on peut se rapporter au LSP lui meme -> Les types doivent etre exactement les meme OU substituable ( c'est generalement la regle que je suis pour respecter le LSP ) ?

            -
            Edité par CrevetteMagique 26 août 2019 à 16:57:58

            • Partager sur Facebook
            • Partager sur Twitter
              26 août 2019 à 18:13:43

              Zérotisme a écrit:

              Donc si je comprend bien , quand ils parlent de covariance du type de retour et de contravariance des parametre (celui la est encore flou ) , on peut se rapporter au LSP lui meme

              Ce n'est pas que l'on peut, c'est que l'on doit se rapporter au LSP lui-même.  C'est particulièrement clair lorsque l'on se place du point de vue de la programmation par contrat : en termes de PpC qu'est ce qu'une valeur de retour, si ce n'est l'une des postconditions majeures de la fonction ?

              Car, de deux choses l'une: ou bien la fonction est en mesure de renvoyer une valeur du type indiqué, et le contrat est respecté, ou bien la fonction n'est pas en éta de renvoyer une valeur du type indiqué, et le contrat n'est pas respecté.

              Comme le dit si bien wikipedia, une postcondition ne peut pas être affaiblie (au niveau du type dérivé) par rapport au type de base, mais cela nous laisse toute latitude pour ... raffermir cette postcondition si on y trouve un intérêt quelconque, non ?

              Si j'ai d'un coté une hiérarchie de classes proche de

              class Base{
                  /* tout ce qu'il faut pour assurer la sémantique d'entité */
              };
              class Derivee : public Base{
              
              };

              et que d'un autre coté, j'ai une hiérarchie de classe proche de

              class AbstractCreator{
              public:
                   virtual Base * create();
              };
              class ConcreteCreator : public AbstractCreator{
                   Derivee * create() override;
              };

              tous les éléments renvoyé par le fonction create peuvent passer pour "être du type de base", que nous les recevions à partir de la version de base de la fonction create ou à partir de sa version "redéfinie".

              Grâce à la possibilité de fournir un objet de type Derivee partout où un objet de type Base est attendu, nous voyons bien qu'il n'y a aucune raison d'empêcher la version redéfinie d'une fonction de renvoyer un type "plus précis" que celui sensément renvoyé par la version "de base" de la fonction en question ;)

              • Partager sur Facebook
              • Partager sur Twitter
              Ce qui se conçoit bien s'énonce clairement. Et les mots pour le dire viennent aisément.Mon nouveau livre : Coder efficacement - Bonnes pratiques et erreurs  à éviter (en C++)Avant de faire ce que tu ne pourras défaire, penses à tout ce que tu ne pourras plus faire une fois que tu l'auras fait
                26 août 2019 à 18:29:31

                koala01 a écrit:

                Zérotisme a écrit:

                Donc si je comprend bien , quand ils parlent de covariance du type de retour et de contravariance des parametre (celui la est encore flou ) , on peut se rapporter au LSP lui meme

                Ce n'est pas que l'on peut, c'est que l'on doit se rapporter au LSP lui-même.  C'est particulièrement clair lorsque l'on se place du point de vue de la programmation par contrat : en termes de PpC qu'est ce qu'une valeur de retour, si ce n'est l'une des postconditions majeures de la fonction ?

                Car, de deux choses l'une: ou bien la fonction est en mesure de renvoyer une valeur du type indiqué, et le contrat est respecté, ou bien la fonction n'est pas en éta de renvoyer une valeur du type indiqué, et le contrat n'est pas respecté.

                Comme le dit si bien wikipedia, une postcondition ne peut pas être affaiblie (au niveau du type dérivé) par rapport au type de base, mais cela nous laisse toute latitude pour ... raffermir cette postcondition si on y trouve un intérêt quelconque, non ?

                Si j'ai d'un coté une hiérarchie de classes proche de

                class Base{
                    /* tout ce qu'il faut pour assurer la sémantique d'entité */
                };
                class Derivee : public Base{
                
                };

                et que d'un autre coté, j'ai une hiérarchie de classe proche de

                class AbstractCreator{
                public:
                     virtual Base * create();
                };
                class ConcreteCreator : public AbstractCreator{
                     Derivee * create() override;
                };

                tous les éléments renvoyé par le fonction create peuvent passer pour "être du type de base", que nous les recevions à partir de la version de base de la fonction create ou à partir de sa version "redéfinie".

                Grâce à la possibilité de fournir un objet de type Derivee partout où un objet de type Base est attendu, nous voyons bien qu'il n'y a aucune raison d'empêcher la version redéfinie d'une fonction de renvoyer un type "plus précis" que celui sensément renvoyé par la version "de base" de la fonction en question ;)


                Merci c'est beaucoup plus clair ^^
                • Partager sur Facebook
                • Partager sur Twitter

                Le "L" de SOLID

                × Après avoir cliqué sur "Répondre" vous serez invité à vous connecter pour que votre message soit publié.
                × Attention, ce sujet est très ancien. Le déterrer n'est pas forcément approprié. Nous te conseillons de créer un nouveau sujet pour poser ta question.
                • Editeur
                • Markdown