Vous êtes nouveau sur Developpez.com ? Créez votre compte ou connectez-vous afin de pouvoir participer !

Vous devez avoir un compte Developpez.com et être connecté pour pouvoir participer aux discussions.

Vous n'avez pas encore de compte Developpez.com ? Créez-en un en quelques instants, c'est entièrement gratuit !

Si vous disposez déjà d'un compte et qu'il est bien activé, connectez-vous à l'aide du formulaire ci-dessous.

Identifiez-vous
Identifiant
Mot de passe
Mot de passe oublié ?
Créer un compte

L'inscription est gratuite et ne vous prendra que quelques instants !

Je m'inscris !

Le changement de visibilité d'une fonction virtuelle est-il un viol du LSP ?

Le , par white_tentacle

0PARTAGES

0  0 
[Edit 3DArchi]
Suite à cette question : Accès à une méthode virtuelle protégée ou privée, un débat s'est ouvert pour savoir si le changement de visibilité d'une fonction virtuelle dans une classe dérivée est un viol du LSP.
Un des énoncés du LSP (ou Principe de Substituion de Liskov introduit par Barbara Liskov en 1987) peut être le suivant (cf ici ou ici):

Si pour chaque objet o1 de type S, il y a un objet o2 de type T tel que pour tous les programmes P définis avec le type T, le comportement de P reste inchangé lorsque o1 est substitué à o2 alors S est un sous-type de T.
La question initiale portait sur le changement de visibilité d'une fonction virtuelle dans la classe dérivée par rapport à la classe de base. Les intervenants s'accordent de reconnaitre que cette pratique est peu justifiable d'un point de vue conception, donc le débat ne porte pas la dessus.

La question posée dans cette discussion est :
Le changement de visibilité d'une fonction virtuelle est-il un viol du LSP ?

[/Edit]


Citation Envoyé par koala01 Voir le message
Le fait de redéfinir une fonction virtuelle en changeant sa visibilité est, surtout, une transgression grave du principe de subsitution de Liskov
En fait, non, j'ai beau retourner dans tous les sens, mais je ne vois pas en quoi ça viole le LSP, si on s'en tient à une approche objet.

Après, si on rajoute les génériques, ça devient différent. Mais comme ça sera résolu à la compilation, le LSP a moins de sens. C'est un peu bizarre d'avoir B polymorphique à A, tout en étant d'un concept (au sens c++1x) différent, mais j'ai du mal à trouver une bonne raison de l'empêcher (à peu près autant de mal qu'à y trouver un intérêt).

Une erreur dans cette actualité ? Signalez-le nous !

Avatar de JolyLoic
Rédacteur/Modérateur https://www.developpez.com
Le 04/08/2010 à 2:32
Citation Envoyé par koala01 Voir le message
Même pâs... Java et d'autres permettent de définir l'accessibilité des fonctions membres.
De manière assez différente, quand même. En particulier, en C++ accessibilité et l'héritage sont assez décorrélés. Essaye d'avoir une fonction privée virtuelle en C#, ce n'est pas possible. Donc le pattern de conception NVI est possible en C++ (et je l'emploie quasi systématiquement, et regrette généralement ensuite le "quasi", mais impossible dans C# (et probablement Java, mais je connais moins). Alors tu peux dire qu'il s'agit de détails de conception, et non de conception générale, mais tout de même...
Citation Envoyé par koala01 Voir le message

Mais on concevra de manière identique pour n'importe quel langage orienté objet: une conception pour C++ peut être utilisée pour java mais risque de nécessiter certains aménagement du fait des restrictions imposées par ce dernier.
Je suis assez fortement en désaccord là dessus. Comme le dit Coplien dans "Multiparadigm design for C++", il y a plusieurs façon possibles d'effectuer l'analyse de commonalités et de variabilité de l'espace du problème, et on peut trouver plusieurs jeux de dimensions différents, incompatibles, et a priori aussi valables les uns que les autres. Sauf qu'au moment de l'analyse de l'espace de la solution, certains choix vont être plus ou moins aisément mettables en œuvre dans le langage cible, et donc il recommande d'avoir une bonne idée de ces choix lors de l'analyse de l'espace du problème, afin de choisir le jeu de dimensions le plus adapté.

Si j'essaye d'être plus concret, en Java 1.4 ou en C#1.0, la bonne manière de designer des collections était un super-objet. Et il serait absurde si l'on sait que l'on doit travailler dans ces langages de faire une analyse à la C++ du problème (détermination des concepts auxquels doivent répondre les objets pour être membre d'un conteneur) pour finalement dire qu'avec des restrictions de ces langages, l'analyse ne sert à rien, puisque de toute façon, on n'a dans ces langages aucun outil qui manipule ces concepts. Il ne s'agit pas de dire que ces langages implémentent la même chose, avec des détails dont il faudra ne tenir compte que dans une phase ultérieure, mais que ces langages ont outils qui permettent de mettre en place des conceptions de nature fondamentalement différentes.

