Почему квадратное наследование от Rectangle будет проблематичным, если мы переопределим методы SetWidth и SetHeight?

105

Если Квадрат является типом Прямоугольника, то почему Квадрат не может наследовать от Прямоугольника? Или почему это плохой дизайн?

Я слышал, как люди говорят:

Если вы сделали Square производным от Rectangle, то Square должен использоваться везде, где вы ожидаете прямоугольник

В чем здесь проблема? И почему Square можно использовать везде, где вы ожидаете прямоугольник? Это будет полезно только в том случае, если мы создадим объект Square, и если мы переопределим методы SetWidth и SetHeight для Square, чем возникнет такая проблема?

Если у вас есть методы SetWidth и SetHeight в базовом классе Rectangle, и если ваша ссылка Rectangle указывает на квадрат, то SetWidth и SetHeight не имеют смысла, поскольку установка одного из них приведет к изменению другого в соответствии с ним. В этом случае Square не проходит тест подстановки Лискова с помощью Rectangle, и абстракция наличия наследования Square от Rectangle является плохой.

Может кто-нибудь объяснить приведенные выше аргументы? Опять же, если мы переопределим методы SetWidth и SetHeight в Square, не решит ли это эту проблему?

Я также слышал / читал:

Реальная проблема заключается в том, что мы моделируем не прямоугольники, а скорее «изменяемые прямоугольники», то есть прямоугольники, ширину или высоту которых можно изменить после создания (и мы по-прежнему считаем, что это один и тот же объект). Если мы посмотрим на класс прямоугольника таким образом, то станет ясно, что квадрат не является «изменяемым прямоугольником», потому что квадрат не может быть изменен и все же будет квадратом (в общем). Математически мы не видим проблемы, потому что изменчивость даже не имеет смысла в математическом контексте

Здесь я полагаю, что «изменяемый размер» является правильным термином. Прямоугольники «изменяемого размера» и квадраты. Я что-то упустил в приведенном выше аргументе? Квадрат можно изменить, как любой прямоугольник.

user793468
источник
15
Этот вопрос кажется ужасно абстрактным. Существует огромное количество способов использования классов и наследования, независимо от того, является ли хорошая идея наследовать какой-либо класс от какого-либо класса, обычно зависит в основном от того, как вы хотите использовать эти классы. Без практического случая я не вижу, как этот вопрос может получить соответствующий ответ.
аааааааааааа
2
Используя некоторый здравый смысл, мы помним, что квадрат является прямоугольником, поэтому, если объект вашего квадратного класса не может использоваться там, где требуется прямоугольник, это, вероятно, в любом случае является недостатком дизайна приложения.
Ктулху
7
Я думаю, что лучший вопрос Why do we even need Square? Это как две ручки. Одна синяя ручка и одна красная синяя, желтая или зеленая ручка. Синяя ручка избыточна - тем более в случае с квадратом, поскольку она не имеет экономической выгоды.
Гусдор
2
@eBusiness Его абстрактность делает его хорошим учебным вопросом. Важно уметь распознавать, какое использование подтипирования является плохим, независимо от конкретных случаев использования.
Довал
5
@Cthulhu Не совсем. Подтипирование - все о поведении, и изменяемый квадрат не ведет себя как изменяемый прямоугольник. Вот почему метафора "это ..." - это плохо.
Довал

Ответы:

189

По сути, мы хотим, чтобы все было разумно.

Рассмотрим следующую проблему:

Мне дали группу прямоугольников, и я хочу увеличить их площадь на 10%. Поэтому я устанавливаю длину прямоугольника в 1,1 раза больше, чем была раньше.

public void IncreaseRectangleSizeByTenPercent(IEnumerable<Rectangle> rectangles)
{
  foreach(var rectangle in rectangles)
  {
    rectangle.Length = rectangle.Length * 1.1;
  }
}

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

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

Хуже того, Джим из бухгалтерии видит мой метод и пишет какой-то другой код, который использует тот факт, что если он передаст квадраты моему методу, он получит очень хорошее увеличение размера на 21%. Джим счастлив, и никто не мудрее.

