Що може піти не так, якщо порушено принцип заміни Ліскова?


27

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


6
Що може піти не так, якщо не дотримуватися LSP? Найгірший сценарій: ви нарешті викликаєте Code-thulhu! ;)
FrustratedWithFormsDesigner

1
Як автор цього оригінального питання, я мушу додати, що це було досить академічне питання. Хоча порушення можуть спричинити помилки в коді, у мене ніколи не було серйозних помилок чи технічного обслуговування, які я можу привести до порушення LSP.
Пол Т Девіс

2
@Paul Отже, у вас ніколи не було проблем із вашими програмами через складні ієрархії ОО (які ви не розробляли самостійно, але, можливо, довелося продовжувати), коли контракти порушували ліворуч і право люди, які не були впевнені у меті базового класу почати з? Я заздрю ​​тобі! :)
Андрес Ф.

@PaulTDavies тяжкість наслідку залежить від того, чи користувачі (програмісти, які користуються бібліотекою) мають детальні знання про реалізацію бібліотеки (тобто мають доступ до та ознайомлюються з кодом бібліотеки.) Зрештою користувачі поставлять десятки умовних перевірок або будуватимуть обгортки навколо бібліотеки для обліку не-LSP (поведінки, що залежить від класу). Найгірший сценарій трапиться, якщо бібліотека є комерційним продуктом із закритим кодом.
rwong

@Andres і rwong, проілюструйте, будь ласка, ці проблеми з відповіддю. Прийнята відповідь в значній мірі підтримує Пола Девіс у тому, що наслідки здаються незначними (Виняток), які швидко помітяться та усунуться, якщо у вас є хороший компілятор, статичний аналізатор або мінімальний одиничний тест.
user949300

Відповіді:


31

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

Тепер при виклику Close () на Задачі є ймовірність, що виклик не вдасться, якщо це ProjectTask зі запущеним статусом, коли він не був би, якщо це була базова задача.

Уявіть, якщо ви:

public void ProcessTaskAndClose(Task taskToProcess)
{
    taskToProcess.Execute();
    taskToProcess.DateProcessed = DateTime.Now;
    taskToProcess.Close();
}

У цьому методі час від часу виклик .Close () вибухне, тому тепер, виходячи з конкретної реалізації похідного типу, ви повинні змінити спосіб поведінки цього методу, як би писався цей метод, якби в Task не було підтипів, які могли б бути передав цей метод.

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


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

@Songo: Не обов'язково: це може, але ці методи "недоступні" від базового вказівника (або довідкового чи змінної чи будь-якої мови, якою ви його використовуєте), і вам потрібна інформація про час виконання, щоб запитати, який тип об'єкта має перш ніж ви зможете викликати ці функції. Але це питання, яке сильно пов'язане з синтаксисом та семантикою мов.
Еміліо Гаравалья

2
Ні. Це для тих випадків, коли на дочірній клас посилається так, ніби це тип батьківського класу, і в цьому випадку члени, які не оголошені в батьківському класі, є недоступними.
Chewy Gumball

1
@Phil Yep; це визначення тісної зв'язку: Зміна однієї речі спричиняє зміни до інших речей. Клас із слабким сполученням може змінити його реалізацію, не вимагаючи від вас змінити код поза нею. Ось чому договори хороші, вони вказують вам, як не вимагати змін споживачів вашого об'єкта: Виконуйте договір, і споживачі не потребуватимуть жодних змін, таким чином, досягається вільне сполучення. Коли ваші споживачі повинні кодувати вашу реалізацію, а не ваш контракт, це жорстке з'єднання, і це потрібно при порушенні LSP.
Джиммі Хоффа

