Який приклад принципу заміни Ліскова?


908

Я чув, що Принцип заміщення Ліскова (ЛСП) є основним принципом об'єктно-орієнтованого проектування. Що це таке і які приклади його використання?


Більше прикладів дотримання та порушення LSP тут
StuartLC

1
Це запитання має нескінченну кількість хороших відповідей і тому занадто широке .
Raedwald

Відповіді:


892

Чудовий приклад, що ілюструє LSP (наданий дядьком Боб у подкасті, який я чув недавно), - це те, як іноді щось, що звучить правильно на природній мові, не дуже працює в коді.

У математиці а Square- це a Rectangle. Адже це спеціалізація прямокутника. Значення "є" змушує вас моделювати це за допомогою спадкування. Однак якщо в коді, який ви Squareотримали Rectangle, то ви Squareповинні використовуватись у будь-якому місці, де ви очікуєте Rectangle. Це спричиняє деяку дивну поведінку.

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

введіть тут опис зображення

Вам слід ознайомитись з іншими безцінними мотиваційними плакатами ТВОРИХ Принципів .


19
@ m-shar Що робити, якщо це непорушний прямокутник, такий, що замість SetWidth та SetHeight ми замість нього використовуємо методи GetWidth та GetHeight?
Pacerier

139
Мораль розповіді: моделюйте свої заняття на основі поведінки, а не за властивостями; моделюйте свої дані на основі властивостей, а не на поведінці. Якщо вона поводиться як качка, це, звичайно, птах.
Скліввз

193
Ну, квадрат очевидно - це тип прямокутника в реальному світі. Чи зможемо ми моделювати це в нашому коді, залежить від специфікації. Що вказує LSP, це те, що поведінка підтипу має відповідати поведінці базового типу, як визначено в специфікації базового типу. Якщо специфікація базового типу прямокутника говорить, що висоту та ширину можна встановити незалежно, то LSP каже, що квадрат не може бути підтипом прямокутника. Якщо специфікація прямокутника говорить, що прямокутник незмінний, то квадрат може бути підтипом прямокутника. Вся справа в підтипах, що підтримують поведінку, вказану для базового типу.
SteveT

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

14
У мене є одне питання щодо принципу. Чому була б проблема, якщо вона Square.setWidth(int width)була реалізована так this.width = width; this.height = width;:? У цьому випадку гарантується, що ширина дорівнює висоті.
MC Імператор

488

Принцип заміщення Ліскова (LSP, ) - це поняття в об'єктно-орієнтованому програмуванні, яке говорить:

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

В основі LSP лежить інтерфейс і контракти, а також як вирішити, коли розширити клас порівняно з іншою стратегією, такою як композиція для досягнення своєї мети.

Найбільш ефективний спосіб , який я бачив , щоб проілюструвати цей момент був в Head First ООА & D . Вони представляють сценарій, коли ви розробник проекту зі створення рамки для стратегічних ігор.

Вони представляють клас, який представляє дошку, яка виглядає приблизно так:

Діаграма класів

Усі методи приймають координати X і Y в якості параметрів, щоб знайти положення плитки в двовимірному масиві Tiles. Це дозволить розробнику ігор керувати підрозділами в дошці під час гри.

Книга продовжує змінювати вимоги, щоб сказати, що робота в ігрових кадрах повинна також підтримувати 3D-ігрові дошки для розміщення ігор, які мають політ. Так ThreeDBoardвводиться клас, який розширюється Board.

На перший погляд це здається хорошим рішенням. Boardзабезпечує як Heightі Widthвластивості, так і ThreeDBoardзабезпечує вісь Z.

Де воно розпадається - це коли ви дивитесь на всіх інших членів, які дісталися у спадок Board. Методи AddUnit, GetTile, GetUnitsі так далі, все приймати як X і Y параметри в Boardкласі , але ThreeDBoardпотрібен параметр Z , а також.

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

Можливо, нам слід знайти інший підхід. Замість того, щоб розширюватись Board, ThreeDBoardслід складатися з Boardоб’єктів. Один Boardоб’єкт на одиницю осі Z.

Це дозволяє нам використовувати хороші об'єктно-орієнтовані принципи, такі як інкапсуляція та повторне використання, і не порушує LSP.


10
Дивіться також проблему "Circle-Ellipse" у Вікіпедії для подібного, але більш простого прикладу.
Брайан

Рекомендація від @NotMySelf: "Я думаю, що приклад - це просто продемонструвати, що успадковування з плати не має сенсу в контексті ThreeDBoard, і всі підписи методу мають сенс із віссю Z".
Контанго

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

5
Це антилісковський приклад. Лісков змушує нас виводити прямокутник з площі. Клас більше параметрів від класу з меншими параметрами. І ви добре показали, що це погано. Це справді хороший жарт, коли ви позначили як відповідь і були запрошені 200 разів на антиліскову відповідь на питання Ліскова. Чи справді принцип Ліскова є помилкою?
Гангнус

3
Я бачив, як спадкування працює неправильно. Ось приклад. Базовим класом повинен бути 3DBoard, а похідний клас класу. Дошка все ще має вісь Z макс (Z) = Min (Z) = 1
Paulustrious

169

Заміна - це принцип об'єктно-орієнтованого програмування, який стверджує, що в комп'ютерній програмі, якщо S є підтипом T, то об'єкти типу T можуть бути замінені об'єктами типу S

зробимо простий приклад на Java:

Поганий приклад

public class Bird{
    public void fly(){}
}
public class Duck extends Bird{}

Качка може літати через це - птах, але що з цього:

public class Ostrich extends Bird{}

Страус - птах, але він не вміє літати, клас страусів - це підтип класу Птах, але він не може використовувати метод мухи, це означає, що ми порушуємо принцип LSP.

Хороший приклад