Джим получил повышение за отличную работу в другом отделе. Альфред присоединяется к компании как младший. В своем первом сообщении об ошибке Джилл из рекламного агентства сообщил, что пропуск квадратов для этого метода приводит к увеличению на 21% и требует исправления ошибки. Альфред видит, что квадраты и прямоугольники используются повсюду в коде, и понимает, что разрыв цепочки наследования невозможен. Он также не имеет доступа к исходному коду учета. Таким образом, Альфред исправляет ошибку следующим образом:

public void IncreaseRectangleSizeByTenPercent(IEnumerable<Rectangle> rectangles)
{
  foreach(var rectangle in rectangles)
  {
    if (typeof(rectangle) == Rectangle)
    {
      rectangle.Length = rectangle.Length * 1.1;
    }
    if (typeof(rectangle) == Square)
    {
      rectangle.Length = rectangle.Length * 1.04880884817;
    }
  }
}

Альфред доволен своими навыками хакерского убера, и Джилл подписывает, что ошибка исправлена.

В следующем месяце никому не платят, потому что бухгалтерия зависела от того, можно ли передать квадраты IncreaseRectangleSizeByTenPercentметоду и получить увеличение на 21%. Вся компания переходит в режим «исправление приоритета 1», чтобы отследить источник проблемы. Они прослеживают проблему до решения Альфреда. Они знают, что им нужно поддерживать и бухгалтерию, и рекламу. Таким образом, они решают проблему, идентифицируя пользователя с помощью вызова метода следующим образом:

public void IncreaseRectangleSizeByTenPercent(IEnumerable<Rectangle> rectangles)
{
  IncreaseRectangleSizeByTenPercent(
    rectangles, 
    new User() { Department = Department.Accounting });
}

public void IncreaseRectangleSizeByTenPercent(IEnumerable<Rectangle> rectangles, User user)
{
  foreach(var rectangle in rectangles)
  {
    if (typeof(rectangle) == Rectangle || user.Department == Department.Accounting)
    {
      rectangle.Length = rectangle.Length * 1.1;
    }
    else if (typeof(rectangle) == Square)
    {
      rectangle.Length = rectangle.Length * 1.04880884817;
    }
  }
}

И так далее.

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

Есть два реалистичных способа решения этой проблемы.

Первый способ - сделать Rectangle неизменным. Если пользователь Rectangle не может изменить свойства длины и ширины, эта проблема исчезнет. Если вы хотите прямоугольник с другой длиной и шириной, вы создаете новый. Квадраты могут наследовать от прямоугольников счастливо.

Второй способ - разорвать цепочку наследования между квадратами и прямоугольниками. Если квадрат определяются как имеющие единственной SideLengthсобственностью и прямоугольники имеют Lengthи Widthимущество , и нет наследства, то невозможно случайно сломать вещи, ожидая прямоугольник и получить квадрат. В терминах C # вы можете использовать sealкласс прямоугольников, который гарантирует, что все прямоугольники, которые вы когда-либо получали, на самом деле являются прямоугольниками.

В этом случае мне нравится способ решения проблемы «неизменяемыми объектами». Идентичность прямоугольника - это его длина и ширина. Имеет смысл, что когда вы хотите изменить идентичность объекта, то, что вы действительно хотите, это новый объект. Если вы потеряете старого клиента и получите нового, вы не меняете Customer.Idполе со старого клиента на нового, вы создаете нового Customer.

Нарушения принципа подстановки Лискова часто встречаются в реальном мире, в основном потому, что много кода написано людьми, которые некомпетентны / испытывают нехватку времени / не заботятся / делают ошибки. Это может и действительно приводит к некоторым очень неприятным проблемам. В большинстве случаев вы предпочитаете композицию вместо наследования .

