Чому наслідування Square з прямокутника буде проблематичним, якщо ми перекриємо методи SetWidth та SetHeight?


105

Якщо квадрат - це тип прямокутника, то чому квадрат не може успадкувати прямокутник? Або чому це поганий дизайн?

Я чув, як люди кажуть:

Якщо ви зробили квадратний похід від прямокутника, то квадрат повинен бути корисним у будь-якому місці, де ви очікуєте прямокутник

У чому тут проблема? І чому площа може бути корисною в будь-якому місці, де ви очікуєте прямокутник? Він може бути корисним лише тоді, коли ми створимо об'єкт Square, і якщо ми перекриємо методи SetWidth і SetHeight для Square, ніж чому б виникала проблема?

Якщо у базового класу прямокутників у вас були методи SetWidth та SetHeight, а якщо ваш посилання на прямокутник вказав на квадрат, то SetWidth та SetHeight не мають сенсу, оскільки встановлення одного змінює інше на відповідне. У цьому випадку Квадрат не дає тесту заміщення Ліскова прямокутником, і абстракція того, що наслідувати квадрат прямокутника, є поганою.

Чи може хтось пояснити вищезазначені аргументи? Знову ж таки, якби ми переоцінили методи SetWidth і SetHeight в Square, чи не вирішить це питання?

Я також чув / читав:

Справжня проблема полягає в тому, що ми не моделюємо прямокутники, а скоріше "змінні прямокутники", тобто прямокутники, ширина чи висота яких можна змінити після створення (і ми все ще вважаємо це одним і тим же об'єктом). Якщо ми подивимось на клас прямокутника таким чином, то зрозуміло, що квадрат не є "прямозамінним прямокутником", тому що квадрат не може бути перероблений і все-таки бути квадратом (загалом). Математично ми не бачимо проблеми, оскільки змінність навіть не має сенсу в математичному контексті

Тут я вважаю, що "повторно помітний" - це правильний термін. Прямокутники є "повторно значущими", як і квадрати. Я щось пропускаю у наведеному аргументі? Квадрат можна змінити за розміром, як і будь-який прямокутник.


15
Це питання видається жахливо абстрактним. Є газильйон способів використання класів та успадкування, незалежно від того, чи зробити якийсь клас успадкованим від якогось класу - хороша ідея, як правило, залежить в основному від того, як ви хочете використовувати ці класи. Без практичного випадку я не бачу, як на це питання можна отримати відповідну відповідь.
aaaaaaaaaaaa

2
Використовуючи певний здоровий глузд, слід нагадати, що квадрат є прямокутником, тому, якщо об’єкт вашого квадратного класу не може бути використаний там, де потрібен прямокутник, це, мабуть, якийсь недолік дизайну програми.
Cthulhu

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

І так далі, і так далі.

Цей анекдот заснований на реальних ситуаціях, з якими щодня стикаються програмісти. Порушення принципу заміни Ліскова може ввести дуже тонкі помилки, які вибираються лише через роки після їх написання, до цього часу виправлення порушення порушить купу речей, а якщо не виправити це, розгніватиме вашого найбільшого клієнта.

Є два реалістичні способи виправити цю проблему.

Перший спосіб - зробити прямокутник незмінним. Якщо користувач прямокутника не може змінити властивості довжини та ширини, ця проблема усувається. Якщо ви хочете прямокутник з різною довжиною і шириною, ви створите новий. Квадрати можуть радісно успадковувати прямокутники.

Другий спосіб - розірвати ланцюг успадкування між квадратами та прямокутниками. Якщо квадрат визначений як один SideLengthвластивість, а прямокутники мають a Lengthі Widthвластивість, і немає спадщини, неможливо випадково розбити речі, очікуючи прямокутник і отримати квадрат. У C # термінах ви могли б отримати sealсвій клас прямокутників, який гарантує, що всі прямокутники, які ви коли-небудь отримуєте, - це фактично прямокутники.

У цьому випадку мені подобається спосіб виправлення проблеми "непорушні об'єкти". Ідентичність прямокутника - його довжина та ширина. Має сенс, що коли ти хочеш змінити ідентичність об'єкта, те, що ти насправді хочеш, - це новий об’єкт. Якщо ви втрачаєте старого клієнта і отримуєте нового клієнта, ви не змінюєте Customer.Idполе зі старого клієнта на новий, ви створюєте нового Customer.

Порушення принципу заміни Ліскова поширені в реальному світі, здебільшого тому, що багато коду там написано людьми, які некомпетентні / під тиском часу / не хвилюються / роблять помилки. Це може і призводить до дуже неприємних проблем. У більшості випадків ви хочете віддавати перевагу складу над спадщиною .


7
Лісков - це одне, а зберігання - інше питання. У більшості реалізацій, екземпляр Square, що успадковує від Rectangle, потребує місця для зберігання двох вимірів, навіть якщо потрібен лише один.
el.pescado

29
Блискуче використання історії для ілюстрації пункту
Рорі Хантер

29
Хороша історія, але я не згоден. Випадок використання був: зміна площі прямокутника. До виправлення слід додати метод перезапису "ChangeArea" до прямокутника, який спеціалізується на Square. Це не розірве ланцюг успадкування, зробить явне те, що користувач хотів зробити, і не спричинило б помилку, введену вашим згаданим виправленням (що було б зафіксовано у належній області постановки).
Рой Т.

33
@RoyT. Чому Прямокутник повинен знати, як встановити свою площу? Це властивість, що випливає повністю з довжини та ширини. І ще більше до того, який розмір слід змінити - довжину, ширину чи обидва?
cHao

32
@Roy T. Це дуже приємно сказати, що ви вирішили б проблему по-іншому, але факт полягає в тому, що це приклад - хоч і спрощений - реальних ситуацій, з якими щодня стикаються розробники, зберігаючи застарілі продукти. І навіть якщо ви застосували цей метод, це не зупинить спадкоємців, що порушують LSP і вводять помилки, подібні до цього. Ось чому майже кожен клас у .NET-рамках запечатаний.
Стівен

30

Якщо всі ваші об’єкти непорушні, проблем немає. Кожна площа - це також прямокутник. Усі властивості прямокутника - це також властивості квадрата.

Проблема починається, коли ви додаєте можливість змінювати об'єкти. Або насправді - коли ви починаєте передавати аргументи об’єкту, а не лише читати одержувачі властивостей.

Існують модифікації, які ви можете зробити в прямокутнику, який підтримує всі інваріанти вашого класу «Прямокутник», але не всі інваріанти «Квадрат» - наприклад, змінюючи ширину чи висоту. Раптом поведінка прямокутника - це не лише його властивості, це також можливі його модифікації. Це не тільки те, що ви виходите з прямокутника, це також те, що ви можете помістити .

Якщо у вашому Прямокутнику є метод, setWidthякий зафіксовано як зміна ширини та не зміна висоти, то квадрат не може мати сумісний метод. Якщо змінити ширину, а не висоту, результат більше не є дійсним Квадрат. Якщо ви вирішили змінити і ширину, і висоту квадрата під час використання setWidth, ви не реалізуєте специфікацію прямокутника setWidth. Ви просто не можете перемогти.

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

Це питання ко-дисперсії проти контра-дисперсії.

Методи відповідного підкласу - той, де екземпляри можуть бути використані у всіх випадках, коли очікується надклас, вимагає кожного методу:

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

Отже, повернення до прямокутника та квадрата: чи площа може бути підкласом прямокутника, повністю залежить від того, якими методами володіє Прямокутник.

Якщо Прямокутник має індивідуальні задачі для ширини та висоти, Квадрат не складе хорошого підкласу.

Так само, якщо ви зробите, що деякі методи є спільними варіантами аргументів, як, наприклад, compareTo(Rectangle)прямокутник і compareTo(Square)квадрат, у вас виникне проблема використання квадрата як прямокутника.

Якщо ви спроектуєте ваш квадрат і прямокутник як сумісні, він, швидше за все, спрацює, але вони повинні бути розроблені разом, або я буду робити справу, що це не спрацює.


"Якщо всі ваші об'єкти незмінні, немає жодної проблеми" - це, мабуть, нерелевантне твердження в контексті цього питання, в якому явно згадуються сетери на ширину та висоту
gnat

11
Мені це
здалося

14
@gnat Я б стверджував, що це актуально, оскільки реальна цінність питання розпізнається, коли між двома типами існує дійсне співвідношення підтипів. Це залежить від того, для яких операцій декларується супертип, тому варто вказати на проблему, якщо мутаторні методи відійдуть.
Довал

1
@gnat також, сетери є мутаторами , тому lrn по суті каже: "Не робіть цього, і це не проблема". Я, мабуть, погоджуюся з незмінністю для простих типів, але ви добре зазначаєте: Для складних об'єктів проблема не така проста.
Патрік М

1
Розглянемо так, яка поведінка гарантована класом «Прямокутник»? Щоб ви могли змінювати ширину та висоту НЕЗАЛЕЖНО один від одного. (тобто метод setWidth і setHeight). Тепер, якщо Square походить від прямокутника, Square повинен гарантувати таку поведінку. Оскільки квадрат не може гарантувати такої поведінки, це погана спадщина. Однак якщо методи setWidth / setHeight видалено з класу Rectangle, то такої поведінки немає, і отже, ви можете отримати клас Square від Rectangle.
Нітін Бхід

17

Тут є багато хороших відповідей; Відповідь Стівена, зокрема, добре пояснює, чому порушення принципу заміни призводять до конфліктів у реальному світі.

Я подумав, що я можу коротко поговорити про конкретну проблему прямокутників та квадратів, а не використовувати її як метафору для інших порушень LSP.

Існує додаткова проблема щодо прямокутника квадрат-є-особливого-роду, який рідко згадується, і це: чому ми зупиняємося з квадратами та прямокутниками ? Якщо ми готові сказати, що квадрат - це особливий вид прямокутника, то, безумовно, ми також повинні бути готові сказати:

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

Що за земля повинні бути всі стосунки тут? Мови, засновані на успадкуванні класів, такі як C # або Java, не були розроблені таким чином, щоб представляти подібні складні взаємозв'язки з кількома різними обмеженнями. Найкраще просто повністю уникати цього питання, не намагаючись представити всі ці речі як класи з підтиповими відносинами.


3
Якщо об'єкти фігури незмінні, то можна мати IShapeтип, який включає в себе обмежувальне поле, і може бути намальований, масштабований і серіалізований, і мати IPolygonпідтип із методом повідомлення кількості вершин і методом повернення IEnumerable<Point>. Тоді можна було б мати IQuadrilateralпідтип, який походить від IPolygon, IRhombusі IRectangleпоходить від цього, і ISquareпоходить від IRhombusі IRectangle. Змінюваність викидає все у вікно, і багатократне успадкування не працює з класами, але я думаю, що це добре з незмінними інтерфейсами.
supercat

Я фактично не згоден з Еріком тут (недостатньо для -1, хоча!). Усі ці стосунки (можливо) актуальні, як згадує @supercat; це лише проблема YAGNI: ви не реалізуєте її, поки не знадобиться.
Марк Херд

Дуже гарна відповідь! Повинно бути вище.
andrew.fox

1
@MarkHurd - це не проблема ЯГНІ: запропонована ієрархія, заснована на спадщині, має форму описаної таксономії, але вона не може бути написана для гарантування відносин, які її визначають. Як IRhombusгарантується, що всі Pointповернуті з Enumerable<Point>визначених IPolygonзначень відповідають ребрам однакової довжини? Оскільки реалізація IRhombusінтерфейсу сама по собі не гарантує, що конкретний об'єкт - ромб, успадкування не може бути відповіддю.
А. Реджер

14

З математичної точки зору квадрат - це прямокутник. Якщо математик модифікує квадрат, щоб він більше не дотримувався квадратного контракту, він перетворюється на прямокутник.

Але в OO-дизайні це проблема. Об'єктом є те, що воно є, і це включає поведінку , а також стан. Якщо я тримаю квадратний об’єкт, але хтось інший модифікує його на прямокутник, це порушує контракт площі з моєї власної вини. Через це трапляються всілякі погані речі.

Ключовим фактором тут є мутабельність . Чи може форма змінюватися, коли вона побудована?

  • Змінні: якщо фігурам дозволяється змінювати один раз побудований, квадрат не може мати співвідношення з прямокутником. Договір прямокутника включає обмеження, що протилежні сторони повинні бути однакової довжини, але суміжні сторони не повинні бути. Квадрат повинен мати чотири рівні сторони. Змінення квадрата через прямокутний інтерфейс може порушити договір квадрата.

  • Невідмінне: якщо фігури не можуть змінитись один раз побудовані, то квадратний об'єкт також повинен завжди виконувати договір прямокутника. Квадрат може мати відношення прямокутника.

В обох випадках можна попросити квадрат створити нову форму на основі його стану з однією або декількома змінами. Наприклад, можна сказати "створити новий прямокутник на основі цього квадрата, за винятком того, що протилежні сторони 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
@Stephen Погодився. Власне, зробити їх незмінними - це розумно робити незалежно від проблем із підтипом. Немає ніяких причин змушувати їх змінюватись - це не важче побудувати новий квадрат або прямокутник, ніж мутувати його, так навіщо відкривати цю банку глистів? Тепер вам не доведеться турбуватися про згладжування / побічні ефекти, і ви можете використовувати їх як ключі до карт / диктовок, якщо це необхідно. Деякі скажуть "продуктивність", на яку я скажу "передчасна оптимізація", поки вони фактично не виміряли і не довели, що гаряча точка знаходиться у коді форми.
Довал

Вибачте, хлопці, було пізно, і я дуже втомився, коли написав відповідь. Я переписав це, щоб сказати, що я насправді мав на увазі, суть якого - незмінність.

13

І чому площа може бути корисною в будь-якому місці, де ви очікуєте прямокутник?

Тому що це частина того, що означає підтип (див. Також: принцип заміщення Ліскова). Ви можете це зробити, потрібно зробити це:

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

Ви фактично робите це весь час (іноді навіть неявніше) під час використання OOP.

і якщо ми переймемо методи SetWidth і SetHeight для Square, ніж чому б виникала проблема?

Тому що ти не можеш розумно змінити їх Square. Тому що квадрат не може бути "переозброєний як будь-який прямокутник". Коли висота прямокутника змінюється, ширина залишається однаковою. Але коли змінюється висота квадрата, ширина повинна відповідно змінюватися. Проблема не просто в тому, що вона може бути повторно розроблена, вона може бути повторно помітна в обох вимірах незалежно.


Багато мов вам навіть не потрібна Rect r = s;лінія, ви можете просто, doSomethingWith(s)і час виконання буде використовувати будь-які дзвінки, sщоб вирішити будь-які віртуальні Squareметоди.
Патрік М

1
@PatrickM Вам не потрібна жодна розумна мова, яка має підтипи. Я включив цей рядок для експозиції, щоб бути явним.

Тож переставляйте setWidthта setHeightзмінюйте і ширину, і висоту.
Наближення

@ValekHalfHeart Саме такий варіант я розглядаю.

7
@ValekHalfHeart: Саме порушення Лісковського принципу заміни призведе до переслідування і змусить вас провести кілька безсонних ночей, намагаючись знайти дивну помилку через два роки, коли ви забули, як повинен працювати код.
Ян Худек

9

Те, що ви описуєте, суперечить тому, що називається Принципом заміни Ліскова . Основна ідея LSP полягає в тому, що щоразу, коли ви використовуєте екземпляр певного класу, ви завжди зможете поміняти місцями замість будь-якого підкласу цього класу, не вводячи помилок.

Проблема прямокутника-квадрата насправді не дуже вдалий спосіб представити Лісков. Він намагається пояснити широкий принцип, використовуючи приклад, який насправді є досить тонким, і відповідає одному з найпоширеніших інтуїтивних визначень у всій математиці. Деякі називають це проблемою Еліпса-Круга з цієї причини, але лише трохи краще, наскільки це йде. Кращий підхід - зробити невеликий крок назад, використовуючи те, що я називаю проблемою Паралелограма-Прямокутник. Це робить речі набагато простішими для розуміння.

Паралелограм - це чотирикутник з двома парами паралельних сторін. Він також має дві пари конгруентних кутів. Не важко уявити об’єкт Паралелограма за цими лініями:

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

Один поширений спосіб думати прямокутник - це паралелограм з прямими кутами. На перший погляд, це може здатися зробити Прямокутник хорошим кандидатом для успадкування від 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) {};
}

