Что не так с наследованием Square и Rectangle?

Я читал некоторые статьи о том, что делать Square классом наследования класса Rectangle является плохой практикой, говоря, что это нарушает LSP (принцип подстановки Лискова). Я до сих пор не понимаю, я сделал пример кода на Ruby:

class Rectangle
    attr_accessor :width, :height
    def initialize(width, height)
        @width = width
        @height = height
    end
end

class Square < Rectangle
    def initialize(length)
        super(length, length)
    end
    def width=(number)
        super(number)
        @height = number
    end

    def height=(number)
        super(number)
        @width = number
    end
end


s = Square.new(100)

s.width = 50

puts s.height

Может ли кто-нибудь сказать мне, что с ним не так?


person mko    schedule 29.05.2013    source источник
comment
Пупырчатая космическая принцесса? youtube.com/watch?v=pJTrD3R5cj0   -  person paxdiablo    schedule 29.05.2013
comment
Вау, это интересно, но я не совсем понимаю   -  person mko    schedule 29.05.2013
comment
yozloy, извините, я просто хотел сказать, что вы можете объяснить, что вы имели в виду под LSP, чтобы тем, кто не знает об этом, не пришлось искать.   -  person paxdiablo    schedule 29.05.2013
comment
Пожалуйста, не ссылайтесь на внешние службы хостинга кода, так как ваши вопросы только содержат. Ваш вопрос должен быть автономным и отвечать на него, не завися от каких-либо внешних ссылок.   -  person meagar    schedule 29.05.2013
comment
кого это волнует, пока он делает то, что вы хотите, и соответствует спецификации?   -  person Adam Waite    schedule 29.05.2013
comment
@AdamWaite: сообщество SO заботится - есть большая вероятность, что этот веб-сайт переживет другие, на которых может быть размещен контент вопроса.   -  person maerics    schedule 29.05.2013
comment
@maerics, я не думаю, что Адам комментировал проблему с внешними ссылками, я подозреваю, что он комментировал фактический вопрос, поскольку спецификация, похоже, не имеет ничего общего с внешними ссылками и все, что связано с тем, работает ли код, используйте Это.   -  person paxdiablo    schedule 29.05.2013
comment
@paxdiablo Я думаю, вы правы, Адам имел в виду сам вопрос   -  person mko    schedule 29.05.2013


Ответы (3)


Мне не всегда нравится Лисков, поскольку он, кажется, ограничивает возможности наследования на основе поведения, а не сущности. На мой взгляд, наследование всегда должно было быть отношением, а не действовать точно так же.

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

Типичным примером, нарушающим LSP, является класс Square, производный от класса Rectangle, при условии, что методы получения и установки существуют как для ширины, так и для высоты.

Класс Square всегда предполагает, что ширина равна высоте. Если объект Square используется в контексте, где ожидается объект Rectangle, может возникнуть неожиданное поведение, поскольку размеры объекта Square не могут (или, скорее, не должны) изменяться независимо.

Эту проблему нелегко решить: если мы сможем модифицировать методы установки в классе Square так, чтобы они сохраняли инвариант Square (т. е. сохраняли одинаковые размеры), то эти методы ослабят (нарушат) постусловия для установок Rectangle, которые утверждают, что размеры могут быть изменены независимо.

Итак, глядя на ваш код вместе с эквивалентным кодом Rectangle:

s = Square.new(100)            r = Rectangle.new(100,100)
s.width = 50                   r.width = 50
puts s.height                  puts r.height

вывод будет 50 слева и 100 справа.

Но, это, на мой взгляд, важная часть статьи:

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

Другими словами, при условии, что код, использующий классы, понимает поведение, проблем не возникает.

Суть в том, что квадрат является правильным подмножеством прямоугольника для достаточно свободного определения прямоугольника :-)

person paxdiablo    schedule 29.05.2013
comment
спасибо за ваше подробное объяснение. Одна вещь, которую я не понимаю из вашего кода Rectangle, это строка r = Rectangle.new(100), вы имеете в виду r = Rectangle.new(100, 100)? - person mko; 29.05.2013
comment
@yozloy, да, извиняюсь, это была ошибка вырезания и вставки, хотя я мог бы утверждать, что это был конструктор Rectangle с одним аргументом, который создал квадрат :-) Исправлено. - person paxdiablo; 29.05.2013
comment
@paxdiable понял! Говоря о выходе s.height и r.height, я думаю, что 50, 100 являются правильными выходами, я прав на этот счет? - person mko; 29.05.2013
comment
@paxdiable, почему превращение квадратов и прямоугольников в подкласс четырехугольника не нарушает LSP? - person mko; 29.05.2013
comment
@yozloy, на самом деле ты прав, это тоже будет нарушением. Я удалил этот абзац, но больше склоняюсь к идее, что LSP имеет ограниченное практическое применение. - person paxdiablo; 29.05.2013
comment
@mko В вики-статье также перечислены несколько решений проблемы (в кавычках, потому что я тоже думаю, что LSP во многих или в большинстве случаев немного глуп), одно из которых, Разрешить более слабый контракт на Эллипсе, является решением, которое вы реализовано в вашем исходном вопросе. - person Kyle Strand; 26.09.2014