Стивен
источник
7
Лисков это одно, а хранилище это другой вопрос. В большинстве реализаций экземпляр Square, унаследованный от Rectangle, потребует места для хранения двух измерений, даже если требуется только одно.
el.pescado
29
Блестящее использование истории, чтобы проиллюстрировать это
Рори Хантер
29
Хорошая история, но я не согласен. Вариант использования был: изменить площадь прямоугольника. Для исправления следует добавить переопределяемый метод «ChangeArea» к прямоугольнику, который специализируется на Square. Это не разорвало бы цепочку наследования, не сделало бы явным то, что пользователь хотел сделать, и не вызвало бы ошибку, внесенную упомянутым вами исправлением (которое было бы обнаружено в правильной промежуточной области).
Рой Т.
33
@RoyT .: Почему прямоугольник должен знать, как установить его площадь? Это свойство полностью зависит от длины и ширины. И, более того, какой размер должен измениться - длина, ширина или оба?
Цао
32
@Roy T. Очень приятно сказать, что вы бы решили проблему по-другому, но факт в том, что это пример - хотя и упрощенный - ситуаций реального мира, с которыми разработчики сталкиваются ежедневно при обслуживании устаревших продуктов. И даже если вы реализуете этот метод, это не остановит наследников, нарушающих LSP и вносящих ошибки, сродни этому. Вот почему почти все классы в .NET Framework запечатаны.
Стивен
30

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

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

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

Если у вашего Rectangle есть метод, setWidthкоторый задокументирован как изменение ширины и не изменение высоты, то у Square не может быть совместимого метода. Если вы измените ширину, а не высоту, результат больше не будет действительным квадратом. Если вы решили изменить ширину и высоту квадрата при использовании setWidth, вы не реализуете спецификацию Rectangle setWidth. Ты просто не можешь победить.

Когда вы посмотрите на то, что вы можете «поместить» в Прямоугольник и Квадрат, какие сообщения вы можете им отправить, вы, вероятно, обнаружите, что любое сообщение, которое вы можете правильно отправить на Квадрат, вы также можете отправить в Прямоугольник.

Это вопрос совмещения и противоречия.

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

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

Итак, вернемся к Rectangle и Square: может ли Square быть подклассом Rectangle, полностью зависит от того, какие методы есть у Rectangle.

Если Rectangle имеет отдельные сеттеры для ширины и высоты, Square не будет хорошим подклассом.

Аналогично, если вы сделаете несколько методов в вариантах аргументов, например, compareTo(Rectangle)для Rectangle и compareTo(Square)Square, у вас будет проблема с использованием Square в качестве Rectangle.

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

ЛРН
источник
«Если все ваши объекты неизменны, проблем нет» - это, по-видимому, неуместное утверждение в контексте этого вопроса, в котором явно упоминаются сеттеры для ширины и высоты
комнат
11
Я нашел это интересным, даже когда это «очевидно не имеет значения»
Джесвин Хосе
14
@gnat Я бы сказал, что это актуально, потому что реальная ценность вопроса заключается в том, чтобы понять, когда между двумя типами существуют правильные отношения подтипов. Это зависит от того, какие операции объявляет супертип, поэтому стоит указать, что проблема исчезнет, ​​если методы мутатора исчезнут.
Довал
1
@gnat также, сеттеры являются мутаторами , поэтому lrn, по сути, говорит: «Не делай этого, и это не проблема». Я согласен с неизменяемостью для простых типов, но вы делаете хорошее замечание: для сложных объектов проблема не так проста.
Патрик М
1
Рассмотрим это так, как поведение гарантируется классом 'Rectangle'? То, что вы можете изменить ширину и высоту НЕЗАВИСИМЫ друг от друга. (т.е. setWidth и setHeight) метод. Теперь, если Square получен из Rectangle, Square должен гарантировать это поведение. Поскольку квадрат не может гарантировать такое поведение, это плохое наследство. Однако, если методы setWidth / setHeight удалены из класса Rectangle, такого поведения нет, и, следовательно, вы можете получить класс Square из Rectangle.
Nitin Bhide
17

Здесь много хороших ответов; Ответ Стивена, в частности, хорошо иллюстрирует, почему нарушения принципа замещения приводят к реальным конфликтам между командами.