Чому ці дві функції вводять помилки в прямокутник? Проблема полягає в тому, що ви не можете змінити кути в прямокутнику : вони визначаються як завжди 90 градусів, і цей інтерфейс насправді не працює для прямокутника, успадкованого від Parallelogram. Якщо я поміняю Прямокутник на код, який очікує Паралелограма, і цей код намагатиметься змінити кут, майже напевно з’являться помилки. Ми взяли щось, що можна було записати в підкласі, і зробили його лише для читання, і це порушення Ліскова.

Тепер, як це стосується квадратів та прямокутників?

Коли ми кажемо, що ви можете встановити значення, ми зазвичай маємо на увазі щось трохи сильніше, ніж просто можливість вписати у нього значення. Ми маємо на увазі певну ступінь ексклюзивності: якщо ви встановите значення, якщо заборонити якісь надзвичайні обставини, воно буде залишатися на цьому значенні, поки ви не встановите його знову. Існує велика кількість застосувань для значень, до яких можна записати, але не залишаються встановленими, але також є багато випадків, які залежать від значення, яке залишається там, де воно колись ви його встановите. І саме тут ми стикаємося з іншою проблемою.

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 полягає в тому, що жодне з них вже не є справді встановленим: ви все одно можете записати значення в будь-яке, але воно зміниться з-під вас, якщо запишеться інше. Якщо я поміняю це на паралелограм у коді, який залежить від того, чи зможемо встановлювати сторони незалежно одна від одної, це вийде.

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