Citation Envoyé par koala01 Voir le message

Le DP composite, qui est la base des graphes, sera le même pour C++ que pour C# ou java.
Très bon exemple (j'aurais plutôt dit arbres que graphes, mais là n'est pas la question) ! En effet, un design en C++ va justement s'efforcer de s'éloigner de ce DP, pour revenir aux fondamentaux mathématiques de cette structure d'arbre, afin de fournir des algorithmes qui soient génériques, en se basant sur la notion de concept (et pas du tout sur des classes de base, ou de la POO). C'est ce que fait boost::graph pour les graphes, en s'abstrayant de savoir si on a une liste de nœud liés, une liste de liens vers d'autres nœuds, un tableau d'adjacence... Un tel design n'aurait pas trop de sens en C#.
Citation Envoyé par koala01 Voir le message

Quand tu vois une fonction protégée ou privée, tu t'attends à ne pas pouvoir l'appeler d'une autre manière que... depuis une fonction membre de la classe (ou depuis une fonction membre des classes dérivées pour les fonctions protégées).
Ou depuis une fonction membre de la classe de base si cette fonction est virtuelle. J'insiste car c'est je pense le plus courant dans mes programmes. Il est très rare qu'une fonction virtuelle et privée soit appelée par une autre fonction de la même classe. Le plus souvent, une telle fonction dans le cadre du NVI est appelée depuis la fonction publique de la classe de base.
Citation Envoyé par koala01 Voir le message

Or, le fait de disposer de la fonction en accessibilité publique dans une classe de base retire justement cette restriction pourtant importante en permettant d'invoquer la fonction depuis... n'importe où, y compris depuis des fonctions qui ne sont absolument pas membres d'une classe intervenant dans la hiérarchie de classe.
C'est là que je suis en désaccord. On ne peut pas l'appeler depuis n'importe où. On peut l'appeler depuis les endroits où l'on sait que la classe dérive de la classe de base qui défini cette fonction publique.

Si une classe A implémente les interfaces I1 et I2, que I1 défini en publique une fonction virtuelle (ce qui est probablement un mauvais choix, dans le cadre d'un langage qui permet le NVI, mais un mauvais choix au niveau de la conception de I1. Si I1 est défini dans une bibliothèque, lorsque je fais la conception de A et I2, je dois vivre avec ce choix, et trouver la meilleure conception possible dans ce périmètre), mais que je souhaite que les gens qui manipulent directement A n'aient pas accès à la fonction, la définir privée dans la classe dérivée me semble un bon choix de conception.

D'autres langages définissent même des fonctionnalités spécifiques pour mettre ça en place, là où en C++ les règles générales marchent, ce qui semble vouloir dire que ce concept n'est pas si mauvais. Je pense par exemple au C# où il est possible de dire qu'une classe va implémenter explicitement une interface, c'est à dire qu'elle va redéfinir les fonctions (forcément publiques) de cette interface, sans pour autant que ces fonctions soient disponibles pour un utilisateur de la classe qui ne passerait pas par l'interface. Pour qu'un langage soit allé jusqu'à prévoir une syntaxe spécifiquement pour, on peut imaginer que le besoin existe, non ?
1  0 
Avatar de white_tentacle
Membre émérite https://www.developpez.com
Le 08/10/2010 à 16:15
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"
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).

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 ?
1  0 
Avatar de white_tentacle
Membre émérite https://www.developpez.com
Le 12/10/2010 à 9:04
LSP ne s'intéresse absolument pas en particulier à la programmation par contrat:

Tu peux respecter LSP sans respecter les règles de la programmation par contrat, car rien ne t'oblige à l'utiliser.


C'est bien la première fois que j'entends défendre ça...

http://en.wikipedia.org/wiki/Liskov_substitution_principle

In the same paper, Liskov and Wing detailed their notion of behavioral subtyping in an extension of Hoare logic, which bears a certain resemblance with Bertrand Meyer's Design by Contract in that it considers the interaction of subtyping with pre- and postconditions.
Et la page en français est tout aussi explicite :

http://fr.wikipedia.org/wiki/Principe_de_substitution_de_Liskovhttp://fr.wikipedia.org/wiki/Princip...tion_de_Liskov

Et pour repartir sur la définition :

