Что случилось с наследование квадрата и прямоугольника?

Я читал некоторые статьи о практике, что делать квадрат классом наследования класса Rectangle-плохая практика, говоря, что это нарушает принцип замены LSP (Liskov). Я все еще не понимаю, я сделал пример кода в 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

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

3 ответов


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

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

типичным примером нарушения LSP является класс Square это происходит от класса Rectangle, предполагая, что методы getter и setter существуют как для ширины, так и для высоты.

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

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

Итак, глядя на ваш код рядом с эквивалентом 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.

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

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


что с ним не так с точки зрения принципа подстановки Лискова (LSP), так это то, что ваш Rectangles и Squares изменчивы. Это означает, что вы должны явно переопределить сеттеры в подклассе, и потерять преимущества наследования. Если вы сделаете Rectangles неизменяемый, т. е., если вы хотите другой 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, для квадрата они всегда будут одинаковыми, и концепция площади последовательна.


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

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

на" квадратной " стороне вещей, an ImmutableSquare должен быть заменяемым для обоих ImmutableRectangle и ReadableSquare. А MutableSquare, однако, заменяется только на ReadableSquare [который, в свою очередь, заменить на ReadableRectangle.] Дальше, в то время как поведение ImmutableSquare - это взаимозаменяемый для ImmutableRectangle, значение, полученное путем наследования конкретного ImmutableRectangle тип будет ограничено. Если ImmutableRectangle были абстрактным типом или интерфейсом,ImmutableSquare классу нужно будет использовать только одно поле, а не два, чтобы удерживать его размеры (для класса с двумя полями сохранение одного не имеет большого значения, но нетрудно представить классы с гораздо большим количеством полей, где экономия может быть значительной). Если, однако, ImmutableRectangle конкретный тип, то любой производный тип придется все поля своей базы.

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