public class Bird{
}
public class FlyingBirds extends Bird{
    public void fly(){}
}
public class Duck extends FlyingBirds{}
public class Ostrich extends Bird{} 

3
Гарний приклад, але що б ви зробили, якщо у клієнта є Bird bird. Ви повинні кинути об'єкт на FlyingBirds, щоб використовувати муху, що не приємно, правда?
Муді

17
Ні. Якщо клієнт має Bird bird, це означає, що він не може використовувати fly(). Це воно. Проходження a Duckне змінює цей факт. Якщо клієнт має FlyingBirds bird, то навіть якщо його пройдуть, Duckвін завжди повинен працювати однаково.
Стів Чамайлард

9
Чи не це також може слугувати хорошим прикладом для розбиття інтерфейсів?
Сахарш

Відмінний приклад Спасибі людина
Абдельхаді Абдо

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

132

LSP стосується інваріантів.

Класичний приклад наводиться наступним псевдокодовим оголошенням (реалізації пропущено):

class Rectangle {
    int getHeight()
    void setHeight(int value)
    int getWidth()
    void setWidth(int value)
}

class Square : Rectangle { }

Зараз у нас є проблема, хоча інтерфейс відповідає. Причина в тому, що ми порушили інваріанти, що випливає з математичного визначення квадратів та прямокутників. Спосіб роботи геттерів та сеттерів Rectangleповинен задовольнити наступний інваріант:

void invariant(Rectangle r) {
    r.setHeight(200)
    r.setWidth(100)
    assert(r.getHeight() == 200 and r.getWidth() == 100)
}

Однак цей інваріант повинен бути порушений правильним виконанням Square, тому він не є дійсною заміною Rectangle.


35
А отже, і складність використання "ОО" для моделювання всього, що ми могли б хотіти насправді моделювати.
DrPizza

9
@DrPizza: Абсолютно. Однак дві речі. По-перше, такі відносини все ще можна моделювати в OOP, хоча і неповно, або використовувати складніші об'їзди (виберіть те, що відповідає вашій проблемі). По-друге, немає кращої альтернативи. Інші відображення / моделювання мають ті ж чи подібні проблеми. ;-)
Конрад Рудольф

7
@ NickW У деяких випадках (але не у вищенаведеному) ви можете просто перевернути ланцюг спадкування - логічно кажучи, 2D-точка - це 3D-точка, де третій вимір не враховується (або 0 - всі точки лежать на одній площині в 3D простір). Але це, звичайно, не дуже практично. Взагалі, це один із випадків, коли спадкування насправді не допомагає, і між сутностями не існує природних зв’язків. Моделюйте їх окремо (принаймні, я не знаю кращого способу).
Конрад Рудольф

7
OOP призначений для моделювання поведінки, а не даних. Ваші заняття порушують інкапсуляцію ще до порушення LSP.
Sklivvz

2
@AustinWBryan Yep; чим довше я працюю в цій галузі, тим більше я прагну використовувати успадкування лише для інтерфейсів і абстрактних базових класів, а для решти композиції. Іноді це трохи більше роботи (набравши мудрості), але це дозволяє уникнути цілого ряду проблем, і це широко повторюється порадами інших досвідчених програмістів.
Конрад Рудольф

77

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

Деякі відповідні частини статті (зауважте, що другий приклад сильно ущільнений):

Простий приклад порушення LSP

Одним з найбільш очевидних порушень цього принципу є використання інформації про тип C ++ про час виконання (RTTI) для вибору функції на основі типу об'єкта. тобто:

void DrawShape(const Shape& s)
{
  if (typeid(s) == typeid(Square))
    DrawSquare(static_cast<Square&>(s)); 
  else if (typeid(s) == typeid(Circle))
    DrawCircle(static_cast<Circle&>(s));
}

Очевидно, що DrawShapeфункція погано сформована. Він повинен знати про всі можливі похідні Shapeкласу, і він повинен змінюватися щоразу, коли Shapeстворюються нові похідні . Дійсно, багато хто розглядає структуру цієї функції як анафему об'єктно-орієнтованого дизайну.

Площа та прямокутник, більш тонке порушення.

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

class Rectangle
{
  public:
    void SetWidth(double w) {itsWidth=w;}
    void SetHeight(double h) {itsHeight=w;}
    double GetHeight() const {return itsHeight;}
    double GetWidth() const {return itsWidth;}
  private:
    double itsWidth;
    double itsHeight;
};

[...] Уявіть, що одного дня користувачі вимагають можливість маніпулювати квадратами, крім прямокутників. [...]

Зрозуміло, що квадрат - це прямокутник для всіх звичайних намірів і цілей. Оскільки відношення ISA має місце, то логічно моделювати Square клас, похідний від якого Rectangle. [...]

Squareбуде успадковано функції SetWidthта SetHeight. Ці функції є абсолютно невідповідними для а Square, оскільки ширина і висота квадрата однакові. Це має бути важливою підказкою, що з дизайном є проблеми. Однак є спосіб усунути проблему. Ми могли б перекрити SetWidthі SetHeight[...]

Але врахуйте таку функцію:

void f(Rectangle& r)
{
  r.SetWidth(32); // calls Rectangle::SetWidth
}

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

[...]


14
Шлях пізно, але я подумав, що це цікава цитата в цій роботі: Now the rule for the preconditions and postconditions for derivatives, as stated by Meyer is: ...when redefining a routine [in a derivative], you may only replace its precondition by a weaker one, and its postcondition by a stronger one. Якщо попередня умова класу дитини є сильнішою за попередню умову батьківського класу, ви не можете замінити дитину батьком, не порушивши попередньої умови. Звідси LSP.
користувач2023861

@ user2023861 Ви абсолютно праві. На цьому я напишу відповідь.
inf3rno

40