Let q(x) be a property provable about objects x of type T. Then q(y) should be true for objects y of type S where S is a subtype of T.
Il n'y a pas de limite à q. Ca peut être n'importe quoi (et notamment, typeid et Liskov ne font pas bon ménage, ce qui est cohérent, alors qu'instanceof (comme en C#, par exemple), ne pose pas de problème).
1  0 
Avatar de Emmanuel Deloget
Expert confirmé https://www.developpez.com
Le 24/11/2010 à 11:48
Citation Envoyé par pseudocode Voir le message
"What is wanted here is something like the following substitution property: If
for each object o1 of type S there is an object o2 of type T such that for all
programs P defined in terms of T, the behavior of P is unchanged when o1 is
substituted for o2 then S is a subtype of T."

(Barbara Liskov, “Data Abstraction and Hierarchy", 1988)

Restons pragmatique et ne donnons pas au LSP des super-pouvoirs qu'il ne devrait pas avoir.
Si Liskov était si fière de cette définition de 1988, elle n'en aurait pas proposé une autre 6 ans plus tard Notamment, cette de 1994 est beaucoup plus stricte que celle de 1988, qui ne se base que sur le comportement substitué (comportement, au sens contrat). La définition usuelle courante parle de propriétés - dont, grosse différence, le comportement ne fait pas partie. Ces propriétés, je les ai cité dans un post précédent, je les liste à nouveau ici :

* accès
* pré- et post-conditions

L'accès impose bien évidemment l'existence.

LSP a effectivement des super-pouvoirs: grâce à ce principe, on sait si une classe peut dériver d'une autre. C'est quand même un point particulièrement important dans le domaine de l'architecture logicielle

Outre ces points; il semblerait que certains d'entre vous continuent de mélanger "ce qu'il est possible de faire" et "ce qui doit être fait", en étant fortement influencé par les langages majeurs de notre génération. Les deux notions sont très différentes - il suffit d'installer eMule pour commencer à télécharger des films, donc c'est tout à fait possible. Est-ce que c'est ce qui doit être fait (au regard de la loi; vos opinions personnelles ont peu d'intérêt dans ce cas précis ).

Ce qui doit être fait, c'est respecter LSP de manière correcte, y compris les notions d'accès, pré-condition et post-condition. Ce qui peut être fait, c'est passer outre certaines restrictions de LSP en se servant de certaines faiblesses des langages usuels.

La pratique est plus permissive que la théorie, mais ça ne veut pas dire que la théorie accepte la pratique. Ce qui ne veut pas dire, en retour, que la pratique est condamnable : il y a des cas légitimes ou je peux souhaiter par exemple que toutes les fonctions de ma classe dérivée soit privées - Je peux l'instancier, mais je ne peux pas utiliser ses services. Je ne peux pas non plus l'étendre en la dérivant. J'impose une limitation technique, afin de contrôler au mieux mon design. C'est, en soi, une bonne chose. Ca reste une violation du LSP, mais (comme je l'ai dit auparavent) on conçoit des systèmes en s'appuyant sur des langages, et on utilise les possibilités de ces langages à notre avantage.

Comme tout principe, comme toute règle, le LSP a une certaine souplesse. De la même manière qu'on peut violer une règle en motivant cette violation, LSP accepte d'être violé à condition que le résultat ait un intérêt fort. Il n'en reste pas moins violé, mais au moins, on sait pourquoi on le fait, et ce que ça nous apporte.

Pour résumer : ce n'est pas parce que le pragmatisme s'éloigne un poil de la théorie que le pragmatisme a tord ou que la théorie a besoin d'être étendu. Comme le dit le célèbre adage, la différence entre la théorie et la pratique, c'est qu'en théorie, c'est la même chose.
1  0 
Avatar de Flob90
Membre expert https://www.developpez.com
Le 22/07/2010 à 21:44
Si tu réduis la visibilité il est clair que u violes le LSP, le LSP dit que toute propriété valide sur une mère l'est sur une fille, si tu réduis l'interface, tu rends invalide ceci (la propriété : "peut envoyer le message xxx" n'est pas valide).
Par contre si tu l'augmentes, ca ne l'invalide pas. Mais ca reste assez illogique de le faire, une méthode privé virtuel permet de ne définir qu'un comportement. Si tu passes de privé (ou protégé) à publique, tu choisies délibérement de faire de cette fonction un élément de l'interface alors que le concepteur de la classe mère avait déterminé qu'elle ne devait pas en faire partie (peut-etre pour respecter le SRP ?).
J'ai du mal à voir ce qui pourrait motiver le choix de faire d'un élément qui ne fait que définir un comportement, un élément de l'interface.
0  0 
Avatar de koala01
Expert éminent sénior https://www.developpez.com
Le 23/07/2010 à 0:19
Comme j'arrive un peu tard, je ne peux qu'appuyer les écrits de Flob90...

La base du raisonnement de Liskov, c'est que toute propriété (comprend: la capacité de recevoir, d'émettre ou de répondre (à) un message donné) valide pour la classe de base doit être valide pour toute classe dérivée, de manière directe ou indirecte.

Cela sous entend que si la classe A a une propriété XXX qui est valide pour A, la propriété devra aussi être valide pour toute classe qui dérive de A, mais aussi pour toute classe qui dérive d'une classe qui dérive d'une (on pourrait continuer 106 ans comme cela) classe qui dérive de A.

Si tu réduis la visibilité d'une fonction prévue dans l'interface publique de la classe mère, tu essaye d'invalider la propriété en question: Tu ne peux en effet pas envisager sereinement de placer une instance de la classe dérivée dans une collection manipulant (des pointeurs sur) la classe de base, car tu cours le risque que l'utilisateur fasse appel, en toute bonne fois, à la propriété en question.

A l'inverse, si une fonction est protégée ou privée dans la classe de base, elle n'est là que pour faciliter le travail des autres fonctions membres de la classe, voire le travail des fonctions membres des classes dérivées.

Elle n'est donc qu'à "usage interne" en quelque sorte, et fait littéralement partie de ce que l'on pourrait appeler les "détails d'implémentation".

Il n'y a donc aucune raison pour exposer cette fonction au tout publique dans les classes dérivées
0  0 
Avatar de white_tentacle
Membre émérite https://www.developpez.com
Le 23/07/2010 à 7:38
Citation Envoyé par koala01 Voir le message
Si tu réduis la visibilité d'une fonction prévue dans l'interface publique de la classe mère, tu essaye d'invalider la propriété en question: Tu ne peux en effet pas envisager sereinement de placer une instance de la classe dérivée dans une collection manipulant (des pointeurs sur) la classe de base, car tu cours le risque que l'utilisateur fasse appel, en toute bonne fois, à la propriété en question.
Sauf que la fonction est toujours appelable via l'interface de A, et que donc le LSP n'est pas malmené de ce point de vue. Tu peux tout à fait le mettre dans une collection, car l'appel à la propriété en question fonctionnera parfaitement (même s'il ne fonctionnerait plus aprés downcast).

Non, le seul cas qui pose problème par rapport au LSP, c'est l'utilisation de B comme paramètre d'une classe template. Mais fondamentalement, je ne crois pas que le LSP ait été prévu pour ça, ça sera détecté à la cmnpilation donc ça ne me semble pas un souci autre mesure.
0  0 
Avatar de koala01
Expert éminent sénior https://www.developpez.com
Le 23/07/2010 à 7:53
Citation Envoyé par white_tentacle Voir le message
Sauf que la fonction est toujours appelable via l'interface de A, et que donc le LSP n'est pas malmené de ce point de vue. Tu peux tout à fait le mettre dans une collection, car l'appel à la propriété en question fonctionnera parfaitement (même s'il ne fonctionnerait plus aprés downcast).
C'est justement parce que la fonction est accessible publiquement depuis l'interface de A que LSP est malmené...

Vu que la fonction ne devrait pas être accessible publiquement pour la classe dérivée.

Comprend moi: Si tu n'a une vision que de la classe dérivée, en tant qu'utilisateur, tu ne vas jamais tenter d'appeler une fonction protégée ou privée depuis l'extérieur, vu que tu t'attend à avoir un message proche de "foo is private / protected in this context".

A l'inverse, si tu n'a qu'une vision de la classe de base, toujours en tant qu'utilisateur de celle-ci, tu envisagera sans problème d'appeler la fonction, vu qu'elle est... publique, et fait donc partie de l'interface dont tu dispose pour interagir avec ton objet.

Quel que soit le sens du point de vue où tu te place, tu as donc une incohérence du seul fait que tu peux faire quelque chose d'un coté que tu ne peux pas faire de l'autre.

LSP n'est donc, purement et simplement, pas respecté.

En gros, l'héritage (public) peut ajouter des possibilités à l'interface de base, mais pas en supprimer
0  0 
Avatar de white_tentacle
Membre émérite https://www.developpez.com
Le 23/07/2010 à 9:43
Citation Envoyé par koala01 Voir le message
C'est justement parce que la fonction est accessible publiquement depuis l'interface de A que LSP est malmené...
Pas vraiment, non. B reste substituable à A, tant que l'on considère que c'est un A. Ca donne une situation un peu étrange, où une instance de B est toujours substituable à une instance de A, mais où le type B n'est plus substituable au type A (d'où le fait que ça ne marche pas pour les génériques).

