Comment vérifier le principe de substitution de Liskov dans une hiérarchie d'héritage?

14

Inspiré par cette réponse:

Le principe de substitution de Liskov exige que

  • Les conditions préalables ne peuvent pas être renforcées dans un sous-type.
  • Les post-conditions ne peuvent pas être affaiblies dans un sous-type.
  • Les invariants du supertype doivent être conservés dans un sous-type.
  • Contrainte d'historique (la "règle d'historique"). Les objets ne sont considérés comme modifiables que par leurs méthodes (encapsulation). Étant donné que les sous-types peuvent introduire des méthodes qui ne sont pas présentes dans le supertype, l'introduction de ces méthodes peut permettre des changements d'état dans le sous-type qui ne sont pas autorisés dans le supertype. La contrainte d'historique l'interdit.

J'espérais que quelqu'un publierait une hiérarchie de classe qui viole ces 4 points et comment les résoudre en conséquence.
Je cherche une explication détaillée à des fins pédagogiques sur la façon d'identifier chacun des 4 points de la hiérarchie et la meilleure façon de le corriger.

Remarque:
J'espérais publier un exemple de code pour que les gens travaillent, mais la question elle-même est de savoir comment identifier les hiérarchies défectueuses :)

Songo
la source
Il y a d'autres exemples de violations de LSP dans les réponses à cette question SO
StuartLC

Réponses:

17

C'est beaucoup plus simple que cette citation rend le son aussi précis qu'il est.

Lorsque vous regardez une hiérarchie d'héritage, imaginez une méthode qui reçoit un objet de la classe de base. Maintenant, demandez-vous, y a-t-il des hypothèses qu'une personne éditant cette méthode pourrait faire qui ne seraient pas valides pour cette classe.

Par exemple ( vu à l'origine sur le site de l'oncle Bob ):

public class Square : Rectangle
{
    public Square(double width) : base(width, width)
    {
    }

    public override double Width
    {
        set
        {
            base.Width = value;
            base.Height = value;
        }
        get
        {
            return base.Width;
        }
    }

    public override double Height
    {
        set
        {
            base.Width = value;
            base.Height = value;
        }
        get
        {
            return base.Height;
        }
    }
}

Semble assez juste, non? J'ai créé un type de rectangle spécialisé appelé Square, qui maintient que la largeur doit être égale à la hauteur à tout moment. Un carré est un rectangle, donc il correspond aux principes OO, n'est-ce pas?

Mais attendez, que se passe-t-il si quelqu'un écrit maintenant cette méthode:

public void Enlarge(Rectangle rect, double factor)
{
    rect.Width *= factor;
    rect.Height *= factor;
}

Pas cool. Mais il n'y a aucune raison pour que l'auteur de cette méthode ait su qu'il pourrait y avoir un problème potentiel.

Chaque fois que vous dérivez une classe d'une autre, pensez à la classe de base et à ce que les gens pourraient en supposer (comme "elle a une largeur et une hauteur et elles seraient toutes deux indépendantes"). Alors pensez "ces hypothèses restent-elles valables dans ma sous-classe?" Sinon, repensez votre conception.

pdr
la source
Très bon et subtil exemple. +1. Ce que vous pourriez faire, c'est faire Agrandir une méthode de la classe Rectangle et la remplacer dans la classe Square.
marco-fiset
@ marco-fiset: Je préférerais voir Square et Rectangle découplés, Square avec une seule dimension, mais chacun mettant en œuvre IResizable. Il est vrai que s'il y avait une méthode Draw, ils seraient similaires, mais je préfère que les deux encapsulent une classe RectangleDrawer, qui inclut le code commun.
pdr
1
Je ne pense pas que ce soit un bon exemple. Le problème est qu'un carré n'a ni largeur ni hauteur. Il a juste une longueur de ses côtés. Le problème ne serait pas là si la largeur et la hauteur étaient seulement lisibles, mais elles sont inscriptibles dans ce cas. Lors de l'introduction d'un état modifiable, il est toujours beaucoup plus difficile de maintenir LSP.
SpaceTrucker
@pdr Merci pour l'exemple, mais concernant les 4 conditions que j'ai mentionnées dans mon post quelle partie du Squarecours les viole?
Songo
1
@Songo: C'est la contrainte d'histoire. Mieux expliqué ici: blackwasp.co.uk/LSP.aspx "De par leur nature, les sous-classes incluent toutes les méthodes et propriétés de leurs superclasses. Elles peuvent également ajouter d'autres membres. La contrainte d'historique indique que les membres nouveaux ou modifiés ne doivent pas modifier le état d'un objet d'une manière qui ne serait pas autorisée par la classe de base . Par exemple, si la classe de base représente un objet de taille fixe, la sous-classe ne doit pas autoriser cette taille à être modifiée. "
pdr