Зачем объявлять экземпляр как супертип, но экземпляр как подтип, плюс принцип замены Лискова

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

Вопрос

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

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

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

IAnimal dog = new Dog();

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

Animal dog = new Dog();

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

Square sq = new Square();

Его по -прежнему можно рассматривать как прямоугольник или добавить в общий список прямоугольников, так почему кто -то хочет объявить его как прямоугольник = 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+квадрат

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

РЕДАКТИРОВАТЬ: придумал третий вопрос, который возник в одной из статей LSP в блоге, которые я читал, и ответа не было, так что это

Вопрос 3: Принцип открытого закрытия гласит, что мы должны вводить новое поведение/функциональность через новые классы (наследование или интерфейсы). Так, если, например, у меня есть метод writeLog в базовом классе, который не имеет предварительных условий, но я представляю новый подкласс, который переопределяет метод, но фактически записывает в журнал, только если событие очень критическое ... Новая предполагаемая функциональность (предварительное кондиционирование затвердевает на подтипе), это все еще будет нарушать LSP? Два принципа, по -видимому, противоречат друг другу в этом случае.

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

Это было полезно?

Решение

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

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

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

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


Вопрос 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Площадьderiving frmoПрямоугольник 'больше не является нарушением LSP.

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


Вопрос 3: Принцип открытого закрытия гласит, что мы должны ввести новое поведение/функциональность через новые классы (наследование или интерфейсы). Так, если, например, у меня есть метод writeLog в базовом классе, который не имеет предварительных условий, но я представляю новый подкласс, который переопределяет метод, но фактически записывает в журнал, только если событие очень критическое ... Новая предполагаемая функциональность (предварительное кондиционирование затвердевает на подтипе), это все еще будет нарушать LSP? Два принципа, по -видимому, противоречат друг другу в этом случае.

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

Другие советы

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

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

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

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top