924 votes

Quel est le principe de substitution de Liskov?

J'ai entendu dire que le principe de substitution de Liskov (LSP) est un principe fondamental de la conception orientée objet. Qu'est-ce que c'est et quels sont quelques exemples de son utilisation?

913voto

m-sharp Points 4349

Un excellent exemple illustrant le LSP (donné par l'Oncle Bob dans un podcast que j'ai entendu récemment) était parfois quelque chose qui sonne juste dans le langage naturel n'est pas tout à fait dans le code.

En mathématiques, un Carré est un Rectangle. En effet c'est une spécialisation d'un rectangle. La "est un" vous donne envie de ce modèle avec l'héritage. Toutefois, si dans le code, vous faites Carré dériver à partir d'un Rectangle, puis un Carré devrait être utilisable n'importe où vous vous attendez à un rectangle. Ceci en fait un comportement étrange.

Imaginez que vous avez SetWidth et SetHeight méthodes sur votre Rectangle de la classe de base; ce qui semble parfaitement logique. Toutefois, si votre Rectangle de référence a souligné un Carré, alors SetWidth et SetHeight n'a pas de sens parce que la définition d'un allait changer l'autre pour le match. Dans ce cas, le Carré d'échec de la Liskov Substitution de Test avec le Rectangle et l'abstraction d'avoir Carrés hérite de Rectangle est une mauvaise.

LiskovSubtitutionPrinciple_52BB5162.jpg

Y ' all doivent vérifier les autres inestimable de SOLIDES Principes de Motivation Affiches.

494voto

NotMyself Points 7567

Le Principe de Substitution de Liskov (LSP) est un concept de Programmation Orientée Objet qui déclare:

Les fonctions qui utilisent des pointeurs ou des les références aux classes de base doit être en mesure d'utiliser des objets des classes dérivées sans le savoir.

Au cœur de la LSP est sur les interfaces et des contrats ainsi que la façon de décider quand pour étendre une classe vs utiliser une autre stratégie, tels que la composition d'atteindre votre objectif.

Le moyen le plus efficace que j'ai vu pour illustrer ce point a dans la Tête la Première OOA&D. Ils présentent un scénario dans lequel vous êtes un développeur sur un projet de construction d'un cadre pour des jeux de stratégie.

Ils présentent une classe qui représente un conseil d'administration qui ressemble à ceci:

Class Diagram

Toutes les méthodes de prendre des coordonnées X et Y en tant que paramètres pour localiser la position de la tuile dans le tableau à deux dimensions des Carreaux. Ceci permet à un développeur de jeu pour gérer les unités dans le conseil d'administration au cours du jeu.

Le livre se passe de changer les exigences de dire que le jeu de cadre de travail doit également prendre en charge des jeux 3d conseils pour accueillir les jeux qui ont vol. Ainsi, un ThreeDBoard classe est introduit qui s'étend Conseil d'administration.

À première vue, cela semble être une bonne décision. Conseil fournit à la fois la Hauteur et la Largeur des propriétés et ThreeDBoard fournit de l'axe Z.

Où il se décompose, c'est quand vous regardez tous les autres membres hérités du Conseil d'administration. Les méthodes pour AddUnit, GetTile, GetUnits et ainsi de suite, prendre les deux paramètres X et Y dans le Conseil de classe, mais la ThreeDBoard a besoin d'un paramètre Z ainsi.

Donc, vous devez implémenter ces méthodes de nouveau avec un Z paramètre. Le paramètre Z n'a pas de contexte pour le Conseil de classe et les méthodes héritées du Conseil de classe perdent leur sens. Une unité de code en essayant d'utiliser le ThreeDBoard classe comme classe de base Conseil d'administration serait très hors de la chance.

Nous devrions peut-être trouver une autre approche. Au lieu de l'étendre Conseil, ThreeDBoard devrait être composé de Conseil des objets. Un Conseil de l'objet par unité de l'axe des Z.

Cela nous permet d'utiliser une bonne orientée objet principes, tels que l'encapsulation et de la réutilisation et de ne pas violer les LSP.

135voto

Konrad Rudolph Points 231505

LSP préoccupations des invariants. Votre conseil d'administration exemple est cassé au départ parce que les interfaces n'ont tout simplement pas de match.

Un meilleur exemple serait le suivant (implémentations omis):

class Rectangle {
    int getHeight() const;
    void setHeight(int value);
    int getWidth() const;
    void setWidth(int value);
};