Я подумал, что мог бы вкратце поговорить о конкретной проблеме прямоугольников и квадратов, а не использовать ее в качестве метафоры для других нарушений LSP.

Существует дополнительная проблема с прямоугольником, представляющим собой особый вид прямоугольника, о котором редко упоминают, а именно: почему мы останавливаемся с квадратами и прямоугольниками ? Если мы хотим сказать, что квадрат - это особый вид прямоугольника, то, конечно же, мы должны также сказать:

  • Квадрат - это особый вид ромба - это ромб с квадратными углами.
  • Ромб - это особый вид параллелограмма - это параллелограмм с равными сторонами.
  • Прямоугольник - это особый вид параллелограмма - это параллелограмм с квадратными углами
  • Прямоугольник, квадрат и параллелограмм - это особый вид трапеции - это трапеции с двумя наборами параллельных сторон
  • Все вышеперечисленное является особым видом четырехугольников
  • Все вышеперечисленное - это особые виды плоских форм.
  • И так далее; Я мог бы продолжать здесь некоторое время.

Что на земле все отношения должны быть здесь? Языки на основе наследования классов, такие как C # или Java, не были предназначены для представления таких сложных отношений с множеством различных видов ограничений. Лучше всего просто избегать вопроса, не пытаясь представить все эти вещи как классы с подтиповыми отношениями.

Эрик Липперт
источник
3
Если объекты фигур являются неизменяемыми, то можно иметь IShapeтип, который включает в себя ограничивающий прямоугольник, его можно рисовать, масштабировать и сериализовать, а также иметь IPolygonподтип с методом для сообщения количества вершин и методом для возврата IEnumerable<Point>. Тогда можно было бы иметь IQuadrilateralподтип, который происходит из IPolygonэтого IRhombusи IRectangleпроисходит из этого, а также ISquareиз IRhombusи IRectangle. Изменчивость отбрасывает все в окно, а множественное наследование не работает с классами, но я думаю, что это нормально с неизменяемыми интерфейсами.
суперкат
Я фактически не согласен с Эриком здесь (хотя этого недостаточно для -1!). Все эти отношения (возможно) актуальны, как упоминает @supercat; это просто проблема YAGNI: вы не реализуете ее, пока она вам не понадобится.
Марк Херд
Очень хороший ответ! Должно быть намного выше.
andrew.fox
1
@MarkHurd - это не проблема YAGNI: предложенная иерархия на основе наследования имеет форму описанной таксономии, но ее нельзя написать, чтобы гарантировать отношения, которые ее определяют. Каким образом IRhombusгарантируется, что все Pointвозвращаемые из Enumerable<Point>заданного значения IPolygonсоответствуют ребрам равной длины? Поскольку реализация одного только IRhombusинтерфейса не гарантирует, что конкретный объект является ромбом, наследование не может быть ответом.
А.
14

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

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

Ключевым фактором здесь является изменчивость . Может ли форма измениться после ее построения?

  • Изменчивый: если формы могут изменяться после создания, квадрат не может иметь отношения is-a с прямоугольником. Контракт прямоугольника включает в себя ограничение на то, что противоположные стороны должны быть равной длины, но смежные стороны не должны быть. Квадрат должен иметь четыре равные стороны. Изменение квадрата через интерфейс прямоугольника может нарушить контракт квадрата.

  • Неизменный: если формы не могут измениться после создания, то квадратный объект также всегда должен выполнять контракт прямоугольника. Квадрат может иметь отношение is-a с прямоугольником.

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