LSP необхідний там, де якийсь код вважає, що він викликає методи типу T, і може несвідомо називати методи типу S, де S extends T(тобто Sуспадковується, походить від або є підтипом супертипу T).

Наприклад, це відбувається, коли функція із вхідним параметром типу Tназивається (тобто викликається) зі значенням аргументу типу S. Або, де ідентифікатору типу T, присвоюється значення типу S.

val id : T = new S() // id thinks it's a T, but is a S

LSP вимагає, щоб очікування (тобто інваріанти) щодо методів типу T(наприклад Rectangle) не порушувалися, коли замість цього викликаються методи типу S(наприклад Square).

val rect : Rectangle = new Square(5) // thinks it's a Rectangle, but is a Square
val rect2 : Rectangle = rect.setWidth(10) // height is 10, LSP violation

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

class Rectangle( val width : Int, val height : Int )
{
   def setWidth( w : Int ) = new Rectangle(w, height)
   def setHeight( h : Int ) = new Rectangle(width, h)
}

class Square( val side : Int ) extends Rectangle(side, side)
{
   override def setWidth( s : Int ) = new Square(s)
   override def setHeight( s : Int ) = new Square(s)
}

LSP вимагає, щоб кожен метод підтипу Sповинен мати противаріантні параметри введення та коваріантний вихід.

Контраваріантний означає, що дисперсія суперечить напрямку спадкування, тобто типу Si, кожного вхідного параметра кожного методу підтипу S, повинна бути однаковою або супертипом типу Tiвідповідного вхідного параметра відповідного методу супертипу T.

Коваріація означає, що дисперсія знаходиться в одному напрямку спадкування, тобто типу So, виведення кожного методу підтипу S, повинно бути однаковим або підтипом типу Toвідповідного виходу відповідного методу супертипу T.

Це тому, що якщо абонент думає, що він має тип T, думає, що він викликає метод T, тоді він постачає аргументи (и) типу Tiі призначає вихід типу To. Коли він фактично викликає відповідний метод S, то кожному Tiвхідному аргументу присвоюється Siвхідний параметр, а Soвихід присвоюється типу To. Таким чином, якби Siне було противаріантним WTT, тоді можна було б призначити Tiпідтип, Xiякому не було б підтип .SiTi

Крім того, для мов (наприклад, Scala або Цейлон), які мають анотації дисперсії на місці визначення типових параметрів поліморфізму (тобто генерики), спів- або контрарсив дисперсійної анотації для кожного параметра типу типу Tповинен бути протилежним або однаковим напрямком відповідно до кожного вхідного параметра або виводу (кожного методу T), який має тип параметру типу.

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


Підтипування підходить там, де інваріанти можуть бути перераховані.

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

Typestate (див. Сторінку 3) оголошує та застосовує інваріанти стану, ортогональні типу. Альтернативно, інваріанти можуть бути застосовані шляхом перетворення тверджень у типи . Наприклад, щоб стверджувати, що файл відкритий перед його закриттям, тоді File.open () може повернути тип OpenFile, який містить метод close (), який недоступний у файлі. Хрестики-нулики API , може бути ще одним прикладом застосування друкувати для забезпечення інваріантів під час компіляції. Система типу може бути навіть комплектацією Тьюрінга, наприклад, Scala . Залежно типізовані мови та докази теореми формалізують моделі типізації вищого порядку.

Через необхідність семантики в абстрагуванні над розширенням , я очікую, що використання типізації для моделювання інваріантів, тобто уніфікованої денотаційної семантики вищого порядку, перевершує Typestate. "Розширення" означає необмежену, перестановлену композицію неузгодженого модульного розвитку. Оскільки мені здається, що є антитезою об'єднання і, таким чином, ступенів свободи, мати дві взаємозалежні моделі (наприклад, типи і Typestate) для вираження спільної семантики, які не можуть бути уніфіковані один з одним для розширюваної композиції . Наприклад, розширення, подібне до виразу Expression , було уніфіковано в області підтипу, перевантаження функцій та параметрів параметричного введення тексту.

Моя теоретична позиція полягає в тому, що для існування знань (див. Розділ «Централізація сліпа і непридатна») ніколи не буде загальної моделі, яка зможе забезпечити 100% охоплення всіх можливих інваріантів на комп'ютерній мові, повністю завершеній Тьюрінгом. Щоб знання існували, несподіваних можливостей існує багато, тобто розлад та ентропія завжди повинні зростати. Це ентропічна сила. Щоб довести всі можливі обчислення потенційного розширення, слід апріорі обчислити все можливе розширення.

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

Інтерпретація цих теорем включає їх у узагальнене концептуальне розуміння ентропічної сили:

  • Теореми незавершеності Геделя : будь-яка формальна теорія, в якій можна довести всі арифметичні істини, є непослідовною.
  • Парадокс Рассела : кожне правило членства для набору, який може містити набір, або перераховує певний тип кожного члена, або містить себе. Таким чином, набори або не можуть бути розширені, або вони є необмеженою рекурсією. Наприклад, набір всього, що не є чайником, включає в себе себе, що включає в себе себе, що включає в себе і т.д. ... Таким чином, правило є непослідовним, якщо воно (може містити набір і) не перераховує конкретні типи (тобто допускає всі невизначені типи) і не дозволяє необмежене розширення. Це безліч множин, які не є членами самих себе. Ця нездатність бути одночасно послідовною та повністю переліченою у всіх можливих розширеннях є теоремами про незавершеність Геделя.
  • Принцип заміщення Ліскова : загалом, це нерозв'язна проблема, чи будь-який набір є підмножиною іншого, тобто успадкування взагалі не визначається.
  • Посилання Лінського : не можна визначити, що таке обчислення чогось, коли воно описується чи сприймається, тобто сприйняття (реальність) не має абсолютної точки відліку.
  • Теорема Коуза : немає ніякої зовнішньої точки відліку, тому будь-який бар'єр для необмежених зовнішніх можливостей виявиться невдалим.
  • Другий закон термодинаміки : весь Всесвіт (замкнена система, тобто все) прагне до максимального безладу, тобто до максимальних незалежних можливостей.

