Как проверить принцип подстановки Лискова в иерархии наследования?

14

Вдохновленный этим ответом:

Лиск принцип замещения требует , что

  • Предпосылки не могут быть усилены в подтипе.
  • Постусловия не могут быть ослаблены в подтипе.
  • Инварианты супертипа должны быть сохранены в подтипе.
  • Ограничение истории («правило истории»). Объекты считаются изменяемыми только через их методы (инкапсуляция). Поскольку подтипы могут вводить методы, которых нет в супертипе, введение этих методов может разрешить изменения состояния в подтипе, которые недопустимы в супертипе. Ограничение истории запрещает это.

Я надеялся, что кто-нибудь опубликует иерархию классов, которая нарушает эти 4 пункта и как их решить соответствующим образом.
Я ищу подробное объяснение для образовательных целей о том, как определить каждый из 4 пунктов в иерархии и как лучше всего это исправить.

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

Songo
источник
В ответах на этот SO-вопрос
StuartLC

Ответы:

17

Это намного проще, чем эта цитата, и звучит точно, как есть.

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

Например ( первоначально видели на сайте дяди Боба ):

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

Кажется достаточно справедливым, верно? Я создал специальный вид прямоугольника, называемый Квадрат, который утверждает, что ширина всегда должна быть равна высоте. Квадрат - это прямоугольник, так что он соответствует принципам ОО, не так ли?

Но подождите, что если кто-то сейчас напишет этот метод:

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

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

Каждый раз, когда вы выводите один класс из другого, подумайте о базовом классе и о том, что люди могут о нем подумать (например, «у него есть ширина и высота, и они оба будут независимы»). Тогда подумайте: "остаются ли эти предположения в силе в моем подклассе?" Если нет, переосмыслите свой дизайн.

прецизионный самописец
источник
Очень хороший и тонкий пример. +1. Что вы можете сделать, это сделать Enlarge метод класса Rectangle и переопределить его в классе Square.
Марко-Фисет
@ marco-fiset: Я бы предпочел разделить Square и Rectangle, Square с одним измерением, но каждый из них реализует IResizable. Это правда, что если бы существовал метод Draw, они были бы похожи, но я бы предпочел, чтобы они оба инкапсулировали класс RectangleDrawer, который включает в себя общий код.
фунтовые
1
Я не думаю, что это хороший пример. Проблема в том, что квадрат не имеет ширины или высоты. У него просто длина сторон. Проблема не возникла бы, если бы ширина и высота были только читаемыми, но в этом случае они доступны для записи. При введении модифицируемого состояния всегда намного сложнее поддерживать LSP.
SpaceTrucker
@pdr Спасибо за пример, но что касается 4 условий, которые я упомянул в своем посте, какая часть Squareкласса их нарушает?
Сонго
1
@Songo: Это ограничение истории. Лучше объяснить здесь: blackwasp.co.uk/LSP.aspx "По своей природе подклассы включают в себя все методы и свойства своих суперклассов. Они также могут добавлять дополнительные члены. Ограничение истории говорит, что новые или измененные члены не должны изменять состояние объекта способом, который не будет разрешен базовым классом . Например, если базовый класс представляет объект с фиксированным размером, подкласс не должен позволять изменять этот размер. "
фунтовые