источник
1
This is one of those cases where the real world is not able to be modeled in a computer 100%, Почему так? У нас все еще может быть функциональная модель квадрата и прямоугольника. Единственное последствие - мы должны искать более простую конструкцию для абстрагирования над этими двумя объектами.
Саймон Берго
6
Между прямоугольниками и квадратами больше общего. Проблема заключается в том, что единица прямоугольника и единица квадрата - это длина его стороны (и угол на каждом пересечении). Лучшее решение здесь - сделать квадраты наследуемыми от прямоугольников, но сделать их неизменяемыми.
Стивен
3
@ Стефан Согласен. На самом деле, делать их неизменяемыми - разумная вещь, независимо от проблем, связанных с подтипами. Нет причин делать их изменяемыми - построить новый квадрат или прямоугольник не сложнее, чем изменить его, так зачем открывать эту банку с червями? Теперь вам не нужно беспокоиться о псевдонимах / побочных эффектах, и вы можете использовать их в качестве ключей для карт / диктов при необходимости. Некоторые скажут «производительность», на что я скажу «преждевременная оптимизация», пока они на самом деле не измерили и не доказали, что горячая точка находится в коде формы.
Довал
Извините, ребята, было уже поздно, и я очень устала, когда написала ответ. Я переписал это, чтобы сказать, что я действительно имел в виду, суть которого - изменчивость.
13

И почему Square можно использовать везде, где вы ожидаете прямоугольник?

Потому что это часть того, что означает подтип (см. Также: принцип подстановки Лискова). Вы можете сделать, должны быть в состоянии сделать это:

Square s = new Square(5);
Rect r = s;
doSomethingWith(r); // written assuming a Rect, actually calls Square methods

Вы фактически делаете это все время (иногда даже неявно) при использовании ООП.

и если мы переопределим методы SetWidth и SetHeight для Square, то с чем это может быть связано?

Потому что вы не можете разумно переопределить их Square. Потому что квадрат не может быть «изменен по размеру как любой прямоугольник». Когда высота прямоугольника изменяется, ширина остается неизменной. Но когда высота квадрата изменяется, ширина должна измениться соответственно. Проблема не только в том, что ее можно изменить, но и в обоих измерениях независимо друг от друга.

Deduplicator
источник
Во многих языках вам даже не нужна Rect r = s;строка, вы можете просто, doSomethingWith(s)и среда выполнения будет использовать любые вызовы sдля разрешения любых виртуальных Squareметодов.
Патрик М
1
@PatrickM Вам не нужно это на любом нормальном языке, который имеет подтип. Я включил эту строку в экспозицию, чтобы быть явным.
Так что переопределите setWidthи setHeightизмените ширину и высоту.
Приближается к
@ValekHalfHeart Это именно тот вариант, который я рассматриваю.
7
@ValekHalfHeart: Это именно то нарушение принципа подстановки Лискова, которое будет преследовать вас и заставит вас провести несколько бессонных ночей, пытаясь найти странную ошибку два года спустя, когда вы забыли, как код должен был работать.
Ян Худек
9

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

Проблема прямоугольника и квадрата не очень хороший способ представить Лискова. Он пытается объяснить широкий принцип, используя пример, который на самом деле довольно тонкий, и идет вразрез с одним из наиболее распространенных интуитивных определений во всей математике. Некоторые называют это проблемой Эллипс-Круга по этой причине, но это только немного лучше, насколько это идет. Лучше всего сделать небольшой шаг назад, используя то, что я называю проблемой Parallelogram-Rectangle. Это делает вещи намного проще для понимания.

Параллелограмм - это четырехугольник с двумя парами параллельных сторон. У этого также есть две пары конгруэнтных углов. Нетрудно представить объект Parallelogram по этим направлениям:

class Parallelogram {
    function getSideA() {};
    function getSideB() {};
    function getAngleA() {};
    function getAngleB() {};
    function setSideA(newLength) {};
    function setSideB(newLength) {};
    function setAngleA(newAngle) {};
    function setAngleB(newAngle) {};
}

Один общий способ думать о прямоугольнике - это параллелограмм с прямыми углами. На первый взгляд может показаться, что Rectangle является хорошим кандидатом для наследования от Parallelogram , так что вы можете повторно использовать весь этот вкусный код. Тем не мение:

class Rectangle extends Parallelogram {
    function getSideA() {};
    function getSideB() {};
    function getAngleA() {};
    function getAngleB() {};
    function setSideA(newLength) {};
    function setSideB(newLength) {};

    /* BUG: Liskov violations ahead */
    function setAngleA(newAngle) {};
    function setAngleB(newAngle) {};
}