17
@Shelyby: Ви перемішали занадто багато речей. Речі не такі заплутані, як ви їх заявляєте. Значна частина ваших теоретичних тверджень стоїть на хитромудрих підставах, як-от "Для того, щоб знання існували, існують несподівані можливості, багато ..." І "загалом це нерозв'язна проблема, чи будь-який набір є підмножиною іншого, тобто успадкування взагалі не можна визначити ". Ви можете створити окремий блог для кожного з цих пунктів. У будь-якому разі, ваші твердження та припущення дуже сумнівні. Не слід використовувати речі, про які не знаєш!
aknon

1
@aknon У мене є блог, який пояснює ці питання більш глибоко. Моя модель TOE нескінченного простору - це необмежені частоти. Мене не бентежить, що рекурсивна індуктивна функція має відоме початкове значення з нескінченним кінцевим обмеженням, або коіндуктивна функція має невідоме кінцеве значення та відому початкову межу. Відносність - це проблема після введення рекурсії. Ось чому Тьюрінг повний еквівалентний необмеженій рекурсії .
Шелбі Мур III

4
@ShelbyMooreIII Ви їдете в занадто багатьох напрямках. Це не відповідь.
Soldalma

1
@Soldalma - це відповідь. Ви цього не бачите в розділі "Відповіді". Ваш - це коментар, оскільки він знаходиться в розділі коментарів.
Шелбі Мур III

1
Як ваше змішання зі світом скала!
Ехсан М. Кермані

24

Я бачу прямокутники та квадрати у кожній відповіді та як порушувати LSP.

Я хотів би показати, як LSP можна відповідати прикладом у реальному світі:

<?php

interface Database 
{
    public function selectQuery(string $sql): array;
}

class SQLiteDatabase implements Database
{
    public function selectQuery(string $sql): array
    {
        // sqlite specific code

        return $result;
    }
}

class MySQLDatabase implements Database
{
    public function selectQuery(string $sql): array
    {
        // mysql specific code

        return $result; 
    }
}

Ця конструкція відповідає LSP, оскільки поведінка залишається незмінною незалежно від реалізації, яку ми обираємо використовувати.

І так, ви можете порушити LSP у цій конфігурації, зробивши одну просту зміну, як-от так:

<?php

interface Database 
{
    public function selectQuery(string $sql): array;
}

class SQLiteDatabase implements Database
{
    public function selectQuery(string $sql): array
    {
        // sqlite specific code

        return $result;
    }
}

class MySQLDatabase implements Database
{
    public function selectQuery(string $sql): array
    {
        // mysql specific code

        return ['result' => $result]; // This violates LSP !
    }
}

Тепер підтипи не можна використовувати однаково, оскільки вони вже не дають однакового результату.


6
Приклад не порушує LSP лише до тих пір, поки ми обмежуємо семантику Database::selectQueryпідтримувати лише підмножину SQL, підтримувану усіма двигунами БД. Це навряд чи практично ... Однак, приклад все-таки легше зрозуміти, ніж більшість інших, що використовуються тут.
Palec

5
Цю відповідь я знайшов найлегше зрозуміти з решти.
Малькольм Сальвадор

23

Існує контрольний список, щоб визначити, чи ви порушуєте Лісков.

  • Якщо ви порушите один із наступних пунктів -> ви порушите Лісков.
  • Якщо ви не порушуєте жодного -> не можу нічого робити.

Контрольний список:

  • Ніяких нових винятків не слід викидати у похідний клас : Якщо ваш базовий клас кинув ArgumentNullException, тоді вашим підкласам було дозволено викидати виключення типу ArgumentNullException або будь-які винятки, отримані з ArgumentNullException. Кидання IndexOutOfRangeException - це порушення Ліскова.
  • Попередні умови не можуть бути посилені : припустимо, що ваш базовий клас працює з користувачем int. Тепер ваш підтип вимагає, щоб int був позитивним. Це посилено попередніми умовами, і тепер будь-який код, який працював ідеально раніше, з негативними вставками, порушується.
  • Пост-умови неможливо ослабити : Припустимо, що базовий клас вимагає, щоб всі з'єднання з базою даних були закриті до повернення методу. У своєму підкласі ви скасували цей метод і залишили з'єднання відкритим для подальшого використання. Ви ослабили пост-умови цього методу.
  • Інваріанти повинні бути збережені : Найважче і найболючіше обмеження, яке потрібно виконати. Інваріанти деякий час приховуються в базовому класі, і єдиний спосіб їх виявити - це прочитати код базового класу. По суті, ви повинні бути впевнені, коли ви перекриєте метод, що-небудь незмінне повинно залишатися незмінним після того, як ваш метод, який переосмислили. Найкраще, що я можу придумати, - це застосувати ці інваріантні обмеження в базовому класі, але це буде непросто.
  • Обмеження історії : При переопределенні методу ви не можете змінювати властивість, що не змінюється, в базовому класі. Погляньте на цей код, і ви побачите, що ім'я визначено як неможливо змінити (приватний набір), але SubType вводить новий метод, що дозволяє змінювати його (через відображення):

    public class SuperType
    {
        public string Name { get; private set; }
        public SuperType(string name, int age)
        {
            Name = name;
            Age = age;
        }
    }
    public class SubType : SuperType
    {
        public void ChangeName(string newName)
        {
            var propertyType = base.GetType().GetProperty("Name").SetValue(this, newName);
        }
    }
    

Є ще два пункти: Протилежність аргументів методу та Коваріація типів повернення . Але це неможливо в C # (я розробник C #), тому я не дбаю про них.

Довідка:


Я також розробник C #, і я скажу, що ваше останнє твердження не відповідає дійсності Visual Studio 2010, в рамках .Net 4.0. Коваріація типів повернення дозволяє отримати більш похідний тип повернення, ніж те, що було визначено інтерфейсом. Приклад: Приклад: IEbroume <T> (T covariant) IEnumerator <T> (T covariant) IQueyable <T> (T is covariant) IGrouping <TKey, TElement> (TKey and TElement are covariant) IComparer <T> (T є противаріантним) IEqualityComparer <T> (T є противаріантним) IComparable <T> (T є противаріантним
LCarter

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

22

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

У псевдопітона

class Base:
   def Foo(self, arg): 
       # *... do stuff*

class Derived(Base):
   def Foo(self, arg):
       # *... do stuff*

задовольняє LSP, якщо кожен раз, коли ви викликаєте Foo на похідному об'єкті, він дає точно такі ж результати, як і виклик Foo на об'єкт Base, доки аргумент буде однаковим.


9
Але ... якщо ти завжди отримуєш однакову поведінку, то який сенс мати похідний клас?
Леонід

2
Ви пропустили бал: це та ж спостерігається поведінка. Наприклад, ви можете замінити щось на продуктивність O (n) чимось функціонально еквівалентним, але на продуктивність O (lg n). Або ви можете замінити щось, що отримує доступ до даних, реалізованих за допомогою MySQL, і замінити їх на базу даних в пам'яті.
Чарлі Мартін

@Charlie Martin, кодування до інтерфейсу, а не реалізації - я це викопую. Це не є унікальним для ООП; Функціональні мови, такі як Clojure, також сприяють цьому. Навіть з точки зору Java або C #, я вважаю, що використання інтерфейсу, а не використання абстрактних ієрархій класу плюс класу було б природним для прикладів, які ви надаєте. Python не сильно набраний і насправді не має інтерфейсів, принаймні не явно. Моя складність полягає в тому, що я вже кілька років займаюся OOP, не дотримуючись SOLID. Тепер, коли я натрапив на це, це здається обмежуючим і майже суперечливим.
Гаміш Грубіян

Ну, вам потрібно повернутися назад і перевірити оригінальний папір Барбари. report-archive.adm.cs.cmu.edu/anon/1999/CMU-CS-99-156.ps Це насправді не зазначено в інтерфейсах, і це логічне відношення, яке є (або не має) в будь-якому мова програмування, яка має певну форму успадкування.
Чарлі Мартін

1
@HamishGrubijan Я не знаю, хто сказав вам, що Python не сильно набраний, але вони брехали вам (і якщо ви мені не вірите, заведіть перекладача Python і спробуйте 2 + "2"). Можливо, ви плутаєте "сильно набраний" зі "статично набраним"?
асмеурер

21

Якщо коротко розповісти, залишимо прямокутники прямокутників і квадратів, практичний приклад під час розширення батьківського класу, ви повинні або ЗАБЕЗПЕЧИТИ точний API для батьків, або розширити його.

Скажімо, у вас є базовий ItemRepository.

class ItemsRepository
{
    /**
    * @return int Returns number of deleted rows
    */
    public function delete()
    {
        // perform a delete query
        $numberOfDeletedRows = 10;

        return $numberOfDeletedRows;
    }
}

І підклас, що розширює його:

class BadlyExtendedItemsRepository extends ItemsRepository
{
    /**
     * @return void Was suppose to return an INT like parent, but did not, breaks LSP
     */
    public function delete()
    {
        // perform a delete query
        $numberOfDeletedRows = 10;

        // we broke the behaviour of the parent class
        return;
    }
}

Тоді ви можете мати клієнта, який працює з API Base ItemsRepository і покладається на нього.

/**
 * Class ItemsService is a client for public ItemsRepository "API" (the public delete method).
 *
 * Technically, I am able to pass into a constructor a sub-class of the ItemsRepository
 * but if the sub-class won't abide the base class API, the client will get broken.
 */
class ItemsService
{
    /**
     * @var ItemsRepository
     */
    private $itemsRepository;

    /**
     * @param ItemsRepository $itemsRepository
     */
    public function __construct(ItemsRepository $itemsRepository)
    {
        $this->itemsRepository = $itemsRepository;
    }

    /**
     * !!! Notice how this is suppose to return an int. My clients expect it based on the
     * ItemsRepository API in the constructor !!!
     *
     * @return int
     */
    public function delete()
    {
        return $this->itemsRepository->delete();
    }
} 

LSP порушується , коли підставляючи батьківський клас з суб брейків класу контракту АНІ в .

class ItemsController
{
    /**
     * Valid delete action when using the base class.
     */
    public function validDeleteAction()
    {
        $itemsService = new ItemsService(new ItemsRepository());
        $numberOfDeletedItems = $itemsService->delete();

        // $numberOfDeletedItems is an INT :)
    }

    /**
     * Invalid delete action when using a subclass.
     */
    public function brokenDeleteAction()
    {
        $itemsService = new ItemsService(new BadlyExtendedItemsRepository());
        $numberOfDeletedItems = $itemsService->delete();

        // $numberOfDeletedItems is a NULL :(
    }
}

Ви можете дізнатися більше про написання ремонту програмного забезпечення в моєму курсі: https://www.udemy.com/enterprise-php/


20

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

Коли я вперше прочитав про LSP, я припустив, що це мається на увазі в дуже суворому сенсі, по суті прирівнюючи його до реалізації інтерфейсу та безпечного для кастингу типу. Що означає, що LSP забезпечується або не забезпечується самою мовою. Наприклад, у цьому суворому сенсі ThreeDBoard, безумовно, заміняється на Board, що стосується компілятора.

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

