Зачем объявлять экземпляр как супертип, а создавать его как подтип, плюс принцип подстановки Лискова - PullRequest
1 голос
/ 02 февраля 2011

Я уже несколько дней пытаюсь понять принцип подстановки Лискова, и, выполняя некоторые тесты кода с очень типичным примером Rectangle / Square, я создал приведенный ниже код и придумал 2 вопроса о нем .

Вопрос 1: Если у нас есть отношение суперкласс / подкласс, почему мы хотим объявить экземпляр как супертип, а создать его экземпляр (создать новый) как подтип?

Я понимаю, почему, если мы делаем полиморфизм через интерфейсы, мы хотели бы объявить и создать экземпляр переменных следующим образом:

IAnimal dog = new Dog();

Однако теперь, когда я вспоминаю об этом в старых классах программирования и в некоторых примерах блогов, при использовании полиморфизма через наследование я все равно видел некоторые примеры, где некоторый код объявлял бы переменную таким образом

Animal dog = new Dog();

В моем коде ниже Square наследуется от Rectangle, поэтому, когда я создаю новый экземпляр Square следующим образом:

Square sq = new Square();

его по-прежнему можно рассматривать как прямоугольник или добавлять в общий список прямоугольников, так почему же кто-то захочет объявить его как Rectangle = new Square ()? Есть ли польза, которую я не вижу, или сценарий, где это потребуется? Как я уже сказал, мой код ниже работает просто отлично.

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);
        }
    }
}

}

Даже если это должно нарушать принцип подстановки Лискова, я получаю следующий вывод:

"Ширина: 90, Высота: 80

ConsoleApp.Program + Прямоугольник

Ширина: 80, Высота: 80

ConsoleApp.Program + Square

Ширина: 80, Высота: 80 ConsoleApp.Program + Square

Вопрос 2. Итак, почему или как этот пример кода нарушил бы LSP? Только из-за того, что инвариант квадрата всех сторон равен, нарушает инвариант прямоугольника, что стороны могут быть изменены независимо? Если это причина, то нарушение LSP будет только теоретическим? Или как в коде я мог видеть, как этот код нарушает принцип?

Редактировать: Третий вопрос возник в одной из статей блога LSP, которые я читал, и ответа не было, так что это

Вопрос 3: Принцип Open-Closed гласит, что мы должны вводить новое поведение / функциональность через новые классы (наследование или интерфейсы). Так, если, например, у меня есть метод WriteLog в базовом классе, который не имеет предварительных условий, но я ввожу новый подкласс, который переопределяет метод, но ТОЛЬКО фактически записывает в журнал, если событие очень критично ... если это новая предполагаемая функциональность (предварительное условие ужесточается для подтипа), это все еще будет нарушать LSP? В этом случае два принципа могут противоречить друг другу.

заранее спасибо.

Ответы [ 2 ]

3 голосов
/ 02 февраля 2011

Вопрос 1: Если у нас есть отношение суперкласс / подкласс, почему мы хотим объявить экземпляр как супертип, но создать его экземпляр (новый) как подтип?

Причины, по которым вы бы делали это с супертипом, - это те же причины, по которым вы делаете это с интерфейсом.Все перечисленные вами причины того, почему объявление переменной как ее определенного подтипа, а не супертипа, применимы в равной степени и к тому, почему объявление переменной как ее конкретного подтипа, а не интерфейса, который реализует этот подтип.

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; }
    ....
}

AsПока все методы Garage используют только методы Car, а открытый интерфейс Garage показывает только Car и ничего специфичного для Prius2011, эти 2 класса фактически эквивалентны.Что легче понять, например, какой из них более точно моделирует реальный мир?Что гарантирует, что я случайно не использую специфичный для Prius метод, то есть построил специфичный для Prius гараж?Что является чуть-чуть более легким в обслуживании, если я решу купить новую машину?Улучшен ли код каким-либо образом с использованием определенного подтипа?


Вопрос 2: Так почему или как этот пример кода нарушает LSP?Только из-за того, что инвариант квадрата всех сторон равен, нарушает инвариант прямоугольника, что стороны могут быть изменены независимо?Если это причина, то нарушение LSP будет только теоретическим?Или как в коде я мог видеть, как этот код нарушает принцип?

Трудно говорить о LSP, не говоря об обещаниях / контрактах.Но да, если Rectangle обещает, что стороны могут быть изменены независимо (более формально, если постусловие для вызова Rectangle.setWidth() включает, что Rectangle.getHeight () не должно быть затронуто), то Square, полученное из Rectangle, нарушает LSP.

Ваша программа не зависит от этого свойства, поэтому все в порядке.Однако возьмите программу, которая пытается удовлетворить значение периметра или площадь.Такая программа может опираться на идею, что Rectangle имеет независимые стороны.

Любой класс, который принимает Rectangle в качестве входных данных и зависит от этого свойства / поведения Rectangle, вероятно, сломается, если ему присваивается Square в качестве ввода.Программы, подобные этой, могут либо перепрыгивать через обручи, чтобы искать и запрещать Square (который является знанием подкласса), либо могут изменять контракт Rectangle относительно независимых размеров.Тогда все программы, использующие Rectangle, могут проверять после каждого вызова setWidth() или setLength () to see whether the adjacent side also changed and react accordingly. If it does the latter, than Square deriving frmo Rectangle` больше не является нарушением LSP.

Это не просто теоретический,это может оказать реальное влияние на программное обеспечение, но на практике оно часто подвергается риску.Вы видите это на Java часто, к сожалению.Класс Iterator в Java предоставляет необязательный метод remove().Классы, использующие итератор, должны знать о реализующем классе и / или его подклассах, чтобы знать, безопасно ли его использовать Iterator.remove().Это нарушает LSP, но это общепринятая практика на Java.Это делает написание и обслуживание программного обеспечения более сложным и более восприимчивым к ошибкам.


Вопрос 3: Принцип Open-Closed гласит, что мы должны вводить новое поведение / функциональность через новые классы (наследование или интерфейсы).Так, если, например, у меня есть метод WriteLog в базовом классе, который не имеет предварительных условий, но я ввожу новый подкласс, который переопределяет метод, но ТОЛЬКО фактически записывает в журнал, если событие очень критично ... если этоновая предполагаемая функциональность (предварительное условие ужесточается для подтипа), это все еще будет нарушать LSP?В этом случае два принципа могут противоречить друг другу.

Я думаю, что вы подразумеваете постусловия, когда говорите предусловия - вы описываете то, что метод обещает выполнить. Если так, то я не вижу нарушения LSP - если суперкласс метода ничего не обещает, тогда подкласс может делать то, что ему нравится, и при этом быть полностью заменяемым. Тот факт, что подкласс является более избирательным («ТОЛЬКО фактически пишет») о том, что он пишет, является новой функциональностью, особенно в свете того факта, что суперкласс ничего не обещает.

0 голосов
/ 02 февраля 2011

Почему вы думаете, что это должно нарушить принцип подстановки Лискова?

Что на самом деле означает LSP, так это то, что методы должны делать правильные вещи, если им передаются объекты, которые имеют подтип, а не исходныйтип.Это означает, что если вы можете доказать, что метод «делает правильные вещи», если вы передаете ему объекты этого типа, то он «будет делать правильные вещи», если вы замените эти объекты объектами подтипа.

Но в вашем случае у вас нет вызовов методов, поэтому LSP несколько не имеет значения.

...