1
@ user949300 Успіх будь-якого програмного забезпечення для виконання своєї роботи - це не міра якості, довгострокових або короткострокових витрат. Принципи проектування - це спроба запровадити вказівки щодо скорочення довгострокових витрат на програмне забезпечення, а не для того, щоб програмне забезпечення «працювало». Люди можуть дотримуватися всіх принципів, які хочуть, поки не в змозі реалізувати робоче рішення, або не дотримуватися жодного та застосовувати робоче рішення. Хоча колекції java можуть працювати для багатьох людей, це не означає, що вартість роботи з ними в довгостроковій перспективі настільки дешева, як і могла.
Джиммі Хоффа

13

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

LSP у wikipedia державах

  • Передумови не можна посилити в підтипі.
  • Послідовності не можуть бути ослаблені в підтипі.
  • Інваріанти супертипу повинні зберігатися в підтипі.

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


1
Чи можете ви придумати якісь конкретні приклади, щоб продемонструвати це?
Марк Бут

1
@MarkBooth Проблема кола-еліпс / квадрат-прямокутник може бути корисною для його демонстрації; стаття з Вікіпедії - це гарне місце для початку: en.wikipedia.org/wiki/Circle-ellipse_problem
Ед Гастінгс

7

Розглянемо класичний випадок із літопису запитань про інтерв'ю: ти отримав Круг із Еліпса. Чому? Тому що коло IS-AN еліпс, звичайно!

За винятком ... еліпс має дві функції:

Ellipse.set_alpha_radius(d)
Ellipse.set_beta_radius(d)

Зрозуміло, що їх слід переосмислити для кола, оскільки коло має рівномірний радіус. У вас є дві можливості:

  1. Після виклику set_alpha_radius або set_beta_radius обидва встановлюються на однакову суму.
  2. Після виклику set_alpha_radius або set_beta_radius об'єкт більше не є Колом.

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

some_function(Ellipse byref e)

Уявіть, що деяка функція викликає e.set_alpha_radius. Але оскільки e справді було Колом, на диво також встановлений його бета-радіус.

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


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