Коротше кажучи, те, що означає для клієнтського коду "знати", що об'єкт за вказівником є ​​похідним типом, а не тип вказівника, не обмежується безпекою типу. Прихильність до LSP також перевіряється шляхом перевірки фактичної поведінки об'єктів. Тобто, вивчаючи вплив аргументів стану об’єкта та методів на результати викликів методу або типи винятків, викинутих з об’єкта.

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

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


19

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

Отже, у Ліскова є три основні правила:

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

  2. Правило методів: Виконання цих операцій є семантично обгрунтованим.

    • Слабші передумови: функції підтипу повинні приймати принаймні те, що супертип був прийнятим як вхідний, якщо не більше.
    • Посильніші постумови: Вони повинні створювати підмножину вихідних даних, що створюються методами супертипу.
  3. Правило властивостей: Це виходить за межі окремих викликів функцій.

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

Усі ці властивості потрібно зберегти, а додаткова функціональність підтипу не повинна порушувати властивості супертипу.

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

Джерело: Розробка програми на Java - Барбара Лісков


18

Важливим прикладом використання LSP є тестування програмного забезпечення .

Якщо у мене є клас A, який відповідає LSP підкласу B, я можу повторно використати тестовий набір B для тестування А.

Для повного випробування підкласу A, можливо, мені потрібно додати ще кілька тестових випадків, але як мінімум я можу повторно використовувати всі тестові випадки суперкласу B.

Спосіб усвідомити це, побудувавши те, що МакГрегор називає "Ієрархією паралельної перевірки": Мій ATestклас успадкує від BTest. Потім потрібна деяка форма ін'єкції, щоб забезпечити тестовий випадок із об'єктами типу A, а не типу B (це буде робити простий шаблон шаблону).

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

Дивіться також відповідь на питання Stackoverflow " Чи можу я реалізувати ряд тестів для багаторазового використання для тестування реалізації інтерфейсу? "


13

Проілюструємо на Java:

class TrasportationDevice
{
   String name;
   String getName() { ... }
   void setName(String n) { ... }

   double speed;
   double getSpeed() { ... }
   void setSpeed(double d) { ... }

   Engine engine;
   Engine getEngine() { ... }
   void setEngine(Engine e) { ... }

   void startEngine() { ... }
}

class Car extends TransportationDevice
{
   @Override
   void startEngine() { ... }
}

Тут немає жодної проблеми, правда? Автомобіль, безумовно, транспортний засіб, і тут ми можемо побачити, що він перекриває метод startEngine () свого суперкласу.

Додамо ще один транспортний пристрій:

class Bicycle extends TransportationDevice
{
   @Override
   void startEngine() /*problem!*/
}

Зараз все не йде так, як планувалося! Так, велосипед - це транспортний пристрій, однак він не має двигуна, отже, метод startEngine () неможливо реалізувати.

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

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

Ми можемо переробляти наш клас TransportDevice наступним чином:

class TrasportationDevice
{
   String name;
   String getName() { ... }
   void setName(String n) { ... }

   double speed;
   double getSpeed() { ... }
   void setSpeed(double d) { ... }
}

Тепер ми можемо розширити TransportationDevice для немоторизованих пристроїв.

class DevicesWithoutEngines extends TransportationDevice
{  
   void startMoving() { ... }
}

А також розширити TransportDevice для моторизованих пристроїв. Ось доцільніше додати об’єкт Engine.

class DevicesWithEngines extends TransportationDevice
{  
   Engine engine;
   Engine getEngine() { ... }
   void setEngine(Engine e) { ... }

   void startEngine() { ... }
}

Таким чином наш клас автомобілів стає більш спеціалізованим, дотримуючись Принципу заміни Лісков.

class Car extends DevicesWithEngines
{
   @Override
   void startEngine() { ... }
}

І наш клас велосипедів також відповідає Принципу заміни Лісков.

class Bicycle extends DevicesWithoutEngines
{
   @Override
   void startMoving() { ... }
}

9

Така рецептура ЛСП занадто сильна:

Якщо для кожного об'єкта o1 типу S є об’єкт o2 типу T таким, що для всіх програм, визначених у терміні T, поведінка P не змінюється, коли o1 заміщений на o2, то S є підтипом T.

Що в основному означає, що S - це інша, повністю інкапсульована реалізація абсолютно тієї ж речі, що і Т.

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

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


2
Ем, ця рецептура є власною Барбарою Ліськовою. Барбара Лісков, "Абстракція даних та ієрархія", Примітки SIGPLAN, 23,5 (травень, 1988). Це не "занадто сильно", це "абсолютно правильно", і це не має наслідку, який ви вважаєте, що це має. Він сильний, але має лише потрібну силу.
DrPizza

Тоді в реальному житті дуже мало підтипів :)
Демієн Поллет

3
"Поведінка незмінна" не означає, що підтип надасть вам точно такі ж конкретні значення результатів. Це означає, що поведінка підтипу відповідає тому, що очікується в базовому типі. Приклад: базовий тип Shape може мати метод малювання () і передбачає, що цей метод повинен надати форму. Два підтипи форми (наприклад, квадрат і коло) обидва реалізували б метод draw (), і результати виглядали б по-різному. Але поки поведінка (надання форми) відповідала заданій поведінці Shape, тоді Square і Cir будуть підтипами Shape відповідно до LSP.
SteveT

9

У дуже простому реченні ми можемо сказати:

Дочірній клас не повинен порушувати його базових характеристик. Він повинен бути з цим здатний. Можна сказати, що це те саме, що підтипу.


9

Принцип заміщення Ліскова (LSP)

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

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

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

Приклад:

Нижче наведено класичний приклад, для якого порушено Принцип заміщення Ліскова. У прикладі використовуються 2 класи: Прямокутник і Квадрат. Припустимо, що об’єкт Rectangle використовується десь у додатку. Розширюємо додаток і додаємо клас Square. Квадратний клас повертається заводським зразком, виходячи з деяких умов, і ми не знаємо, який саме тип об’єкта буде повернутий. Але ми знаємо, що це прямокутник. Отримуємо об’єкт прямокутника, встановлюємо ширину 5 і висоту 10 і отримуємо площу. Для прямокутника шириною 5 і висотою 10 площа повинна бути 50. Натомість результат буде 100

    // Violation of Likov's Substitution Principle
class Rectangle {
    protected int m_width;
    protected int m_height;

    public void setWidth(int width) {
        m_width = width;
    }

    public void setHeight(int height) {
        m_height = height;
    }

    public int getWidth() {
        return m_width;
    }

    public int getHeight() {
        return m_height;
    }

    public int getArea() {
        return m_width * m_height;
    }
}

class Square extends Rectangle {
    public void setWidth(int width) {
        m_width = width;
        m_height = width;
    }

    public void setHeight(int height) {
        m_width = height;
        m_height = height;
    }

}

class LspTest {
    private static Rectangle getNewRectangle() {
        // it can be an object returned by some factory ...
        return new Square();
    }

    public static void main(String args[]) {
        Rectangle r = LspTest.getNewRectangle();

        r.setWidth(5);
        r.setHeight(10);
        // user knows that r it's a rectangle.
        // It assumes that he's able to set the width and height as for the base
        // class

        System.out.println(r.getArea());
        // now he's surprised to see that the area is 100 instead of 50.
    }
}

Висновок:

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

Дивіться також: Відкрийте принцип закриття

Деякі подібні поняття для кращої структури: Конвенція щодо конфігурації


8

Принцип заміщення Ліскова

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

7

Додаток:
Цікаво, чому ніхто не написав про Інваріант, передумови та умови виконання базового класу, яким повинні відповідати похідні класи. Щоб похідний клас D повністю підходить базовому класу B, клас D повинен відповідати певним умовам:

  • В-варіанти базового класу повинні зберігатися похідним класом
  • Попередні умови базового класу не повинні посилюватися похідним класом
  • Пост-умови базового класу не повинні бути ослаблені похідним класом.

Таким чином, похідні повинні знати про вищезазначені три умови, встановлені базовим класом. Отже, правила підтипу заздалегідь прийняті. Що означає, що відносини "IS A" повинні дотримуватися лише тоді, коли підтип дотримується певних правил. Ці правила, у вигляді інваріантів, прекодицій та посткондицій, повинні вирішуватися офіційним договором на проектування .

Подальші дискусії з цього приводу доступні в моєму блозі: Принцип заміни Ліскова


6

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

Наприклад, якщо у нас є Catі в Dogклас , похідний від Animalкласу, будь-які функції , що використовують клас Animal повинні бути в змозі використати Catабо Dogй поводяться нормально.


4

Чи корисна б реалізація ThreeDBoard з точки зору масиву Ради?

Можливо, ви, можливо, захочете обробляти шматочки ThreeDBoard у різних площинах як Раду. У такому випадку ви можете вилучити інтерфейс (або абстрактний клас) для Board, щоб дозволити кілька реалізацій.

Що стосується зовнішнього інтерфейсу, то, можливо, ви захочете визначити інтерфейс плати як для TwoDBoard, так і для ThreeDBoard (хоча жоден із перерахованих вище методів не підходить).


1
Я думаю, що приклад - просто продемонструвати, що успадковувати з плати не має сенсу в контексті ThreeDBoard, і всі підписи методу мають сенс із віссю Z.
NotMyself

4

Квадрат - це прямокутник, де ширина дорівнює висоті. Якщо квадрат задає два різних розміри за шириною та висотою, він порушує квадрат інваріант. Це вирішується шляхом введення побічних ефектів. Але якщо прямокутник мав заданий розмір (висота, ширина) з попередньою умовою 0 <висота та 0 <ширина. Похідний метод підтипу вимагає висоти == ширина; сильніша передумова (і це порушує lsp). Це показує, що хоча квадрат є прямокутником, він не є дійсним підтипом, оскільки попередня умова посилена. Робота навколо (взагалі погана річ) викликає побічний ефект, і це послаблює стан поста (що порушує lsp). setWidth на базі має стан пост 0 <ширина. Отримане послаблює його з висотою == шириною.

Тому квадрат, що змінюється, не є прямокутником із зміною розміру.


4

Цей принцип був введений Барбарою Ліськовою в 1987 році і поширює принцип відкритого закриття, акцентуючи увагу на поведінці надкласу та його підтипів.

Його важливість стає очевидною, коли ми враховуємо наслідки її порушення. Розглянемо додаток, який використовує наступний клас.

public class Rectangle 
{ 
  private double width;

  private double height; 

  public double Width 
  { 
    get 
    { 
      return width; 
    } 
    set 
    { 
      width = value; 
    }
  } 

  public double Height 
  { 
    get 
    { 
      return height; 
    } 
    set 
    { 
      height = value; 
    } 
  } 
}

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

public class Square : Rectangle
{
} 

Однак, зробивши це, ми зіткнемось з двома проблемами:

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

public class Square : Rectangle
{
  public double SetWidth 
  { 
    set 
    { 
      base.Width = value; 
      base.Height = value; 
    } 
  } 

  public double SetHeight 
  { 
    set 
    { 
      base.Height = value; 
      base.Width = value; 
    } 
  } 
}

Тепер, коли хтось встановить ширину квадратного об’єкта, його висота відповідно зміниться і навпаки.

Square s = new Square(); 
s.SetWidth(1); // Sets width and height to 1. 
s.SetHeight(2); // sets width and height to 2. 

Давайте рухатимемося вперед і розглянемо цю іншу функцію:

public void A(Rectangle r) 
{ 
  r.SetWidth(32); // calls Rectangle.SetWidth 
} 

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

