Liskov me dit, si B hérite de A (B est un A), quelque soit P(A) vrai, alors P(B) vrai.En quoi RTTI viole-t-il LSP
Soit P(x) = typeid(x) == A;
P(A) est vrai.
P(B) est faux.
CQFD .
Liskov me dit, si B hérite de A (B est un A), quelque soit P(A) vrai, alors P(B) vrai.En quoi RTTI viole-t-il LSP
Soit P(x) = typeid(x) == A;
P(A) est vrai.
P(B) est faux.
CQFD .
Je traduit ta propriété P par P(x) = "x est un A". (C'est ce que dit le typeid )
Soit a un A et b un B, on suppose que tout B est un A (hypothèse du principe).
On a P(a) vraie par définiton, d'autre part on a b est un B, or tout B est un A, donc b est un A (transitivité de "est un"), donc P(b) est vraie. CQFD
En traduisant ta propriété P hors du contexte C++ je trouve que ca marche mieux.
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
le LSP est un principe de substitution basé sur le comportement d'un objet (behavioral subtyping) et pas sur sa nature.
typeid(x) renseigne la nature de "x", et pas son comportement.
On pourrait imaginer avoir deux classes A et B qui n'héritent pas l'une de l'autre mais qui sont substituables (meme méthodes). Ce n'est pas possible en C++ a cause du typage statique, mais ça c'est juste une limitation du langage de programmation et pas du concept objet.
ALGORITHME (n.m.): Méthode complexe de résolution d'un problème simple.
LSP dit "quelque soit". Changer ma propriété P pour en trouver une similaire mais qui t'arrange, ça n'est pas très honnête . Je dis justeJe traduit ta propriété P par P(x) = "x est un A". (C'est ce que dit le typeid )
Donc oui, je fais quelque chose de stupide, et je n'ai aucune honte à le dire. Mais ceci uniquement dans le but d'ébranler des certitudes.vouloir appliquer le LSP de manière trop stricte conduit à des absurdités (le RTTI dont typeid viole le LSP, par exemple).
Et puis, fondamentalement, typeid me permet vraiment d'écrire explicitement du code qui casse la substituabilité (test sur le type --> exception si pas le bon typeid, je casse toute substituabilité). Contrairement au changement de visibilité en C++ .
Tu as fait la première étape du raisonnement. Te reste à faire la deuxième, et tu seras de mon avis (càd, si changer la visibilité n'empêche pas la substituabilité (c'est le cas en C++, mais ça ne serait probablement pas le cas en python, par exemple), ça ne viole pas le LSP).
ALGORITHME (n.m.): Méthode complexe de résolution d'un problème simple.
Le problème, c'est que tu confond un comportement et son résultat.
Le comportement de typeid est "récupérer un identifiant unique permettant de savoir à quel type on a affaire".
Et tu remarquera que ce comportement est valide pour n'importe quel type de donnée, que la substituabilité soit envisagée ou non
Par contre, c'est le résultat du comportement qui te permet de déterminer ta logique, et non le comportement en lui-même.
Or, le principe même du polymorphisme est de permettre d'adapter un comportement en fonction du type auquel on a réellement affaire.
Le fait que le résultat d'un comportement te permette ou non de considérer deux types comme étant substituables n'intervient donc absolument pas dans la décision.
S'il en était autrement, LSP ne serait jamais qu'un principe inapplicable, car tu ne pourrais en aucun cas estimer que la redéfinition d'une fonction dans un type dérivé permet malgré tout de le respecter, et donc, le fondement même de l'héritage serait une pure ineptie, et le modèle (ou les modèles) objet(s) s'effondrerai(en)t "comme un château de carte"
Je le répète: Il n'y a aucune raison de prendre le langage utilisé en compte au moment de déterminer si LSP est respecté (par contre, il peut être intéressant de le prendre en compte tout de suite après ) : il n'est jamais fait référence au moindre langage de programmation dans les différents énoncés du principe !!!Tu as fait la première étape du raisonnement. Te reste à faire la deuxième, et tu seras de mon avis (càd, si changer la visibilité n'empêche pas la substituabilité (c'est le cas en C++, mais ça ne serait probablement pas le cas en python, par exemple), ça ne viole pas le LSP).
Comme je l'ai dit, LSP doit être un "Go / No go" qui doit être pris en compte avant même de s'intéresser à ce que peut autoriser ou interdire le langage.
Tu te plains de ne pas pouvoir travailler "hors contexte", mais tu dois veiller à te poser les bonnes questions au bon moment:
Le contexte dans lequel tu dois t'intéresser au respect de LSP est le contexte dans lequel ton analyse des besoins est terminée et où tu commence à te poser la question des types dont tu va avoir besoin pour rencontrer ces besoins.
A l'extrême limite, tu pourrais presque te direet revenir une semaine plus tard sur tes notes (parce que les décideurs se sont enfin mis d'accord sur le langage utiliser ), et contiuer ta conception en des termes proches deBon, je n'ai pas encore déterminé le langage que je vais utiliser (différents décideurs sur le sujet ne se sont pas encore mis d'accord ), mais je sais que, pour répondre aux besoins qui sont mis en évidence, il me faudra le type A et le type B qui sont finalement particulièrement proche.
Est il sensé d'envisager de faire hériter B de A
Ben, si LSP est respecté, je peux l'envisager, autrement, je ne peux pas
Selon tes notes, si LSP est respecté, la réponse seraBon, j'avais déterminé qu'il me fallait le type A et le typ B...
Ces deux types sont forts proches, il serait peut être intéressant d'envisager l'héritage, parce que je suis dans une situation dans laquelle je peux encore l'envisager (ex: il n'y a pas encore d'autre héritage si le langage n'autorise pas l'héritage multiple)
Qu'avais-je dis au sujet de cette possibilité
autrement, elle sera du genre deOK... j'avais déterminé que LSP était respecté, je fais donc hériter B de A
Je suis d'accord que cette approche est presque caricaturale des problèmes auxquels on peut être confronté avec les décideurs, et qu'elle n'est pas flatteuse pour ces derniers, mais tu t'es peut être déjà retrouvé dans une situation similaireOuppsss... je m'étais dis que LSP n'était pas respecté, je dois donc trouver une autre solution (sans doute basée sur une agrégation ou sur la possibilité de convertir l'un en l'autre)
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
ALGORITHME (n.m.): Méthode complexe de résolution d'un problème simple.
Ah, il est clair que le contexte de base est une conception OO, mais cela me semblait aller de soi: vouloir appliquer un principe de conception OO alors que l'on n'utilise pas le paradigme en question semble quelque peux compliqué
Mais comme, en règle général, il est possibles de trouver plusieurs langage appliquant un paradigme donné (les langages multi paradigmes étant finalement l'exception ), le choix du langage sera oritenté enté en fonction du (éventuellement des) paradigme(s) que l'on veut utiliser, et non l'inverse
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
L'intérêt du LSP, c'est que si B est-un A au sens LSP, tout le code que j'ai écrit pour A reste valide pour B. Je ne parle pas du code de A, ni de celui de B, mais bien du code de tous les clients de A (codes qui utilisent un A).S'il en était autrement, LSP ne serait jamais qu'un principe inapplicable, car tu ne pourrais en aucun cas estimer que la redéfinition d'une fonction dans un type dérivé permet malgré tout de le respecter, et donc, le fondement même de l'héritage serait une pure ineptie, et le modèle (ou les modèles) objet(s) s'effondrerai(en)t "comme un château de carte"
Avec typeid et C++, je peux écrire du code pour lequel ce n'est pas le cas. (ce faisant, je casse la substituabilité de mes objets). Avec le changement de visiblité et C++, je ne peux pas (je ne casse pas la substituabilité de mes objets).
Mais tu dis que le second mécanisme viole le LSP, et pas le premier ? Ça ne te semble pas plutôt l'inverse ?
Ben, qu'ai-je écrit "S'il en était autrement (comprend: si la redéfinition d'un comportement dans la classe dérivée venait à rendre ce comportement invalide pour la classe dérivée), LSP serait un principe inaplicable, parce que tu ne pourrais jamais redéfinir un comportement de la classe de base dans la classe dérivée.
Autrement dit, le fait qu'un comportement soit polymorphe serait en désaccord avec LSP, et donc, tu ne pourrais envisager LSP que pour des héritages dans lesquels le résultat d'un comportement de la classe dérivée serait strictement identique au résultat obtenu par la classe de base.
Tu avouera que cela n'a strictement aucun sens
Si ce n'est que c'est le résultat de typeid qui est différent.Avec typeid et C++, je peux écrire du code pour lequel ce n'est pas le cas. (ce faisant, je casse la substituabilité de mes objets). Avec le changement de visiblité et C++, je ne peux pas (je ne casse pas la substituabilité de mes objets).
Cela revient exactement au même que si tu décidais que la fonction virtuelle foo d'un type dérivé devait renvoyer une exception parce que l'on ne peut pas utiliser cette fonction foo dans le cas du type dérivé:
Un code proche de
ne rend absolument pas le comportement addChildren invalide, bien que le résultat du comportement soit différent selon que l'on exécute addChildren au départ d'une instance de Leaf ou d'une instance de Node (toutes deux passant pour être... une instance de Base).
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23 class Base { public: virtual void addChildren() = 0; } class Leaf : public Base { public: virtual void addChildren(Base *) { throw CanNotHaveChildren(); } }; class Node : public Base { public: virtual void addChidren(Base * b) { children_.push_back(b); } private: std::vector<Base*> children_; };
Pour typeid, le principe reste exactement similaire: le comportement reste tout à fait valide, mais le résultat du comportement est différent.
Et comme tu utilise le résultat du comportement pour décider de la logique à suivre, il est tout à fait normal que tu rentre dans une branche de différente de la logique
Mais comme le comportement (et non le résultat du comportement) reste parfaitement valide, tu n'est absolument pas en désaccord avec LSP.
D'ailleurs, on remarque que le comportement reste tout à fait valide y compris pour les types non substituables (n'intervenant absolument pas dans la même hiérarchie de classe).
Quels sont les comportements dont tu parlesMais tu dis que le second mécanisme viole le LSP, et pas le premier ? Ça ne te semble pas plutôt l'inverse ?
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
!!?
heu... si ! Le comportement est invalide, du moins en terme de conception orienté objet. Une classe dérivée décrit une spécialisation du comportement, elle ne peut pas faire "moins de choses" que la classe mère !
Dans ton exemple, tu utilises le "mécanisme" d'héritage pour faire autre chose que de la spécialisation de comportement. On n'est donc plus dans les concepts orienté objet, et donc le principe du LSP ne s'applique pas.
ALGORITHME (n.m.): Méthode complexe de résolution d'un problème simple.
A condition que le comportement de la classe mère soit déjà défini:
Dans l'exemple que je donne, la le comportement addChildren ne fait absolument rien, alors, comment veux tu que n'importe quelle redéfinition en fasse moins
Justement, non, je spécialise un comportement donné (le fait d'essayer d'ajouter un enfant) en fonction de ce qu'il est cohérent d'accepterDans ton exemple, tu utilises le "mécanisme" d'héritage pour faire autre chose que de la spécialisation de comportement. On n'est donc plus dans les concepts orienté objet, et donc le principe du LSP ne s'applique pas.
Et cela te démontre bien que le résultat peut être totalement différents (simplement impossible à définir pour la classe de base parce qu'elle ne dispose pas des informations permettant de le définir, lancement d'une exception dans une classe dérivée parce que cela n'a puremet et simplement pas de sens d'accepter d'ajouter un enfant à quelque chose qui est connu comme étant une feuille, et carrément accepté pour le type dérivé qui... autorise le fait de définir un enfant).
Nous sommes tout à fait dans le cadre de LSP parce que l'appel d'un comportement est valide aussi bien pour la classe de base que pour les classes dérivées, bien que le résultat de ce comportement soit clairement différent en fonction du type réellement manipulé.
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
Je ne change pas ta propriété, je l'abstrait juste du langage.
(Norme, on est bien dans le cas de type polymorphique), donc la propriété P est bel et bien "le type dynamique de x est A". Or le LSP ne distingue pas type dynamique et statique, le seul type considéré est le type réel, ie dynamique (cf plusieurs messages de Koala plus tôt dans la discution), donc la propriété P est "x est de type A", et appliqué a un objet de type B avec "tout B est un A", on obtient que P est valide pour tout B. Je ne vois pas de malhonnêteté dans mon raisonnement. (Je démontre juste que la propriété P que tu exhibes n'invalide pas le LSP quelque soit les types A et B, avec B fille de A).When typeid is applied to a glvalue expression whose type is a polymorphic class type (10.3), the result refers
to a std::type_info object representing the type of the most derived object (1.8) (that is, the dynamic
type) to which the glvalue refers.
Le fait est que tu t'intéresse au résultat d'un comportement, qui, manque de bol, renvoit un résultat unique permettant de déterminer de manière unique le type le plus dérivé.
Le résultat du comportement est donc, effectivement, différent quel que soit le type réel envisagé, mais le comportement reste, quant à à lui, clairement identique.
Il n'y a donc strictement aucune différence entre ce que je te présente avec une fonction virtuelle: le comportement reste tout à fait similaire, même si le résultat est fondamentalement différent .
LSP est donc parfaitement respecté, ne serait-ce que parce que cette fonction sort à la limite totalement du contexte même de la substituabilité:
Même si cela n'a pas énormément de sens, tu pourrais parfaitement envisager d'invoquer typeid sur une classe qui n'entre dans aucune hiérarchie de classe:
n'a, il faut bien l'avouer, pas énormément de sens mais est tout à fait valide
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11 Class A { }; void foo() { A *a=new A; if(typeid(*a)) { /* n'importe quoi */ } }
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
J'ai du mal a voir ce que tu veus dire koala, l'énoncé dit "toutes propriétés valide sur un A l'est sur un B", un propriété sur un bojet est une application qui a un objet associe une valeur booléen. Donc P(x) = "typeid(x) == typeid(A)", est bel est bien une propriété.
Là où je ne suis pas d'accord avec white_tentacle, c'est qu'il utilise le contexte du C++ pour vérifier cette propriété, fonctionnement du C++ qui "invalide" la propriété . Mais si on abstrait la propriété (P(x) = "x est un A"), alors il n'y a aucun problème.
Il me semble que c'est l'idée qu'exprimait white_tentacle. L'appel à foo n'est plus valide (assertion) si l'objet est en réalité un B, donc à fortiori le comportement est clairement différent.
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9 struct A { virtual void foo() { assert(typeid(*this) == typeid(A)); std::cout << 0; } virtual ~A(){} }; struct B : A {};
NB: Ce code n'a aucun utilité pratique AMA, il ne sert qu'à illustrer, un peu comme ton code dans le message précédent
Tu remarqueras que tes deux arguments sont un peu contradictoires.Justement, non, je spécialise un comportement donné (le fait d'essayer d'ajouter un enfant) en fonction de ce qu'il est cohérent d'accepter
Soit la classe de base définit bel et bien un comportement "ajouter un enfant", soit elle ne définit pas un comportement et alors cette méthode ne devrait pas être publique, et donc on n'est plus dans un concept objet.
L'idée de définir une signature de méthode "a priori" sans définir son comportement, ce n'est pas trop de l'objet. D'autant plus si classe dérivée n'implémente pas ce comportement et qu'on se retrouve obligé de lever une exception quand on appelle la méthode :/
ALGORITHME (n.m.): Méthode complexe de résolution d'un problème simple.
Si ce n'est que typeid n'est absolument pas une propriété des différents type: c'est une fonction libre qui récupère "quelque chose "(que l'on pourrait effectivement considérer comme étant une propriété) qui permet d'identifier le type réellement utilisé de manière unique.
Une propriété pour un type donné ne peut absolument pas être indépendante du type en question, qu'elle se retrouve ou non dans les types parent, et qui doit se retrouver dans les types dérivés.
Or, typeid est totalement indépendant de l'objet qu'on lui passe en paramètre.
LL'appel reste valide: tu n'a absolument aucune erreur à la compilation ni même à l'exécution lors de l'appel de typeid.à où je ne suis pas d'accord avec white_tentacle, c'est qu'il utilise le contexte du C++ pour vérifier cette propriété, fonctionnement du C++ qui "invalide" la propriété . Mais si on abstrait la propriété (P(x) = "x est un A"), alors il n'y a aucun problème.
Il me semble que c'est l'idée qu'exprimait white_tentacle. L'appel à foo n'est plus valide (assertion) si l'objet est en réalité un B, donc à fortiori le comportement est clairement différent.
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9 struct A { virtual void foo() { assert(typeid(*this) == typeid(A)); std::cout << 0; } virtual ~A(){} }; struct B : A {};
Ce qui se passe, c'est que if(typeid(*this)==typeid(A) ne compare absolument pas le comportement (qui reste identique qu'il s'agisse de l'invoquer en passant *this ou en passant A), mais compare le résultat de ce comportement pour décider de la logique à suivre.
Si donc, même, on considère le fait de pouvoir appeler typeid sur une instance d'objet ou sur un type comme étant une propriété, il faut se rendre contre que cette propriété est totalement valide, quel que soit le type passé en paramètre.
Or comme il est carrément possible (bien que nous soyons d'accord pour dire que cela n'a pas vraiment de sens ) d'invoquer cette propriété sur un objet qui n'intervient même pas *forcément* dans le contexte de l'héritage, on se rend compte que cette propriété ne peut absolument pas être mise en relation avec le concept de substituabilité, et n'a donc absolument rien à voir avec le respect (ou non) de LSP.
Nous sommes bien d'accord là dessusNB: Ce code n'a aucun utilité pratique AMA, il ne sert qu'à illustrer, un peu comme ton code dans le message précédent
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
Absolument pas...
Tu remarque que je ne définis absoluement aucun comportement pour la classe Base...
C'est d'ailleurs pour cela que, si le compilateur accepte que je manipule une référence ou un pointeur sur Base, il n'acceptera absolument pas que je crée une instance de Base sans autre précision
PourquoiSoit la classe de base définit bel et bien un comportement "ajouter un enfant", soit elle ne définit pas un comportement et alors cette méthode ne devrait pas être publique, et donc on n'est plus dans un concept objet.
Le fait d'être en mesure d'invoquer un comportement qui essaye d'ajouter un enfant est une propriété qui est littéralement valide pour n'importe quel objet passant pour être de type Base.
Le fait que les comportements soient adaptés en fonction du type réel n'invalide absolument pas la propriété associée
PourquoiL'idée de définir une signature de méthode "a priori" sans définir son comportement, ce n'est pas trop de l'objet. D'autant plus si classe dérivée n'implémente pas ce comportement et qu'on se retrouve obligé de lever une exception quand on appelle la méthode :/
Dois-je te rappeler que l'exemple que je donne est l'exemple type d'un design pattern connu
Le fait de lancer une exception est "un moyen comme un autre" d'assurer la remontée d'erreur, même si, nous sommes bien d'accord, l'utilisation qui sera faite de cette hiérarchie devrait faire en sorte d'éviter de se placer dans une situation dans laquelle l'exception sera lancée.
Mais, si on considère que la propriété "est susceptible de permettre l'ajout d'un enfant" est valide pour "n'importe quel objet passant pour être du type Base", le fait qu'il n'y ait pas de comportement clairement défini pour Base et que le comportement occasionne un résultat différent pour Leaf que pour Node ne viole absolument pas LSP: le comportement associé à cette propriété est valide, même si le résultat est différent parce qu'adapté au type réellement manipulé.
Encore une fois, il ne faut pas confondre le comportement associé à une propriété et le résultat de ce comportement
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
Je ne confond pas les deux (cf post #104) et je suis d'accord avec ton jugement sur l'exemple avec typeid.
C'est bien le problème. Une classe, meme avec des methodes abstraites/virtuelles doit définir un comportement. Sinon ce n'est plus une conception d'objet, c'est juste une liste de signature de fonctions.Tu remarque que je ne définis absoluement aucun comportement pour la classe Base...
Non. On n'invoque pas un comportement. On invoque une méthode. Et le fait d'être en mesure d'invoquer une méthode ca ne valide rien du tout, a part que le code compile correctement.Le fait d'être en mesure d'invoquer un comportement qui essaye d'ajouter un enfant est une propriété qui est littéralement valide pour n'importe quel objet passant pour être de type Base.
Le fait que l'implémentation de la méthode respecte le "contrat" du comportement de la classe, ça oui, ca valide le comportement.
Et ce que dit le LSP, c'est qu'un comportement existant ne doit pas être supprimé lors d'un héritage. On peut éventuellement ajouter ou enrichir les comportements, mais pas les supprimer.
Le fait qu'en C++ (java, c#) on hérite automatique des méthodes publiques, ca implique alors que les méthodes publiques ne doivent être utilisées uniquement que pour décrire des comportements. C'est d'ailleurs une bonne pratique en Java/C# de passer par la définition d'interface pour spécifier des comportements et de les faire hériter à la classe.
Mais comme je l'ai dit, tout cela n'a de sens que si on reste dans un model de conception orienté objet. Si on utilise le "mécanisme" d'héritage pour ce qui est somme toute du templating ou de la réutilisation de code, tout cela ne s'applique pas. Et ton exemple de base/node/leaf entre a mon sens dans cette catégorie.
ALGORITHME (n.m.): Méthode complexe de résolution d'un problème simple.
Vous avez un bloqueur de publicités installé.
Le Club Developpez.com n'affiche que des publicités IT, discrètes et non intrusives.
Afin que nous puissions continuer à vous fournir gratuitement du contenu de qualité, merci de nous soutenir en désactivant votre bloqueur de publicités sur Developpez.com.
Partager