Comprend moi: Si tu n'a une vision que de la classe dérivée, en tant qu'utilisateur, tu ne vas jamais tenter d'appeler une fonction protégée ou privée depuis l'extérieur, vu que tu t'attend à avoir un message proche de "foo is private / protected in this context".
Tout à fait d'accord.

A l'inverse, si tu n'a qu'une vision de la classe de base, toujours en tant qu'utilisateur de celle-ci, tu envisagera sans problème d'appeler la fonction, vu qu'elle est... publique, et fait donc partie de l'interface dont tu dispose pour interagir avec ton objet.
Là aussi je suis d'accord.

Quel que soit le sens du point de vue où tu te place, tu as donc une incohérence du seul fait que tu peux faire quelque chose d'un coté que tu ne peux pas faire de l'autre.
Si, je peux des deux côtés. Ce que tu occultes, c'est que l'interface d'une classe ne se restreint pas à ce qu'elle définit, mais aussi à tout ce dont elle hérite. Et donc, si j'ai la chose suivante :

Code : Sélectionner tout
1
2
3
4
5
6
7
8
9
class A {
public:
  virtual void f();
};
class B: public A {
protected:
  virtual void f();
}
L'interface de B contient toujours ce public A::f() (la preuve : on peut l'appeler ).

LSP n'est donc, purement et simplement, pas respecté.
LSP est parfaitement respecté sur les instances des objets. Le problème se pose pour les types eux-mêmes, mais le LSP ne s'intéresse qu'aux instances.