Однак, оголошуючи властивості сеттера віртуальними, ми зіштовхнемось з іншим порушенням, OCP. Фактично, створення похідного квадрата класу викликає зміни прямокутника базового класу.


3

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


1
Наведіть приклади коду на stackoverflow.
sebenalern

3

Скажімо, ми використовуємо прямокутник у своєму коді

r = new Rectangle();
// ...
r.setDimensions(1,2);
r.fill(colors.red());
canvas.draw(r);

У нашому класі геометрії ми дізналися, що квадрат - це особливий тип прямокутника, оскільки його ширина така ж довжина, що і його висота. Давайте складемо Squareклас на основі цієї інформації:

class Square extends Rectangle {
    setDimensions(width, height){
        assert(width == height);
        super.setDimensions(width, height);
    }
} 

Якщо ми замінимо Rectangleз Squareв нашому першому коді, то він зламається:

r = new Square();
// ...
r.setDimensions(1,2); // assertion width == height failed
r.fill(colors.red());
canvas.draw(r);

Це відбувається тому , що Squareє нове попередня умова у нас не було в Rectangleкласі: width == height. Відповідно до LSP, Rectangleекземпляри повинні бути замінені на Rectangleекземпляри підкласу. Це тому, що ці екземпляри передають перевірку типуRectangle екземплярів, і тому вони спричинять несподівані помилки у вашому коді.

Це був приклад для того, що "передумови не можна посилити в підтипі" у статті wiki . Отже, підводячи підсумок, порушення LSP, ймовірно, призведе до помилок у вашому коді в якийсь момент.


3

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

Дитячі класи ніколи не повинні порушувати визначення типів батьківського класу.

і наступний приклад допомагає краще зрозуміти LSP.

Без LSP:

public interface CustomerLayout{

    public void render();
}


public FreeCustomer implements CustomerLayout {
     ...
    @Override
    public void render(){
        //code
    }
}


public PremiumCustomer implements CustomerLayout{
    ...
    @Override
    public void render(){
        if(!hasSeenAd)
            return; //it isn`t rendered in this case
        //code
    }
}

public void renderView(CustomerLayout layout){
    layout.render();
}

Виправлення за допомогою LSP:

public interface CustomerLayout{
    public void render();
}


public FreeCustomer implements CustomerLayout {
     ...
    @Override
    public void render(){
        //code
    }
}


public PremiumCustomer implements CustomerLayout{
    ...
    @Override
    public void render(){
        if(!hasSeenAd)
            showAd();//it has a specific behavior based on its requirement
        //code
    }
}

public void renderView(CustomerLayout layout){
    layout.render();
}

2

Я закликаю вас прочитати статтю: Порушення принципу заміни Ліскова (LSP) .

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


2

ПРИНЦИП ЛІСКОВОГО ЗАМОВЛЕННЯ (З книги Марка Семана) говорить, що нам слід мати можливість замінити одну реалізацію інтерфейсу іншою, не порушуючи ні клієнта, ні імплементації. Цей принцип дозволяє вирішити вимоги, які виникають у майбутньому, навіть якщо ми можемо ' t передбачити їх сьогодні.

Якщо ми від'єднаємо комп’ютер від стіни (Впровадження), ні розетка (Інтерфейс), ні комп’ютер (Клієнт) не виходять з ладу (адже, якщо це портативний комп'ютер, він може навіть заряджатись від своїх акумуляторів протягом певного часу) . Однак, використовуючи програмне забезпечення, клієнт часто очікує, що послуга буде доступною. Якщо послугу було видалено, ми отримуємо NullReferenceException. Для вирішення подібних ситуацій ми можемо створити реалізацію інтерфейсу, який не робить нічого. Це схема дизайну, відома як Null Object [4], і вона приблизно відповідає від'єднанню комп'ютера від стіни. Оскільки ми використовуємо нещільне з'єднання, ми можемо замінити реальну реалізацію тим, що нічого не робить, не створюючи проблем.


2

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

Намір - похідні типи повинні бути повністю замінені здатними до їх базових типів.

Приклад - типи повернення ко-варіанту в java.


1

Ось уривок з цієї публікації, який добре пояснює речі:

[..] для розуміння деяких принципів важливо усвідомити, коли це було порушено. Це я і зараз буду робити.

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

Розглянемо наступний приклад:

interface Account
{
    /**
     * Withdraw $money amount from this account.
     *
     * @param Money $money
     * @return mixed
     */
    public function withdraw(Money $money);
}
class DefaultAccount implements Account
{
    private $balance;
    public function withdraw(Money $money)
    {
        if (!$this->enoughMoney($money)) {
            return;
        }
        $this->balance->subtract($money);
    }
}

Це порушення ЛСП? Так. Це тому, що контракт рахунку говорить нам, що рахунок буде знято, але це не завжди так. Отже, що мені робити, щоб це виправити? Я просто змінюю договір:

interface Account
{
    /**
     * Withdraw $money amount from this account if its balance is enough.
     * Otherwise do nothing.
     *
     * @param Money $money
     * @return mixed
     */
    public function withdraw(Money $money);
}

Voilà, тепер контракт виконано.

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

class Client
{
    public function go(Account $account, Money $money)
    {
        if ($account instanceof DefaultAccount && !$account->hasEnoughMoney($money)) {
            return;
        }
        $account->withdraw($money);
    }
}

І це автоматично порушує принцип відкритого закриття [тобто вимоги щодо зняття грошей. Тому що ти ніколи не знаєш, що станеться, якщо об’єкт, що порушує договір, не має достатньо грошей. Напевно, це просто нічого не повертає, напевно, буде викинуто виняток. Тож вам доведеться перевірити, чи цеhasEnoughMoney() - який не є частиною інтерфейсу. Тож ця вимушена перевірка, що залежить від конкретного класу, є порушенням OCP].

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

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