Pourquoi déclarer une instance comme supertype mais instancier comme un sous-type, plus Liskov principe de substitution

StackOverflow https://stackoverflow.com/questions/4871618

Question

J'ai essayé de comprendre le Liskov principe de substitution pendant quelques jours maintenant, et tout en faisant des tests de code avec l'exemple Rectangle / carré typique, j'ai créé le code ci-dessous et est venu avec 2 questions à ce sujet .

Question 1: Si nous avons une superclasse / sous-classe relation, pourquoi voudrions-nous déclarer une instance comme le supertype mais instancier (nouveau vers le haut) comme le sous-type

Je comprends pourquoi, si nous faisons polymorphisme à travers des interfaces, nous voulons déclarer et les variables instancier cette façon:

IAnimal dog = new Dog();

Cependant, maintenant que je me rappelle à ce sujet dans les classes de programmation anciens et quelques exemples de blog, lors de l'utilisation du polymorphisme par héritage, je vois encore quelques exemples où un code déclarerait une variable de cette façon

Animal dog = new Dog();

Dans mon code ci-dessous, hérite carrés de rectangle, donc quand je crée une nouvelle instance la place de cette façon:

Square sq = new Square();

il peut encore être traité comme un rectangle ou ajouté à une liste générique de Rectangles, alors pourquoi quelqu'un voudrait déclarer encore comme Rectangle = new Carré ()? Y at-il un avantage que je ne vois pas, ou un scénario où cela serait nécessaire? Comme je l'ai dit, mon code ci-dessous fonctionne très bien.

namespace ConsoleApp
{
class Program
{
    static void Main(string[] args)
    {
        var rect = new Rectangle(300, 150);
        var sq = new Square(100);
        Rectangle liskov = new Square(50);

        var list = new List<Rectangle> {rect, sq, liskov};

        foreach(Rectangle r in list)
        {
            r.SetWidth(90);
            r.SetHeight(80);

            r.PrintSize();
            r.PrintMyType();

            Console.WriteLine("-----");
        }


        Console.ReadLine();
    }

    public class Rectangle
    {
        protected int _width;
        protected int _height;

        public Rectangle(int width, int height)
        {
            _width = width;
            _height = height;
        }

        public void PrintMyType()
        {
            Console.WriteLine(this.GetType());
        }

        public void PrintSize()
        {
            Console.WriteLine(string.Format("Width: {0}, Height: {1}", _width, _height));
        }

        public virtual void SetWidth(int value)
        {
            _width = value;
        }

        public virtual void SetHeight(int value)
        {
            _height = value;
        }

        public int Width { get { return _width; } }
        public int Height { get { return _height; } }
    }

    public class Square : Rectangle
    {
        public Square(int size) : base(size, size) {}

        public override void SetWidth(int value)
        {
            base.SetWidth(value);
            base.SetHeight(value);
        }

        public override void SetHeight(int value)
        {
            base.SetHeight(value);
            base.SetWidth(value);
        }
    }
}

}

Même si ce devrait enfreindrait la Liskov principe de substitution, je reçois la sortie suivante:

"Largeur: 90, Hauteur: 80

ConsoleApp.Program + Rectangle

Largeur: 80 Hauteur: 80

ConsoleApp.Program + place

Largeur: 80 Hauteur: 80 ConsoleApp.Program + place

Question 2: Alors, pourquoi ou comment cet exemple de code enfreindrait la LSP? Est-ce seulement à cause de l'invariant place de tous les côtés étant égaux brise l'invariant rectangle que les côtés peuvent être modifiés indépendamment? Si c'est la raison, la violation de LSP serait purement théorique? Ou comment, dans le code, pourrais-je voir ce code briser le principe?

Edit: Entré avec une troisième question qui est venu dans l'un des articles de blog LSP je lisais, et il n'y avait pas de réponse il est donc cette

Question 3: Selon le principe ouvert-fermé que nous devrions introduire un nouveau comportement / fonctionnalité grâce à de nouvelles classes (héritage ou interfaces). Donc, si par exemple, j'ai une méthode WriteLog dans la classe de base, qui n'a pas de conditions préalables, mais je présente une nouvelle sous-classe qui remplace la méthode, mais écrit seulement en fait le journal si l'événement est très critique .... si cela est nouvelle fonctionnalité destinée (condition sine qua non d'être durci sur le sous-type), serait-ce encore le LSP brise? Les deux principes semblent contredire les uns les autres dans ce cas.

Merci d'avance.

Était-ce utile?

La solution

Question 1: Si nous avons une superclasse / sous-classe relation, pourquoi voudrions-nous déclarer une instance comme le supertype mais instancier (nouveau vers le haut) comme le sous-type

Les raisons pour lesquelles vous devez faire cela avec un super-type sont les mêmes raisons que vous le feriez pour une interface. Toutes les raisons que vous énumérez pourquoi déclarer la variable comme son sous-type spécifique plutôt que le supertype appliquent tout aussi bien pourquoi déclarer la variable comme son sous-type spécifique plutôt que d'une interface que les outils de sous-type.