8

Підтипізація стосується поведінки.

Щоб тип Bбув підтипом типу A, він повинен підтримувати кожну операцію, яку Aпідтримує тип, з однаковою семантикою (фантазії для «поведінки»). Використовуючи обгрунтування , що кожен B є а зовсім НЕ працює - сумісність поведінки має вирішальне слово. Більшу частину часу "B - це різновид A", що перетинається з "B поводиться як A", але не завжди .

Приклад:

Розглянемо набір дійсних чисел. У будь-якій мові, ми можемо очікувати , що вони підтримують операції +, -, *і /. Тепер розглянемо набір натуральних чисел ({1, 2, 3, ...}). Зрозуміло, що кожне додатне ціле число - це також дійсне число. Але чи є тип натуральних чисел підтипом типу дійсних чисел? Давайте подивимось на чотири операції та побачимо, чи дійсні натуральні числа поводяться так само, як дійсні числа:

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

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

Звичайно, ви можете визначити +для 32-бітових ints, щоб результат був 64-бітним цілим числом, але тепер вам потрібно буде резервувати 64 біти місця щоразу, коли ви додаєте два 32-бітні числа. Це може бути або неприйнятним для вас залежно від потреб у вашій пам'яті.

Чому це має значення?

Важливо, щоб програми були правильними. Це, мабуть, найважливіша властивість програми. Якщо програма є правильною для певного типу A, єдиний спосіб гарантувати, що програма буде і надалі правильним для певного підтипу, Bце якщо він Bведе себе так, як Aвсіляко.