En gros, l'héritage (public) peut ajouter des possibilités à l'interface de base, mais pas en supprimer
Il ne la supprime pas vraiment, puisqu'elle reste disponible si tu utilises l'interface de base. Partout où tu utilises une instace de A, tu peux utiliser une instance de B à la place. C'est en ce sens là que pour moi, le LSP n'est pas malmené.

Par contre, je n'ai toujours pas compris à quoi ça peut servir
0  0 
Avatar de 3DArchi
Rédacteur https://www.developpez.com
Le 23/07/2010 à 12:28
Salut,
Citation Envoyé par white_tentacle Voir le message
Pas vraiment, non. B reste substituable à A, tant que l'on considère que c'est un A. Ca donne une situation un peu étrange, où une instance de B est toujours substituable à une instance de A, mais où le type B n'est plus substituable au type A (d'où le fait que ça ne marche pas pour les génériques).
J'ai tendance à penser comme toi. C'est bien la différence entre une instance substituable à une autre dans un expression qui reste valide mais la substitution de type non.
En passant de l'énoncé théorique à sa pratique concrète dans une expression C++, cela implique qu'on parle d'une expression dans laquelle l'instance de A est présente par référence ou pointeur permettant d'avoir toujours le même type statique (A) et un type dynamique différent (B) devant supporter le LSP.
Si l'instance de A est présente par valeur, il faudrait mieux préciser ce que l'on entend par substituable : soit cela implique une copie de B en A (beurk) donc rien n'a vraiment été substitué dans l'expression, soit cela implique que l'expression reste valide avec un type B au quel cas cela n'est plus substituable, soit cela ne concerne pas les instances dont les types statiques et dynamiques ne peuvent être différents ce que j'ai tendance à penser (soit je mélange tout ).
Ceci dit, c'est un choix du C++ d'avoir indiqué que le contrôle d'accès à une fonction virtuelle était fait au moment de la compilation sur le type statique. Si demain on choisissait de faire un contrôle à l'exécution sur le type dynamique (peut être cela existe-t-il dans d'autres langages objets ?), alors le LSP ne pourrait plus être respecté si on change la visibilité.

Citation Envoyé par white_tentacle Voir le message

L'interface de B contient toujours ce public A::f() (la preuve : on peut l'appeler ).
Que dans une expression utilisant A comme type statique :
Code : Sélectionner tout
1
2
3
4
5
6
A a;
a.f(); // ok
B b;
b.f(); // erreur
A &rb = b;
rb.f(); // ok
Citation Envoyé par white_tentacle Voir le message
Par contre, je n'ai toujours pas compris à quoi ça peut servir
Moi non plus . Si on peut légitimement penser que LSP est respecté, je pense qu'on peut tout aussi légitimement penser qu'il y a (au moins dans 90% des cas) un problème de conception si on commence à changer la visibilité d'une fonction virtuelle dans les classes dérivées.
0  0