Почему эти две функции вводят ошибки в Rectangle? Проблема в том, что вы не можете изменить углы в прямоугольнике : они определены как всегда равными 90 градусам, и поэтому этот интерфейс фактически не работает для Rectangle, наследуемого от Parallelogram. Если я поменяю Rectangle на код, который ожидает параллелограмм, и этот код попытается изменить угол, почти наверняка будут ошибки. Мы взяли что-то, что было доступно для записи в подклассе, и сделали его доступным только для чтения, и это нарушение Лискова.

Теперь, как это применимо к квадратам и прямоугольникам?

Когда мы говорим, что вы можете установить значение, мы обычно имеем в виду нечто более сильное, чем просто возможность записать значение в него. Мы подразумеваем определенную степень исключительности: если вы установите значение, а затем исключите некоторые чрезвычайные обстоятельства, оно останется на этом уровне, пока вы не установите его снова. Существует множество вариантов использования значений, в которые можно записывать значения, но они не остаются установленными, но также существует много случаев, которые зависят от значения, которое остается на месте после его установки. И вот тут мы сталкиваемся с другой проблемой.

class Square extends Rectangle {
    function getSideA() {};
    function getSideB() {};
    function getAngleA() {};
    function getAngleB() {};

    /* BUG: More Liskov violations */
    function setSideA(newLength) {};
    function setSideB(newLength) {};

    /* Liskov violations inherited from Rectangle */
    function setAngleA(newAngle) {};
    function setAngleB(newAngle) {};
}

Наш класс Square унаследовал ошибки от Rectangle, но у него есть некоторые новые. Проблема с setSideA и setSideB заключается в том, что ни один из них больше не является действительно устанавливаемым: вы все равно можете записать значение в любой из них, но оно изменится из-под вас, если записать другое. Если я поменяю местами параллелограмм в коде, который зависит от возможности устанавливать стороны независимо друг от друга, он будет в шоке.

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

Ложка
источник
8

Подтип о поведении.

Чтобы тип Bбыл подтипом типа A, он должен поддерживать каждую операцию, Aподдерживаемую этим типом, с одной и той же семантикой (причудливый разговор о «поведении»). Используя обоснование, что каждый B является A , не работает - совместимость поведения имеет решающее значение. Большую часть времени "B является разновидностью A" совпадает с "B ведет себя как A", но не всегда .

Пример:

Рассмотрим множество действительных чисел. В любом языке, мы можем ожидать , что они поддерживают операции +, -, *и /. Теперь рассмотрим множество натуральных чисел ({1, 2, 3, ...}). Понятно, что каждое положительное целое число также является действительным числом. Но является ли тип натуральных чисел подтипом типа действительных чисел? Давайте посмотрим на четыре операции и посмотрим, ведут ли положительные целые числа так же, как действительные числа:

  • +: Мы можем добавить положительные целые числа без проблем.
  • -: Не все вычитания натуральных чисел приводят к натуральным числам. Например 3 - 5.
  • *: Мы можем умножать натуральные числа без проблем.
  • /: Мы не можем всегда делить положительные целые числа и получать положительное целое число. Например 5 / 3.

Таким образом, несмотря на то, что натуральные числа являются подмножеством действительных чисел, они не являются подтипами. Аналогичный аргумент может быть сделан для целых чисел конечного размера. Очевидно, что каждое 32-разрядное целое число также является 64-разрядным целым числом, но оно 32_BIT_MAX + 1даст вам разные результаты для каждого типа. Так что, если я дал вам какую-то программу, и вы изменили тип каждой 32-битной целочисленной переменной на 64-битное целое, есть большая вероятность, что программа будет вести себя по-разному (что почти всегда означает, что неправильно ).

Конечно, вы можете определить +32-битные числа, чтобы получить 64-разрядное целое число, но теперь вам придется резервировать 64-битное пространство каждый раз, когда вы добавляете два 32-разрядных числа. Это может или не может быть приемлемым для вас в зависимости от ваших потребностей памяти.

Почему это важно?