abstract class Car { ... }
public abstract class ToyotaCamery2011 extends Car ( ... )

class Garage {
    private Car car = new ToyotaCamery2011();
    public Car getCar() { return car; }
    ....
}

class Garage {
    private ToyotaCamery2011 toyotaCamery2011 = new ToyotaCamery2011();
    public Car getCar() { return toyotaCamery2011; }
    ....
}

Tant que toutes les méthodes de Garage n'utilisent des méthodes de Car, et l'interface publique de Garage ne montre Car et spécifique de rien à Prius2011, les 2 classes sont effectivement équivalentes. Ce qui est plus facile à comprendre, par exemple que l'on modélise le monde réel de plus près? Ce qui assure que je ne pas utiliser accidentellement une méthode spécifique à la Prius, à savoir construit un garage spécifique à la Prius? Ce qui est juste le moindre peu plus maintenable quand si je décide d'obtenir une nouvelle voiture? Le code est amélioré de quelque façon que l'utilisation du sous-type spécifique?


Question 2: Alors, pourquoi ou comment cet exemple de code enfreindrait la LSP? Est-ce seulement à cause de l'invariant place de tous les côtés étant égaux brise l'invariant rectangle que les côtés peuvent être modifiés indépendamment? Si c'est la raison, la violation de LSP serait purement théorique? Ou comment, dans le code, pourrais-je voir ce code briser le principe?

Il est difficile de parler de LSP sans parler de promesses / contrats. Mais oui, si les promesses de Rectangle que les côtés peuvent être modifiés indépendamment (de façon plus formelle, si un postcondition pour appeler Rectangle.setWidth() comprend que Rectangle.getHeight () doit être affecté), puis Square dérivant de ruptures de Rectangle LSP.

Votre programme ne dépend pas de cette propriété, de sorte que son bien. Cependant prendre un programme qui tente de répondre à une valeur de périmètre ou de la valeur de la zone. Un tel programme peut se fonder sur l'idée que Rectangle a des côtés indépendants.

Toute classe qui accepte une Rectangle en entrée et dépend de cette propriété / comportement de Rectangle cassera probablement quand donné une Square en entrée. Les programmes comme celui-ci peuvent soit sauter à travers des cerceaux à rechercher et à désavouer un Square (qui est la connaissance d'une sous-classe) ou il peut modifier le contrat de Rectangle par rapport aux dimensions indépendantes. Ensuite, tous les programmes que l'utilisation Rectangle peut vérifier après chaque appel à setWidth() ou setLength () to see whether the adjacent side also changed and react accordingly. If it does the latter, thanSquarederiving frmoRectangle` n'est plus une violation du LSP.

Ce ne est pas seulement théorique, il peut avoir un impact réel sur le logiciel, mais il est souvent compromis sur la pratique. Vous voyez cela en Java souvent malheureusement. La classe Iterator de Java fournit une méthode de remove() qui est en option. Les classes qui utilisent iterator doivent avoir des connaissances sur la classe mise en œuvre et / ou ses sous-classes pour savoir si son coffre-fort à utiliser Iterator.remove(). Cela constitue une violation LSP, mais sa pratique acceptée en Java. Il rend l'écriture et la maintenance des logiciels plus complexes et plus sensibles aux insectes.


Question 3: ouvert-fermé principe stipule que nous devrions introduire un nouveau comportement / fonctionnalité grâce à de nouvelles classes (héritage ou interfaces). Donc, si par exemple, j'ai une méthode WriteLog dans la classe de base, qui n'a pas de conditions préalables, mais je présente une nouvelle sous-classe qui remplace la méthode, mais écrit seulement en fait le journal si l'événement est très critique .... si cela est nouvelle fonctionnalité destinée (condition sine qua non d'être durci sur le sous-type), serait-ce encore le LSP brise? Les deux principes semblent contredire les uns les autres dans ce cas.

Je pense que vous moyenne quand vous dites postconditions conditions préalables - que vous décrivez sur les qui les promesses de la méthode à remplir. Si oui, alors je ne vois pas violation LSP - si rien de promesses de la méthode, la sous-classe peut faire ce qu'il aime et être encore parfaitement substituable. Le fait que la sous-classe est plus sélective ( « seulement écrit en fait ») au sujet de ce qu'il écrit est une nouvelle fonctionnalité, surtout à la lumière du fait que rien de promesses superclasse.

Autres conseils

Pourquoi pensez-vous que cela devrait briser le Liskov principe de substitution?

Ce que le LSP est est vraiment que les méthodes devraient faire la bonne chose si elles sont adoptées des objets qui sont d'un sous-type plutôt que le type d'origine. Ce qui signifie que si vous pouvez prouver que la méthode « fait la bonne chose » si vous passez des objets du type, alors il « faire la bonne chose » si vous remplacez ces objets avec des objets d'un sous-type.

Mais dans votre cas, vous avez pas d'appels de méthode, et donc le LSP est un peu hors de propos.

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top