Принцип підстановки Лісков

Див. також: Лісков

Принцип заміщення Лісков (англ. Liskov Substitution Principle, LSP) в об'єктно-орієнтованому програмуванні — це спеціальне визначення підтипу, запропоноване Барбарою Лісков у 1987 році на конференції у доповіді під назвою "Абстракція даних та ієрархія"[1].

У своїй статті [2] Лісков сформулювала свій принцип так:

Нехай є властивістю правильною для об'єктів деякого типу . Тоді також має бути правильним для об'єктів типу , де  — підтип типу .

Таким чином ідея Лісков про «підтип» визначає поняття заміщення — якщо S підтип T, тоді об'єкти типу T в програмі можуть бути заміщені об'єктами типу S без будь-яких змін бажаних властивостей цієї програми (наприклад, коректність[en]).

Цей принцип — найважливіший критерій для оцінки якості ухвалених рішень при побудові ієрархій успадкування. Сформулювати його можна у вигляді простого правила: тип S буде підтипом Т тоді і тільки тоді, коли кожному об'єктові o1 типу S відповідає певний об'єкт o2 типу T таким чином, що для всіх програм P, реалізованих в термінах T, поведінка P не зміниться, якщо o2 замінити на o1.

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

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

Див. коваріантність і контраваріантність і типи даних.

Проєктування за контрактом

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

  • Передумови не можуть бути посилені в підтипі.
  • Післяумови не можуть бути послаблені в підтипі.
  • Інваріанти базового типу повинні виконуватись у підтипі.
  • «Історичне обмеження»: заборона модифікації стану об'єкта методами підтипу, які відсутні в базовому типі.

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

У цьому контексті принцип заміщення Лісков можна переформулювати так:

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

Андрей Александреску та Герб Саттер в книзі «C++ Coding Standards» описують виконання цього принципу так:

«Підклас не повинен вимагати від коду, що його викликає, більше ніж вимагав базовий клас. І повинен надавати цьому коду не менше ніж надавав базовий клас».

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

Приклад

Роберт Мартін зазначає, що до перших ознак порушення принципу підстановки Лісков є наявність функцій, які перевіряють тип отриманого аргументу для визначення своєї поведінки:[3]

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

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

Однак, оскільки квадрат успадкує від прямокутника обидва атрибута — ширину і висоту, то постане проблема: як гарантувати їхню рівність в методах присвоєння (так званих «сеттерах»)? Просте рішення може мати такий вигляд:

void Square::SetWidth(double w)
{ 
  Rectangle::SetWidth(w);
  Rectangle::SetHeight(w);
}
void Square::SetHeight(double h)
{
  Rectangle::SetHeight(h);
  Rectangle::SetWidth(h);
}

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

void g(Rectangle& r)
{
  r.SetWidth(5);
  r.SetHeight(4);
  assert(r.GetWidth() * r.GetHeight()) == 20);
}

Таким чином, порушення принципу підстановки Лісков може спричиняти порушення принципу відкритості/закритості[4].

Примітки

  1. Liskov, Barbara (4 жовтня 1987). Data abstraction and hierarchy. Архів оригіналу за 30 червня 2019. Процитовано 23 березня 2008.
  2. Liskov, Barbara; Jeannette Wing (July 1999). Behavioral Subtyping Using Invariants and Constraints (PS). Архів оригіналу за 30 серпня 2012. Процитовано 5 жовтня 2006.
  3. а б Robert Martin, The Liskov Substitution Principle
  4. Martin, Robert C. (2000). Design Principles and Design Patterns (PDF). Архів оригіналу (PDF) за 6 вересня 2015. Thus, violations of LSP are latent violations of OCP.

Посилання