Важно, чтобы программы были правильными. Это, пожалуй, самое важное свойство для программы. Если программа верна для некоторого типа A, единственный способ гарантировать, что программа продолжит быть верным для некоторого подтипа, B- это если вести Bсебя как Aво всех отношениях.

Таким образом, у вас есть тип Rectangles, спецификация которого гласит, что его стороны могут быть изменены независимо. Вы написали несколько программ, которые используют Rectanglesи предполагают, что реализация соответствует спецификации. Затем вы ввели подтип, Squareчьи стороны не могут быть изменены независимо. В результате большинство программ, изменяющих размер прямоугольников, теперь будут ошибаться.

Doval
источник
6

Если квадрат является типом прямоугольника, то почему квадрат не наследуется от прямоугольника? Или почему это плохой дизайн?

Перво-наперво, спросите себя, почему вы думаете, что квадрат - это прямоугольник.

Конечно, большинство людей узнали об этом в начальной школе, и это казалось бы очевидным. Прямоугольник - это четырехсторонняя фигура с углами 90 градусов, и квадрат отвечает всем этим свойствам. Так разве квадрат не является прямоугольником?

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

Поэтому, прежде чем вы скажете «квадрат - это тип прямоугольника», вы должны сначала спросить себя, основано ли это на критериях, которые меня интересуют .

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

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

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

В этом случае не моделируйте их как таковые. Почему ты? Это не приносит вам ничего, кроме ненужных ограничений.

Это будет полезно только в том случае, если мы создадим объект Square, и если мы переопределим методы SetWidth и SetHeight для Square, чем возникнет такая проблема?

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

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

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

Многие ответы на этот вопрос сосредоточены на подтипах, касающихся группового поведения , потому что они являются правилами .

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

Кормак Мулхолл
источник
Если переменные типа Rectangleиспользуются только для представления значений , класс может иметь возможность Squareнаследовать Rectangleи полностью соблюдать свой контракт. К сожалению, многие языки не делают различий между переменными, которые инкапсулируют значения, и переменными, которые идентифицируют сущности.
суперкат
Возможно, но зачем тогда вообще беспокоиться. Суть проблемы прямоугольника / квадрата заключается не в том, чтобы попытаться выяснить, как заставить работать отношение «квадрат - это прямоугольник», а в том, чтобы понять, что отношения на самом деле не существуют в контексте, в котором вы используете объекты. (поведенчески) и как предупреждение о том, что не нужно навязывать ненужные отношения в вашем домене.
Кормак Мулхолл,
Или, говоря по-другому: не пытайтесь согнуть ложку. Это невозможно. Вместо этого только попытайтесь осознать, что ложки нет. :-)
Кормак Мулхолл,
1
Наличие неизменяемого Squareтипа, который наследуется от неизменяемого Rectnagleтипа, может быть полезным, если существуют некоторые виды операций, которые можно выполнять только над квадратами. В качестве реалистичного примера концепции рассмотрим ReadableMatrixтип [базовый тип, прямоугольный массив, который может храниться различными способами, в том числе редко), и ComputeDeterminantметод. Возможно, имеет смысл ComputeDeterminantработать только с ReadableSquareMatrixтипом, производным от ReadableMatrixкоторого, я бы посчитал примером Squareпроизводного от a Rectangle.
суперкат
5

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

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

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

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

  • определить Square как базовый класс, с widthпараметром и resize(double factor)изменением ширины на заданный коэффициент
  • определить класс Rectangle и подкласс Square, потому что он добавляет еще один атрибут heightи переопределяет его resizeфункцию, которая вызывает, super.resizeа затем изменяет размер высоты с помощью заданного коэффициента

С точки зрения программирования, в Square нет ничего, чего нет у Rectangle. Нет смысла делать квадрат как подкласс Rectangle.

Дунайский моряк
источник
+1 То, что квадрат - это особый вид прямоугольника в математике, не означает, что в ОО то же самое.
Ловис
1
Квадрат - это квадрат, а прямоугольник - это прямоугольник. Отношения между ними должны сохраняться и в моделировании, или у вас довольно плохая модель. Реальные проблемы: 1) если вы сделаете их изменяемыми, вы больше не будете моделировать квадраты и прямоугольники; 2) предполагая, что только из-за того, что между двумя типами объектов сохраняются некоторые отношения типа «есть», вы можете без разбора заменять один другим.
Доваль
4