2
У суто функціональному світі (з незмінними об'єктами) метод set_alpha_radius (d) мав би еліпс зворотного типу (і в еліпсі, і в класі кола).
Джорджіо

@Giorgio Так, я мав би зазначити, що ця проблема виникає лише з об'єктами, що змінюються.
Kaz Dragon

@KazDragon: Чому хтось замінить еліпс об'єктом кола, коли ми знаємо, що еліпс НЕ коло? Якщо хтось це робить, у них немає правильного розуміння сутностей, які вони намагаються моделювати. Але дозволяючи цю заміну, чи ми не заохочуємо слабке розуміння основної системи, яку ми намагаємося моделювати в нашому програмному забезпеченні, і, таким чином, створюючи погане програмне забезпечення на ділі?
maverick

@maverick Я вважаю, що ви прочитали відносини, які я описав назад. Запропоноване є - відношення є навпаки: коло - це еліпс. Зокрема, коло - це еліпс, де альфа та бета-радіуси однакові. Отже, можливо, очікується, що будь-яка функція, яка очікує еліпса як параметр, може однаковою мірою взяти коло. Розглянемо Calcu_area (Еліпс). Проходження кола до цього дало б такий же результат. Але проблема полягає в тому, що поведінка мутаційних функцій Еліпса не є замінною для тих, хто в Circle.
Kaz Dragon

6

Словами мирянина:

Ваш код буде мати дуже багато випадків CASE / переключення .

Кожен з цих випадків CASE / switch потребуватиме нових випадків, що додаються час від часу, тобто база коду не є такою масштабованою та підтримуваною, як повинна бути.

LSP дозволяє коду працювати як апаратне забезпечення:

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


2
-1: все навколо погана відповідь
Томас Едінг

3
@ Томас Я не згоден. Це хороша аналогія. Він говорить про те, щоб не порушити очікувань, саме про це і полягає LSP. (хоча частина про справу / перемикач трохи слабка, я згоден)
Андрес Ф.

2
І тоді Apple зламала LSP, змінюючи роз'єми. Ця відповідь живе далі.
Маг

Я не розумію, що заяви переключення стосуються LSP. якщо ви маєте на увазі перехід, typeof(someObject)щоб вирішити, що вам "дозволено робити", тоді впевнено, але це зовсім інший антидіапазон.
сара

Різке зменшення кількості операторів комутації є бажаним побічним ефектом ЛСП. Оскільки об'єкти можуть стояти за будь-яким іншим об’єктом, що розширює той самий інтерфейс, не потрібно опікуватися особливими випадками.
Тулан Кордова

1

щоб навести приклад із реального життя з програмою UndoManager Java

він успадковує, з AbstractUndoableEditконтракту якого зазначено, що він має 2 стани (скасовано та перероблено) і може переходити між ними за допомогою одиночних дзвінків до undo()таredo()

однак UndoManager має більше станів і діє як буфер скасування (кожен виклик undoскасовує деякі, але не всі зміни, послаблюючи постумова)

це призводить до гіпотетичної ситуації, коли ви додаєте UndoManager до CompoundEdit перед тим, як викликати, end()тоді виклик скасувати на цьому CompoundEdit призведе до виклику undo()кожного редагування, залишаючи частково скасованими зміни

Я згорнув своє, UndoManagerщоб уникнути цього (я, мабуть, мушу перейменувати його на UndoBufferхоч)


1

Приклад: Ви працюєте з фреймворком інтерфейсу, і ви створюєте власний користувальницький інтерфейс управління, підкласируючи Controlбазовий клас. ControlБазовий клас визначає метод , getSubControls()який повинен повертати колекцію вкладених елементів управління (якщо такі є). Але ви перекриваєте метод фактичного повернення списку народження президентів США.

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


0

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

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

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


0

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

Я маю Class A, що походить від Class B. Class Aі Class Bподілитися купою властивостей, що Class Aпереосмислює власну реалізацію. Налаштування або отримання Class Aресурсу впливає на встановлення або отримання точно такої самої власності Class B.

public Class A
{
    public virtual string Name
    {
        get; set;
    }
}

Class B : A
{
    public override string Name
    {
        get
        {
            return TranslateName(base.Name);
        }
        set
        {
            base.Name = value;
            FunctionWithSideEffects();
        }
    }
}

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

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

У коді використовуються як об'єкти, так Class Aі Class Bя не можу просто зробити Class Aабстрактним, щоб змусити людей використовувати Class B.

Є кілька дуже корисних функцій утиліти, які працюють, Class Aта інші дуже корисні функції утиліти, які працюють на Class B. В ідеалі я хотів би мати можливість використовувати будь-яку функцію утиліти, яка може працювати Class Aна Class B. Багато функцій, які беруть на себе, Class Bможна було б легко Class Aвиконати, якби не порушення LSP.

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

Що мені доведеться зробити, щоб це виправити, - це створити NameTranslatedвластивість, яка буде Class Bверсією Nameвластивості, і дуже, дуже ретельно змінити кожне посилання на похідне Nameмайно, щоб використовувати мою нову NameTranslatedвластивість. Однак, помиляючись навіть в одному з цих посилань, вся програма може підірватись.

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


Що буде, якби у похідному класі ви затінили успадковану властивість іншим видом речі, який мав те саме ім’я [наприклад, вкладений клас] та створив нові ідентифікатори BaseNameта отримав TranslatedNameдоступ до стилю класу A Nameта значення B-B? Тоді будь-яка спроба доступу Nameдо змінної типу Bбуде відхилена з помилкою компілятора, так що ви можете переконатися, що всі посилання були перетворені в одну з інших форм.
supercat

Я більше не працюю на тому місці. Це було б дуже незручно виправити. :-)
Стівен

-4

Якщо ви хочете відчути проблему порушення LSP, подумайте, що станеться, якщо у вас є лише .dll / .jar базового класу (без вихідного коду) і вам доведеться створити новий похідний клас. Ви ніколи не можете виконати це завдання.


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