Таким чином, у вас є тип Rectangles, специфікація якого говорить про те, що його сторони можна змінювати незалежно. Ви написали деякі програми, які використовують Rectanglesта передбачають, що реалізація слідує специфікації. Потім ви представили підтип, названий Squareсторонами якого не можна змінити розміри самостійно. Як результат, більшість програм, які змінюють розмір прямокутників, зараз помиляться.


6

Якщо квадрат - це тип прямокутника, ніж чому не може наслідувати Квадрат прямокутника? Або чому це поганий дизайн?

Спочатку спочатку запитайте себе, чому ви думаєте, що квадрат є прямокутником.

Звичайно, більшість людей дізналися про це в початковій школі, і це здавалося б очевидним. Прямокутник - це двостороння форма з кутами 90 градусів, а квадрат відповідає всім цим властивостям. Тож чи не квадрат прямокутник?

Річ у тому, що все залежить від того, які твої початкові критерії для групування об'єктів, у якому контексті ти переглядаєш ці об’єкти. За геометрією фігури класифікуються за властивостями їх точок, ліній та ангелів.

Тож перш ніж ви навіть скажете "квадрат - це тип прямокутника", спочатку ви повинні запитати себе, чи це ґрунтується на критеріях, які мене цікавлять .

У переважній більшості випадків це зовсім не те, про що ви дбаєте. Більшість систем, що моделюють форми, такі як графічні інтерфейси, графіка та відеоігри, стосуються не в першу чергу геометричного групування об'єкта, а саме поведінки. Ви коли-небудь працювали над системою, яка мала значення, щоб квадрат був типом прямокутника в геометричному сенсі. Що б це вам навіть дало, знаючи, що у нього 4 сторони та кути 90 градусів?