Потому что с помощью LSP создание отношения наследования между двумя и переопределением, setWidthа setHeightтакже обеспечение того, что квадрат имеет как одно и то же, вводит в заблуждение и неинтуитивное поведение. Допустим, у нас есть код:

Rectangle r = createRectangle(); // create rectangle or square here
r.setWidth(10);
r.setHeight(20);
print(r.getWidth()); // expect to print 10
print(r.getHeight()); // expect to print 20

Но если метод createRectangleвернулся Square, потому что это возможно благодаря Squareнаследованию от Rectange. Тогда ожидания нарушаются. Здесь, с этим кодом, мы ожидаем, что установка ширины или высоты приведет только к изменению ширины или высоты соответственно. Смысл ООП состоит в том, что когда вы работаете с суперклассом, вы ничего не знаете о подклассе. И если подкласс изменяет поведение так, что оно идет вразрез с ожиданиями, которые мы имеем в отношении суперкласса, то существует высокая вероятность возникновения ошибок. И такие ошибки трудно отлаживать и исправлять.

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

Euphoric
источник
2

LSP говорит, что все, что наследуется, Rectangleдолжно быть Rectangle. То есть он должен делать все, что Rectangleделает.

Вероятно, в документации Rectangleнаписано, что поведение Rectangleименованного rвыглядит следующим образом:

r.setWidth(10);
r.setHeight(20);
print(r.getWidth());  // prints 10

Если ваш Квадрат не имеет такого поведения, то он не ведет себя как Rectangle. Таким образом, LSP говорит, что это не должно наследоваться от Rectangle. Язык не может применить это правило, потому что он не может помешать вам сделать что-то неправильно в переопределении метода, но это не значит, что «все в порядке, потому что язык позволяет мне переопределять методы» - это убедительный аргумент для этого!

Теперь, это было бы возможно , чтобы написать документацию Rectangleтаким образом , что это не означает , что приведенный выше код печатает 10, в этом случае , может быть , ваш Squareможет быть Rectangle. Вы можете увидеть документацию, в которой говорится что-то вроде: «это делает X. Более того, реализация в этом классе делает Y». Если так, то у вас есть хороший пример для извлечения интерфейса из класса и разграничения того, что интерфейс гарантирует, и того, что класс гарантирует в дополнение к этому. Но когда люди говорят, что «изменяемый квадрат не является изменяемым прямоугольником, тогда как неизменный квадрат является неизменяемым прямоугольником», они в основном предполагают, что приведенное выше действительно является частью разумного определения изменяемого прямоугольника.

Стив Джессоп
источник
это, кажется, просто повторяет пункты, объясненные в ответе, опубликованном 5 часов назад
gnat
@gnat: вы бы предпочли, чтобы я отредактировал этот другой ответ примерно до такой краткости? ;-) Я не думаю, что смогу, не удаляя пункты, которые, по-видимому, другие респонденты считают необходимыми для ответа на вопрос, а я считаю, что это не так.
Стив Джессоп
1

Подтипы и, соответственно, ОО-программирование часто полагаются на Принцип замещения Лискова, согласно которому любое значение типа А может использоваться там, где требуется Б, если А <= В. Это в значительной степени аксиома в ОО-архитектуре, т.е. предполагается, что все подклассы будут иметь это свойство (и если нет, то подтипы ошибочны и должны быть исправлены).

Однако оказывается, что этот принцип либо нереалистичен / непредставителен для большей части кода, либо действительно невозможен для выполнения (в нетривиальных случаях)! Эта проблема, известная как проблема прямоугольника или прямоугольника ( http://en.wikipedia.org/wiki/Circle-ellipse_problem ), является одним из известных примеров того, насколько трудно ее решить .

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

В качестве примера см. Http://okmij.org/ftp/Computation/Subtyping/

Warbo
источник