Что не так с точки зрения принципа замещения Лискова (LSP), так это то, что ваши Rectangles и Squares изменяемы. Это означает, что вы должны явно переопределить сеттеры в подклассе и потерять преимущества наследования. Если вы сделаете Rectangle неизменяемыми, т. е. если вам нужен другой Rectangle, вы создадите новый, а не измените измерения существующего, тогда нет проблем с нарушением LSP.

class Rectangle
  attr_reader :width, :height

  def initialize(width, height)
    @width = width
    @height = height
  end

  def area
    @width * @height
  end
end

class Square < Rectangle
  def initialize(length)
    super(length, length)
  end
end

Использование attr_reader дает геттеры, но не сеттеры, следовательно, неизменность. В этой реализации как Rectangles, так и Squares обеспечивают видимость height и width, для квадрата они всегда будут одинаковыми, и концепция площади непротиворечива.

person pjs    schedule 29.05.2013
comment
Фу! Я вижу, откуда вы исходите, и это хорошее объяснение. Но похоже, что это делает повторное использование объекта более сложным. Чтобы изменить размер объекта, вы должны создать новый объект с измененными атрибутами, а затем уничтожить старый. Это не делает ваш ответ менее достоверным, просто он снижает полезность LSP в моих глазах. - person paxdiablo; 29.05.2013
comment
@pjs, почему mutable теряет преимущества наследования? Я думаю, что просто переопределите методы установки, класс Square может повторно использовать методы, определенные в классе Rectangle, это своего рода преимущество? - person mko; 29.05.2013
comment
@paxdiablo: я не защищаю LSP, просто пытаюсь объяснить (мое понимание) его. Тем не менее, я склонен поддерживать неизменяемые объекты. Прямоугольник с другими размерами — это другой прямоугольник! Это делает вещи намного безопаснее во многих случаях. Например, рассмотрите возможность размещения набора прямоугольников в двоичном дереве поиска, упорядоченных по их площадям. Теперь, если вы измените размерность одного из них, дерево начнет загадочным образом давать сбои для будущих обращений — один из его элементов внезапно нарушает фундаментальное свойство дерева упорядочивания. Такие ошибки бывает очень трудно отследить. - person pjs; 29.05.2013
comment
@yozloy: чем больше вам приходится переписывать методы, чтобы заставить их работать в подклассе, тем больше вы говорите, что подкласс на самом деле не является расширением своего родительского класса, это отдельная вещь. Чем больше кода вы должны поместить в подкласс, тем меньше вы используете наследование. Обратите внимание, что в неизменном прямоугольнике мне не нужно было ничего добавлять или переопределять, кроме инициализатора. Не поймите меня неправильно, я не пытаюсь защищать LSP как превосходящий или низший по сравнению с альтернативными взглядами, просто указываю на некоторые соображения по поводу дизайна. - person pjs; 29.05.2013
comment
@pjs Вы имеете в виду, что переписывать методы в подклассе плохо? - person mko; 29.05.2013
comment
@yozloy Не совсем - я имею в виду, что переписывать методы в подклассе плохо, если есть лучшая архитектура. Если вы переписываете множество методов, то интерфейс может быть лучшим выбором дизайна, чем наследование. Я использую интерфейсы, когда абстракция с общим именем, например, областью, имеет совершенно разные реализации для разных классов, например, трапеция, треугольник и окружность. Интерфейс говорит мне, что все геометрические фигуры имеют понятие площади, но реализация этого понятия зависит от класса. - person pjs; 29.05.2013

Рассмотрим абстрактный базовый класс или интерфейс (будь то интерфейс или абстрактный класс — это деталь реализации, не имеющая отношения к LSP) ReadableRectangle; он имеет свойства только для чтения Width и Height. Из этого можно было бы вывести тип ReadableSquare, который имеет те же свойства, но по договору гарантирует, что Width и Height всегда будут равны.

Из ReadableRectangle можно определить конкретный тип ImmutableRectangle (который принимает высоту и ширину в своем конструкторе и гарантирует, что свойства Height и Width всегда будут возвращать одни и те же значения) и MutableRectangle. Можно также определить конкретный тип MutableRectangle, который позволяет устанавливать высоту и ширину в любое время.

Что касается «квадрата», то ImmutableSquare можно заменить как ImmutableRectangle, так и ReadableSquare. Однако MutableSquare можно заменить только на ReadableSquare [который, в свою очередь, можно заменить на ReadableRectangle]. Кроме того, хотя поведение ImmutableSquare можно заменить на ImmutableRectangle, ценность, полученная за счет наследования конкретного типа ImmutableRectangle, будет ограничена. Если бы ImmutableRectangle был абстрактным типом или интерфейсом, классу ImmutableSquare нужно было бы использовать только одно поле, а не два, для хранения своих измерений (для класса с двумя полями сохранение одного не составляет большого труда, но нетрудно представить себе классы с намного больше полей, где экономия может быть значительной). Однако если ImmutableRectangle является конкретным типом, то любой производный тип должен иметь все поля своей базы.

Некоторые типы квадратов можно заменить соответствующими типами прямоугольников, но изменяемый квадрат нельзя заменить изменяемым прямоугольником.

person supercat    schedule 02.07.2013