class Square : public Rectangle { };

Maintenant, nous avons un problème, bien que l'interface de matchs. La raison en est que nous avons violé (implicite) des invariants. La façon dont les accesseurs et mutateurs travail, Rectangle devrait satisfaire l'invariant suivant:

void invariant(Rectangle& r) {
    r.setHeight(200);
    r.setWidth(100);
    assert(r.getHeight() == 200 and r.getWidth() == 100);
}

Cependant, cet invariant doit être violés par une mise en œuvre correcte de l' Square, il n'est donc pas un substitut valable de Rectangle.

77voto

Phillip Wells Points 2625

Robert Martin a un excellent article sur le principe de la substitution Liskov ici . Il discute des manières subtiles et pas si subtiles dans lesquelles le principe peut être violé.

41voto

Shelby Moore III Points 2088

LSP est nécessaire lorsqu'un code pense que c'est d'appeler les méthodes d'un type T, et peuvent inconsciemment appeler les méthodes d'un type SS extends T (c - S hérite, découle de, ou est un sous-type d', le supertype T).

Par exemple, cela se produit lorsqu'une fonction avec un paramètre d'entrée de type T, est appelé (c'est à dire invoquée) avec une valeur de l'argument de type S. Ou, si un identifiant de type T, est attribuée une valeur de type S.

val id : T = new S() // id thinks it's a T, but is a S

LSP exige que les attentes (c'est à dire les invariants) pour les méthodes de type T (par exemple, Rectangle), qui ne peut être violé lorsque les méthodes de type S (par exemple, Square) sont appelés à la place.

val rect : Rectangle = new Square(5) // thinks it's a Rectangle, but is a Square
val rect2 : Rectangle = rect.setWidth(10) // height is 10, LSP violation

Même un type immuable champs a encore des invariants, par exemple, l' immuable Rectangle setters attendre dimensions à être modifiés séparément, mais l' immuable Carré poseurs de violer cette attente.

class Rectangle( val width : Int, val height : Int )
{
   def setWidth( w : Int ) = new Rectangle(w, height)
   def setHeight( h : Int ) = new Rectangle(width, h)
}

class Square( val side : Int ) extends Rectangle(side, side)
{
   override def setWidth( s : Int ) = new Square(s)
   override def setHeight( s : Int ) = new Square(s)
}

LSP, il faut que chaque méthode de la sous-type S doit avoir contravariant paramètre d'entrée(s) et un covariant de sortie.

Contravariant signifie que la variance est contraire à la direction de l'héritage, c'est à dire le type d' Ti, de chaque paramètre d'entrée de chaque méthode de la sous-type S, doit être le même ou un supertype de type Si de la correspondante du paramètre d'entrée de la méthode correspondante de la supertype T.

La Covariance signifie que la variance est dans le même sens de l'héritage, c'est à dire le type d' So, de la sortie de chaque méthode de la sous-type S, doit être le même ou un sous-type du type To de la sortie correspondante de la méthode correspondante de la supertype T.

C'est parce que si l'appelant pense qu'il a un type T, pense que c'est l'appel d'une méthode de T, puis il fournit de l'argument(s) de type Si et affecte la sortie du type To. Lorsqu'il est fait appel de la méthode correspondante de S, puis chaque Si argument d'entrée est assignée à un Ti paramètre d'entrée, et l' So la production est attribué au type d' To.

En outre, pour chaque paramètre d'entrée qui a une fonction de type, la variance, les rôles sont inversés, c'est à dire chacun de ses paramètres d'entrée doivent être covariant et sa sortie doit être contravariant. Cette règle n'est pas appliquée de manière récursive.

En outre, pour les langues (par exemple Scala) qui ont la définition de la place de la variance des annotations sur les paramètres de type (c'est à dire les génériques), la direction (c'est à dire co - ou contra-) de la variance de l'annotation pour chaque type de paramètre(s) de sous-type S doit être opposé ou de même sens, respectivement, pour chaque paramètre d'entrée ou de sortie (de chaque méthode d' S) qui contient le paramètre de type. C'est par rapport à la direction de l'annotation de variance pour le type correspondant paramètre de type de paramètre d'entrée ou de sortie.


Le sous-typage est approprié où les invariants peuvent être énumérés.

Il y a beaucoup de recherches en cours sur la façon de modéliser les invariants, de sorte qu'ils sont appliqués par le compilateur.

Typestate (voir page 3) déclare et applique l'état invariants orthogonale de type. Alternativement, les invariants peuvent être appliquées par la conversion des assertions de types. Par exemple, affirmer qu'un fichier est ouvert avant de le fermer, puis Fichier.open() peut retourner un OpenFile type, qui contient une méthode close() qui n'est pas disponible dans le Fichier. Un tic-tac-toe API peut être un autre exemple de l'emploi de dactylographie pour faire respecter les invariants au moment de la compilation. Le type de système peut même être Turing-complet, par exemple Scala. Dépendante tapé langues et des démonstrateurs de formaliser les modèles d'ordre supérieur de la frappe.

En raison de la nécessité pour la sémantique de résumé par extension, j'attends que le recours à taper pour modèle invariants, c'est à dire unifiée d'ordre supérieur denotational sémantique, est supérieure à la Typestate. La "vulgarisation" signifie l'abîme, permutées composition de manquer de coordination, de développement modulaire. Parce qu'il me semble être l'antithèse de l'unification et donc les degrés de liberté, pour avoir deux mutuellement dépendantes des modèles (par exemple: types et Typestate) pour exprimer la sémantique partagé, qui ne peuvent pas être unis les uns avec les autres pour extensible composition. Par exemple, l'Expression de Problème-comme l'extension a été unifiée dans le sous-typage, de surcharge de fonctions paramétriques tapant les domaines.

Ma position théorique est que par la connaissance d'exister (voir la section "la Centralisation est aveugle et inapte"), il n'y aura jamais d' être un modèle général peut appliquer une couverture de 100% de tous les possibles invariants dans une Turing-complet le langage de l'ordinateur. Pour la connaissance d'exister, inattendu beaucoup de possibilités existent, c'est à dire le désordre et l'entropie doit toujours être à la hausse. C'est la force entropique. Pour prouver à tous possible les calculs d'un potentiel d'extension, est de calculer a priori possible extension.

C'est pourquoi l'Arrêt du Théorème existe, c'est à dire qu'il est indécidable de savoir si chaque programme dans une Turing-complet langage de programmation se termine. Il peut être prouvé que certains programmes spécifiques se termine (que toutes les possibilités ont été définies et calculées). Mais il est impossible de prouver que tout est possible extension de ce programme se termine, à moins que les possibilités d'extension de ce programme n'est pas Turing (par exemple par l'intermédiaire de dépendant de la frappe). Depuis que l'exigence fondamentale pour Turing-complétude est la surabondance de la récursivité, il est intuitif de comprendre comment Gödel est théorèmes d'incomplétude et Russell paradoxe s'appliquent à l'extension.

Une interprétation de ces théorèmes, les intègre dans une généralisé de la compréhension conceptuelle de la force entropique:

  • Gödel est théorèmes d'incomplétude: toute formelle de la théorie, dans laquelle toutes les vérités de l'arithmétique peut être prouvé, est incohérent.
  • Russell paradoxe: chaque règle d'adhésion pour un jeu qui peut contenir un ensemble, énumère le type spécifique de chacun des membres ou contient elle-même. Ainsi définit soit ne peut être prolongée ou ils sont sans limite de récursivité. Par exemple, l'ensemble de tout ce qui n'est pas une théière, comprend lui-même, qui comprend lui-même, qui comprend lui-même, etc.... Donc une règle est incohérent s'il (peut contenir un ensemble d') n'a pas d'énumérer les types spécifiques (c'est à dire permet à tous de type non spécifié) et ne permet pas l'utilisation illimitée de l'extension. C'est l'ensemble des ensembles qui ne sont pas membres d'elles-mêmes. Cette incapacité à être à la fois cohérent et totalement énumérés sur tous les possibles de l'extension, est Gödel est théorèmes d'incomplétude.
  • Liskov Substition Principe: en général, c'est un problème indécidable de savoir si tout est le sous-ensemble de l'autre, c'est à dire de l'héritage est en général indécidable.
  • Linsky Référencement: il est indécidable de ce que le calcul de la chose est, quand il est décrit ou perçu, c'est à dire la perception (la réalité) n'a pas de point absolu de référence.
  • Le théorème de Coase: il n'y a pas de point de référence externe, ainsi que tout obstacle à l'utilisation illimitée de l'externe possibilités échouera.
  • Deuxième loi de la thermodynamique: l'univers tout entier (un système fermé, c'est à dire tout) tendances à un maximum de désordre, c'est à dire maximum de possibilités indépendantes.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X