Ви не моделюєте статичну систему, ви моделюєте динамічну систему, де все буде відбуватися (форми створюватимуться, руйнуються, змінюються, малюються тощо). У цьому контексті ви дбаєте про спільну поведінку між об'єктами, тому що ваша основна проблема полягає в тому, що ви можете зробити з формою, які правила потрібно дотримуватися, щоб все-таки мати цілісну систему.

У цьому контексті квадрат, безумовно, не є прямокутником , тому що правила, які регулюють спосіб зміни квадрата, не збігаються з прямокутником. Отже, вони не є однотипними.

У такому випадку не моделюйте їх як таких. Чому б ти? Це не отримує від вас нічого, крім зайвого обмеження.

Він може бути корисним лише тоді, коли ми створимо об'єкт Square, і якщо ми перекриємо методи SetWidth і SetHeight для Square, ніж чому б виникала проблема?

Якщо ви це зробите, хоч ви практично заявляєте в коді, що це не те саме. Ваш код сказав би, що квадрат поводиться таким чином, а прямокутник поводиться таким чином, але вони все одно однакові.

Вони, очевидно, не однакові в контексті, який вас хвилює, тому що ви просто визначили дві різні форми поведінки. То чому б робити вигляд, що вони однакові, якщо вони схожі лише в контексті, який вам не цікавий?

Це підкреслює значну проблему, коли розробники приходять до домену, який вони бажають моделювати. Настільки важливо уточнити, який контекст вас цікавить, перш ніж починати думати про об’єкти в домені. Який аспект вас цікавить. Тисячі років тому греки дбали про спільні властивості ліній та ангелів фігур, і групували їх на основі цих. Це не означає, що ви змушені продовжувати групування, якщо це не те, про що ви дбаєте (що в 99% часу моделювання в програмному забезпеченні вас не хвилює).

Дуже багато відповідей на це питання зосереджуються на підтипах, що стосуються групування поведінки , оскільки вони викликають у них правила .

Але так важливо розуміти, що ви робите це не лише для того, щоб дотримуватися правил. Ви робите це, тому що в переважній більшості випадків це саме те, що вас насправді хвилює. Вам байдуже, якщо квадрат і прямокутник мають однакові внутрішні ангели. Ви дбаєте про те, що вони можуть робити, будучи квадратами та прямокутниками. Ви дбаєте про поведінку об’єктів, тому що ви моделюєте систему, орієнтовану на зміну системи на основі правил поведінки об'єктів.


