Le principe de substitution de Liskov est-il incompatible avec l'introspection ou le typage de canard?

11

Dois-je bien comprendre que le principe de substitution de Liskov ne peut pas être observé dans des langues où les objets peuvent s’inspecter eux-mêmes, comme ce qui est habituel dans les langues de type canard?

Par exemple, dans Ruby, si une classe Bhérite d'une classe A, alors pour chaque objet xde A, x.classva revenir A, mais si xest un objet de B, x.classne va pas revenir A.

Voici une déclaration de LSP:

Que q (x) soit une propriété démontrable sur les objets x de type T . Ensuite , q (y) doit être prouvable pour les objets y de type S , où S est un sous - type de T .

Ainsi, en Ruby, par exemple,

class T; end
class S < T; end

violer LSP sous cette forme, comme en témoigne la propriété q (x) =x.class.name == 'T'


Une addition. Si la réponse est "oui" (LSP incompatible avec l'introspection), alors mon autre question serait: existe-t-il une forme modifiée "faible" de LSP qui peut éventuellement tenir pour un langage dynamique, éventuellement sous certaines conditions supplémentaires et avec seulement des types spéciaux des propriétés .


Mettre à jour. Pour référence, voici une autre formulation de LSP que j'ai trouvée sur le web:

Les fonctions qui utilisent des pointeurs ou des références à des classes de base doivent pouvoir utiliser des objets de classes dérivées sans le savoir.

Et un autre:

Si S est un sous-type déclaré de T, les objets de type S devraient se comporter comme les objets de type T devraient se comporter, s'ils sont traités comme des objets de type T.

Le dernier est annoté avec:

Notez que le LSP concerne le comportement attendu des objets. On ne peut suivre le LSP que si l'on sait clairement quel est le comportement attendu des objets.

Cela semble plus faible que l'original et pourrait être possible à observer, mais j'aimerais le voir officialisé, en particulier expliqué qui décide du comportement attendu.

Le LSP n'est-il donc pas une propriété d'une paire de classes dans un langage de programmation, mais d'une paire de classes avec un ensemble donné de propriétés, satisfaites par la classe ancêtre? Concrètement, cela signifierait-il que pour construire une sous-classe (classe descendante) respectant LSP, toutes les utilisations possibles de la classe ancêtre doivent être connues? Selon LSP, la classe ancêtre est censée être remplaçable par n'importe quelle classe descendante, non?


Mettre à jour. J'ai déjà accepté la réponse, mais je voudrais ajouter un autre exemple concret de Ruby pour illustrer la question. En Ruby, chaque classe est un module dans le sens où la Classclasse est un descendant de Moduleclasse. Pourtant:

class C; end
C.is_a?(Module) # => true
C.class # => Class
Class.superclass # => Module

module M; end
M.class # => Module

o = Object.new

o.extend(M) # ok
o.extend(C) # => TypeError: wrong argument type Class (expected Module)
Alexey
la source
2
Presque toutes les langues modernes offrent un certain degré d'introspection, donc la question n'est pas vraiment spécifique à Ruby.
Joachim Sauer
Je comprends, j'ai donné Ruby comme exemple. Je ne sais pas, peut-être que dans certaines autres langues avec introspection il y a des "formes faibles" de LSP, mais, si j'ai bien compris le principe, il est incompatible avec l'introspection.
Alexey
J'ai supprimé "Ruby" du titre.
Alexey
2
La réponse courte est qu'ils sont compatibles. Voici un article de blog avec lequel je suis principalement d'accord: Principe de substitution de Liskov pour la
saisie de
2
@Alexey Les propriétés dans ce contexte sont des invariants d'un objet. Par exemple, les objets immuables ont la propriété que leurs valeurs ne changent pas. Si vous regardez de bons tests unitaires, ils devraient tester exactement ces propriétés.
K.Steff

Réponses:

29

Voici le principe réel :

Soit q(x)une propriété prouvable sur les objets xde type T. Cela q(y)devrait alors être prouvable pour les objets yde type SSest un sous-type de T.

Et l'excellent résumé de wikipedia :

Il indique que, dans un programme informatique, si S est un sous-type de T, les objets de type T peuvent être remplacés par des objets de type S (c'est-à-dire que les objets de type S peuvent être remplacés par des objets de type T) sans altérer les propriétés souhaitables de ce programme (exactitude, tâche effectuée, etc.).

Et quelques citations pertinentes du document:

Ce qu'il faut, c'est une exigence plus forte qui contraint le comportement des sous-types: les propriétés qui peuvent être prouvées en utilisant la spécification du type présumé d'un objet devraient tenir même si l'objet est en fait membre d'un sous-type de ce type ...

Une spécification de type comprend les informations suivantes:
- le nom du type;
- Une description de l'espace des valeurs du type;
- Pour chacune des méthodes du type:
--- Son nom;
--- Sa signature (y compris les exceptions signalées);
--- Son comportement en termes de pré-conditions et post-conditions.

Passons donc à la question:

Dois-je bien comprendre que le principe de substitution de Liskov ne peut pas être observé dans des langues où les objets peuvent s’inspecter eux-mêmes, comme ce qui est habituel dans les langues de type canard?

Non.

A.classrenvoie une classe.
B.classrenvoie une classe.

Étant donné que vous pouvez effectuer le même appel sur le type le plus spécifique et obtenir un résultat compatible, LSP est valide. Le problème est qu'avec les langages dynamiques, vous pouvez toujours appeler des choses sur le résultat en vous attendant à ce qu'elles soient là.

Mais considérons un langage typé structurellement (canard). Dans ce cas, A.classretournerait un type avec une contrainte qu'il doit être Aou un sous-type de A. Ceci fournit la garantie statique que tout sous - type de Adoit fournir une méthode T.classdont le résultat est un type qui satisfait cette contrainte.

Cela fournit une affirmation plus forte que LSP détient dans les langues qui prennent en charge la frappe de canard, et que toute violation de LSP dans quelque chose comme Ruby se produit davantage en raison d'une mauvaise utilisation dynamique normale que d'une incompatibilité de conception de langue.

Telastyn
la source
1
"Puisque vous pouvez faire le même appel sur le type le plus spécifique et obtenir un résultat compatible, LSP est valable". LSP tient si les résultats sont identiques, si j'ai bien compris. Peut-être qu'il peut y avoir une forme "hebdomadaire" de LSP par rapport à des contraintes données, exigeant au lieu de toutes les propriétés que seules les contraintes données soient satisfaites. En tout cas, j'apprécierais toute référence.
Alexey
@Alexey a été modifié pour inclure ce que signifie LSP. Si je peux utiliser B là où j'attends A, alors LSP est valable. Je suis curieux de savoir comment vous pensez que la classe de Ruby viole cela.
Telastyn
3
@Alexey - Si votre programme contient fail unless x.foo == 42et qu'un sous-type renvoie 0, c'est la même chose. Ce n'est pas un échec de LSP, c'est le fonctionnement normal de votre programme. Le polymorphisme n'est pas une violation de LSP.
Telastyn
1
@Alexey - Bien sûr. Supposons que c'est une propriété. Dans ce cas, votre code viole LSP car il ne permet pas aux sous-types d'avoir le même comportement sémantique. Mais ce n'est pas particulièrement spécial pour les langages dynamiques ou typés canard. Il n'y a rien qu'ils fassent dans leur conception de langage qui provoque la violation. Le code que vous avez écrit le fait. N'oubliez pas que le LSP est un principe de conception de programme (d'où le devrait dans la définition) plutôt qu'une propriété mathématique des programmes.
Telastyn
6
@Alexey: si vous écrivez quelque chose qui dépend de x.class == A, alors c'est votre code qui viole LSP , pas la langue. Il est possible d'écrire du code qui viole LSP dans presque tous les langages de programmation.
Andres F.
7

Dans le contexte du LSP, une "propriété" est quelque chose qui peut être observé sur un type (ou un objet). Il s'agit en particulier d'une "propriété prouvable".

Une telle "propriété" pourrait exister une foo()méthode qui n'a pas de valeur de retour (et suit le contrat fixé dans sa documentation).

Assurez-vous de ne pas confondre ce terme avec "propriété" car dans " classest une propriété de chaque objet dans Ruby". Une telle "propriété" peut être une "propriété LSP", mais ce n'est pas automatiquement la même chose!

Maintenant, la réponse à vos questions dépend beaucoup de la rigueur avec laquelle vous définissez la «propriété». Si vous dites « la propriété de la classe Aest que .classrenverra le type de l'objet », alors Beffectivement fait avoir cette propriété.

Si, cependant, vous définissez la "propriété" comme " .classretours A", alors évidemment, elle Bn'a pas cette propriété.

Cependant, la deuxième définition n'est pas très utile, car vous avez essentiellement trouvé un moyen détourné de déclarer une constante.

Joachim Sauer
la source
Je ne peux penser qu'à une définition d'une "propriété" d'un programme: pour une entrée donnée, il renvoie une valeur donnée, ou, plus généralement, lorsqu'il est utilisé comme bloc dans un autre programme, cet autre programme pour une entrée donnée retournera un valeurs données. Avec cette définition, je ne vois pas ce que cela signifie que " .classva retourner le type de l'objet". Si cela signifie que x.class == x.classce n'est pas une propriété intéressante.
Alexey
1
@Alexey: J'ai mis à jour ma question avec une clarification sur ce que signifie "propriété" dans le contexte du LSP.
Joachim Sauer
2
@Alexey: en examinant l'article, je ne trouve pas de définition ou de "propriété" spécifique. C'est probablement parce que le terme est utilisé dans le sens général de CS "quelque chose qui peut être observé / prouvé sur un objet". Cela n'a rien à voir avec l'autre signifiant "un champ d'un objet".
Joachim Sauer
4
@Alexey: Je ne sais pas quoi dire de plus. J'utilise la définition de "une propriété est une certaine qualité ou attribution d'un objet". la "couleur" est une propriété d'un objet physique visible. la "densité" est une propriété d'un matériau. "avoir une méthode spécifiée" est une propriété d'une classe / d'un objet.
Joachim Sauer
4
@Alexey: Je pense que vous jetez bébé avec l'eau du bain: Ce n'est pas parce que pour certaines propriétés que le LSP ne peut pas être tenu qu'il est inutile ou "ne tient pas dans n'importe quelle langue". Mais cette discussion irait loin ici.
Joachim Sauer
5

Si je comprends bien, il n'y a rien sur l'introspection qui serait incompatible avec le LSP. Fondamentalement, tant qu'un objet prend en charge les mêmes méthodes qu'un autre, les deux doivent être interchangeables. Autrement dit, si votre code attend un Addressobjet, il n'a pas d' importance si elle est un CustomerAddressou une WarehouseAddress, tant que les deux fournissent (par exemple) getStreetAddress(), getCityName(), getRegion()et getPostalCode(). Vous pouvez certainement créer une sorte de décorateur qui prend un type d'objet différent et utilise l'introspection pour fournir les méthodes requises (par exemple, une DestinationAddressclasse qui prend un Shipmentobjet et présente l'adresse de livraison en tant que Address), mais ce n'est pas obligatoire et certainement pas 'empêche pas le LSP d'être appliqué.

TMN
la source
2
@Alexey: Les objets sont "les mêmes" s'ils prennent en charge les mêmes méthodes. Cela signifie le même nom, le même nombre et le même type d'arguments, le même type de retour et les mêmes effets secondaires (tant qu'ils sont visibles pour le code appelant). Les méthodes peuvent se comporter complètement différemment, mais tant qu'elles respectent le contrat, c'est OK.
TMN
1
@Alexey: mais pourquoi aurais-je un tel contrat? Quelle utilisation réelle ce contrat remplit-il? Si j'avais un tel contrat, je pourrais simplement remplacer chaque occurrence de x.class.nameavec 'A' , ce qui le x.class.name rendrait inutile .
Joachim Sauer
1
@Alexey: encore une fois: ce n'est pas parce que vous pouvez définir un contrat qui ne peut pas être exécuté en étendant une autre classe que LSP est rompu. Cela signifie simplement que vous avez créé une classe non extensible. Si je définis une méthode pour «retourner si le bloc de code fourni se termine dans un temps fini », j'ai également un contrat qui ne peut pas être respecté. Cela ne signifie pas que la programmation est inutile.
Joachim Sauer
2
@Alexey essaie de déterminer s'il x.class.name == 'A'y a un anti-modèle dans la frappe de canard: après tout, la frappe de canard vient de "Si elle se casse et marche comme un canard, c'est un canard". Donc, s'il se comporte Aet respecte les contrats qui ont Aété proposés, c'est un Aexemple
K.Steff
1
@Alexey On vous a présenté des définitions claires. La classe d'un objet ne fait pas partie de son comportement, de son contrat ou de ce que vous voulez appeler. Vous équivoquez "propriété" avec "champ d'objet, tel que x.myField", qui, comme cela a été souligné, n'est PAS le même. Dans ce contexte, une propriété ressemble plus à une propriété mathématique , comme les invariants de type. De plus, c'est un anti-modèle pour vérifier le type exact si vous voulez taper du canard. Alors, quel est votre problème avec le LSP et la frappe de canard, encore une fois? ;)
Andres F.
4

Après avoir parcouru l'article original de Barbara Liskov, j'ai trouvé comment compléter la définition de Wikipedia afin que LSP puisse en effet être satisfait dans presque toutes les langues.

Tout d'abord, le mot "prouvable" est important dans la définition. Elle n'est pas expliquée dans l'article de Wikipédia, et les "contraintes" sont mentionnées ailleurs sans s'y référer.

Voici la première citation importante de l'article:

Ce qu'il faut, c'est une exigence plus forte qui contraint le comportement des sous-types: les propriétés qui peuvent être prouvées en utilisant la spécification du type présumé d'un objet devraient tenir même si l'objet est en fait membre d'un sous-type de ce type ...

Et voici le second, expliquant ce qu'est une spécification de type :

Une spécification de type comprend les informations suivantes:

  • Le nom du type;
  • Une description de l'espace des valeurs du type;
  • Pour chacune des méthodes du type:
    • Son nom;
    • Sa signature (y compris les exceptions signalées);
    • Son comportement en termes de pré-conditions et post-conditions.

Ainsi, LSP n'a de sens que par rapport à des spécifications de type données , et pour une spécification de type appropriée (pour la vide par exemple), il peut être satisfait dans probablement n'importe quelle langue.

Je considère que la réponse de Telastyn est la plus proche de ce que je cherchais car les "contraintes" étaient explicitement mentionnées.

Alexey
la source
Telastyn, si vous pouviez ajouter ces citations à votre réponse, je préférerais accepter la vôtre plutôt que la mienne.
Alexey
2
le balisage n'est pas tout à fait le même, et j'ai changé un peu de l'accent, mais les citations ont été incluses.
Telastyn
Désolé, Joachim Sauer a déjà mentionné des propriétés "prouvables", que vous n'aimiez pas. En général, vous avez simplement reformulé les réponses existantes. Honnêtement, je ne sais pas ce que vous cherchez ...
Andres F.
Non, il n'a pas été expliqué "prouvable de quoi?". La propriété x.class.name = 'A'est prouvable pour toute la xclasse Asi vous autorisez trop de connaissances. La spécification de type n'était pas définie et sa relation exacte avec le LSP ne l'était pas non plus, bien que de manière informelle certaines indications aient été données. J'ai déjà trouvé ce que je cherchais dans l'article de Liskov et j'ai répondu à ma question ci-dessus.
Alexey
Je pense que les mots que vous avez enhardis sont la clé. Si le supertype documente que, pour n'importe lequel xde ces types, x.woozledonnera undefined, alors aucun type pour lequel x.woozlene donne undefinedne sera un sous-type approprié. Si le supertype ne documente rien x.woozle, le fait que l'utilisation x.woozlesur le supertype produira undefinedne signifierait rien à ce qui pourrait faire sur le sous-type.
supercat
3

Pour citer l'article de Wikipedia sur LSP , «la substituabilité est un principe de la programmation orientée objet». C'est un principe et une partie de la conception de votre programme. Si vous écrivez du code qui en dépend x.class == A, c'est votre code qui viole le LSP. Notez que ce type de code cassé est également possible en Java, aucun type de canard n'est nécessaire.

Rien dans le typage canard ne brise intrinsèquement LSP. Seulement si vous en abusez, comme dans votre exemple.

Réflexion supplémentaire: la vérification explicite de la classe d'un objet ne contredit-elle pas le but de la saisie de canard, de toute façon?

Andres F.
la source
Andres, pouvez-vous donner votre définition de LSP, s'il vous plaît?
Alexey
1
@Alexey La définition précise de LSP est énoncée dans Wikipedia en termes de sous-types. La définition informelle est Liskov's notion of a behavioral subtype defines a notion of substitutability for mutable objects; that is, if S is a subtype of T, then objects of type T in a program may be replaced with objects of type S without altering any of the desirable properties of that program (e.g., correctness).. La classe exacte d'un objet n'est PAS l'une des "propriétés souhaitables du programme"; sinon, cela irait à l'encontre non seulement de la frappe de canard mais du sous-typage en général, y compris la saveur de Java.
Andres F.
2
@Alexey Notez également que votre exemple de x.class == Aviole à la fois le LSP et le typage canard. Cela n'a aucun sens d'utiliser la saisie de canard si vous voulez vérifier les types réels.
Andres F.
Andres, cette définition n'est pas assez précise pour que je puisse comprendre. Quel programme, un programme donné ou un autre? Qu'est-ce qu'une propriété souhaitable? Si la classe se trouve dans une bibliothèque, différentes applications peuvent considérer différentes propriétés souhaitables. Je ne vois pas comment la ligne de code pourrait violer LSP, car je pensais que LSP était une propriété d'une paire de classes dans un langage de programmation donné: ( A, B) satisfait LSP ou non. Si LSP dépend du code utilisé ailleurs, il n'est pas expliqué quel code est autorisé. J'espère trouver quelque chose ici: cse.ohio-state.edu/~neelam/courses/788/lwb.pdf
Alexey
2
@Alexey LSP est valable (ou non) pour une conception spécifique. C'est quelque chose à rechercher dans un design; ce n'est pas la propriété d'une langue en général. Il ne devient pas plus précis que la définition réelle: Let q(x) be a property provable about objects x of type T. Then q(y) should be provable for objects y of type S where S is a subtype of T. Il est évident que ce x.classn'est pas une des propriétés intéressantes ici; sinon le polymorphisme de Java ne fonctionnerait pas non plus. Il n'y a rien d' inhérent à la saisie de canard dans votre "problème x.class". Êtes-vous d'accord jusqu'à présent?
Andres F.