Якщо змінні типу Rectangleвикористовуються лише для представлення значень , то, можливо, клас Squareуспадковує Rectangleі повністю дотримується його контракту. На жаль, багато мов не роблять різниці між змінними, які інкапсулюють значення, та тими, які ідентифікують сутність.
суперкарт

Можливо, але тоді навіщо турбувати в першу чергу. Суть проблеми прямокутника / квадрата полягає не в тому, щоб спробувати і з'ясувати, як зробити так, щоб взаємозв'язок "квадрат є прямокутником", а в тому, щоб зрозуміти, що відносини насправді не існують у контексті, яким ви користуєтеся об'єктами (поведінково) та як попередження про не надто нав'язування нерелевантних стосунків у вашому домені.
Кормак Малхолл

Або кажучи іншим способом: не намагайтеся зігнути ложку. Це неможливо. Натомість намагайтеся лише усвідомити правду, що ложки немає. :-)
Cormac Mulhall

1
Наявність непорушного Squareтипу, який успадковується від непорушного Rectnagleтипу, може бути корисним, якщо були б якісь операції, які можна було робити тільки на квадратах. Як реалістичний приклад концепції розглянемо ReadableMatrixтип [базовий тип прямокутного масиву, який може зберігатися різними способами, включаючи рідко], та ComputeDeterminantметод. Можливо, має сенс ComputeDeterminantпрацювати лише з ReadableSquareMatrixтипом, похідним від ReadableMatrixякого я вважаю приклад Squareпоходження від a Rectangle.
supercat

5

Якщо квадрат - це тип прямокутника, ніж чому не може наслідувати Квадрат прямокутника?

Проблема полягає в думці, що якщо речі якимось чином пов’язані з реальністю, вони повинні бути пов'язані точно так само, як і моделювання.

Найважливіше в моделюванні - це визначити загальні ознаки та загальну поведінку, визначити їх у базовому класі та додати додаткові атрибути в дочірніх класах.

Проблема з вашим прикладом полягає в тому, що це абсолютно абстрактно. Поки ніхто не знає, для чого ви плануєте використовувати ці заняття, важко здогадатися, який дизайн ви повинні зробити. Але якщо ви дійсно хочете мати лише висоту, ширину та розмір, було б більш логічним:

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

З точки зору програмування, у Square прямо немає прямокутника. Немає сенсу робити квадрат як підклас Прямокутник.


+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. Тоді очікування порушуються. Тут, з цим кодом, ми очікуємо, що встановлення ширини чи висоти призведе до зміни лише відповідно ширини чи висоти. Суть OOP полягає в тому, що, працюючи з надкласом, ви маєте нульові знання про будь-який підклас під ним. І якщо підклас змінить поведінку так, щоб це суперечило очікуванням щодо суперкласу, то існує велика ймовірність появи помилок. І такі помилки важко налагодити і виправити.

Однією з головних ідей щодо ООП є те, що саме поведінка, а не дані успадковуються (що також є однією з основних помилкових уявлень ІМО). А якщо подивитися на квадрат і прямокутник, вони самі не мають такої поведінки, яку ми могли б співвідносити у спадковому відношенні.


2

Що говорить ЛСП - це те, що все, що успадковується, 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

Підтипи та розширення програми OO часто покладаються на Принцип заміщення Ліскова, що будь-яке значення типу A можна використовувати там, де потрібне B, якщо A <= B. Це в значній мірі є аксіомою в архітектурі ОО, тобто. передбачається, що всі підкласи матимуть цю властивість (а якщо ні, то підтипи є помилковими та потребують виправлення).

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

Зауважте, що ми могли б реалізовувати все більше і більше еквівалентних за спостереженням Квадратів та Прямокутників, але лише викидаючи все більше та більше функціональних можливостей, поки розмежування не буде марним.

Як приклад, див. Http://okmij.org/ftp/Computation/Subtyping/

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.