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

Захотілося поділитися потаємним знанням з цієї теми. До того ж матеріалів за цим, досить важливим принципом проектування класів та їх успадкування, в Рунеті якось негусто. Є формулювання виду:

Але вони виносять мій мозок мене зовсім не тішать.

Якщо хочеться почути пояснення цієї марі розумної фрази — прошу під кат. Отже, принцип підстановки Барбари Лисков. Він же Liskov Substitution Principle. Він же є LSP. Простими словами принцип звучить так:

Спадковий клас повинен доповнювати, а не замінювати поведінку базового класу.

1. Що це означає на практиці?

Якщо у нас є клас A (не віртуальний, а цілком реально використовуваний у коді) і успадкований від нього клас B, то якщо ми замінимо всі використання класу A на B, нічого не повинно змінитися в роботі програми. Адже клас B лише розширює функціонал класу A. Якщо ця перевірка працює, то вітаю: ваша програма відповідає принципу підстановки Лисків! Якщо ні, варто звільнити провідного програміста задуматися: «Чи правильно спроектовані класи?».

2. Ну і навіщо це потрібне?

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

Словом, прямий як з валідним HTML: сайт пройшов перевірку на W3C валідаторе — плюсадин у карму верстальника. Не пройшов — треба чітко розуміти чому він не пройшов: це помилка чи черговий викрунтас іншими способами реалізувати неможливо?

З цього можна зробити висновки: * дотримання принципу підстановки Лисков робить ваш проект ближче до духу ОВП; * це дозволить уникнути низки помилок (про них нижче).

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

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

Насамперед — базовий клас. У нього мають бути такі методи: *InitializeDevice : ініціалізація підключеного термостата. Зрозуміло, метод pure virtual: для різних пристроїв можуть знадобитися різні попередні ласки, щоб воно запрацювало як слід. *Get/Set Reference : гетер/сеттер для необхідної (опорної) температури. Цілком собі конкретні методи (не віртуальні) для встановлення змінної. *GetTemperature : читання температури пристрою. Знову суто віртуальний метод. *AdjustTemperature : знову чисто віртуальний метод. Власне, для встановлення температури.

Опишемо це зрозумілішою мовою, тобто C++:

  1. class TemperatureController
  2. // Змінна для зберігання опорної температури
  3. int m_referenceTemperature;
  4. public :
  5. int GetReferenceTemperature() const
  6. return m_referenceTemperature;
  7. >
  8. void SetReferenceTemperature( int referenceTemperature)
  9. m_referenceTemperature = referenceTemperature;
  10. >
  11. віртуальний int GetTemperature() const = 0;
  12. virtual void AdjustTemperature(int temperature) = 0;
  13. virtual void InitializeDevice() = 0;
  14. >;

3.2. Міттельшпіль

Атепер намалюємо 2 конкретні класи для роботи з «реальними» термостатами широко відомих і популярних фірм Brand_A і Brand_B (Як? Ви їх не знаєте? Я теж):

  1. class Brand_A_TemperatureController : public TemperatureController
  2. public :
  3. int GetTemperature() const
  4. return (io_read(TEMP_REGISTER));
  5. >
  6. adjustTemperature( int temperature)
  7. io_write(TEMP_CHANGE_REGISTER, temperature);
  8. >
  9. void InitializeDevice()
  10. // Умовляємо девайс товаришувати з нами
  11. >
  12. >;
  13. class Brand_B_TemperatureController : public TemperatureController
  14. public :
  15. int GetTemperature() const
  16. return (io_read(STATUS_REGISTER) & TEMP_MASK);
  17. >
  18. adjustTemperature( int temperature)
  19. // Аж надто хитрий девайс попався: йому температуру в треба
  20. // Кельвін надати! Добре, що не у Фаренгейтах.
  21. io_write(CHANGE_REGISTER, temperature + 273);
  22. >
  23. void InitializeDevice()
  24. // Схиляємо термостат до співпраці
  25. >
  26. >;
Вуаля! Залишилось написати пару рядків у нашу програму:
  1. . . .
  2. TemperatureController *pTempCtrl = GetNextTempController();
  3. pTempCtrl->SetReferenceTemperature(10);
  4. pTempCtrl->InitializeDevice();
  5. . . .
І все круто! Програмка працює, замовник задоволений, ми читаємо Хабр.

3.3. Ендшпіль

Проходить якийсь час і маркетологи (вони не дарма хліб їдять!) придумали новий стильний термостат з великим сенсорним екраном і FM тюнером. Наш замовник, придбавши новий девайс, знову оголошується і з порога заявляє: «Хочу, чи розумієш, щобпрограма підтримувала мою прелесть!».

Ок, додамо ще один девайс. Написати Brand_C_TemperatureController нам же не складе труднощів? У процесі доопрацювання несподівано з'ясовується, новий термостат крім свого сенсорного екрана має й просунуту автоматику: тобто. його не треба вручну перевіряти та підганяти температури. Достатньо згодувати один раз потрібну температуру (у нас це ReferenceTemperature), а все інше він зробить сам. Це і добре (менше метушні), і погано (наші класи не особливо пристосовані для такої ситуації).

Вихід знаходимо в 5 хвилин: Get/Set Reference у базовому класі оголошуємо віртуальними, а в класі для нового Brand_C термостата ми просто перевизначаємо ці методи для прямого читання/запису температури термостата. Краса, чи не так? Сказано зроблено:

  1. class TemperatureController
  2. // Змінна для зберігання опорної температури
  3. int m_referenceTemperature;
  4. public :
  5. // Геттер/Сетер тепер віртуальний
  6. // Наш новий концепт
  7. virtual int GetReferenceTemperature() const
  8. return m_referenceTemperature;
  9. >
  10. virtual void SetReferenceTemperature( int referenceTemperature)
  11. m_referenceTemperature = referenceTemperature;
  12. >
  13. віртуальний int GetTemperature() const = 0;
  14. virtual void AdjustTemperature(int temperature) = 0;
  15. virtual void InitializeDevice() = 0;
  16. >;
  17. class Brand_C_TemperatureController : public TemperatureController
  18. public :
  19. // Геттер/сеттер спілкується безпосередньо з девайсом
  20. int GetReferenceTemperature() const
  21. return (io_read (REFERENCE_REGISTER);
  22. >
  23. void SetReferenceTemperature( int referenceTemperature)
  24. io_write(REFERENCE_REGISTER, referenceTemperature);
  25. >
  26. int GetTemperature() const
  27. return (io_read(TEMP_MONITORING_REGISTER));
  28. >
  29. adjustTemperature( int temperature)
  30. // Нафіг непотрібний метод: ми керуємо температурою в іншому місці
  31. >
  32. void InitializeDevice()
  33. // Тут шаманські танці, щоб термостат послав нам гарну погоду
  34. >
  35. >;
За законом жанру, стає зрозуміло, що зараз буде кульмінація. Саме час сказати: "Шах і мат!"

3.4. Ой, а що це було?

Перед розбором польотів ще раз згадаємо принцип підстановки Лисків:Спадковий клас має доповнювати, а не замінювати поведінку базового класу. А що ми щойно зробили? Правильно! Ми замінили методи GetReferenceTemperature та SetReferenceTemperature. Ми змінили поведінку класу. Чим це загрожує? Процитую ще раз використання наших класів, щоб не зношувати колесо вашої мишки:

  1. . . .
  2. TemperatureController *pTempCtrl = GetNextTempController();
  3. pTempCtrl->SetReferenceTemperature(10);
  4. pTempCtrl->InitializeDevice();
  5. . . .
Ще не зрозуміло? У разі роботи з обладнанням Brand_A і Brand_B все відмінно. А ось у разі використання Brand_C ми спочатку пишемо у пристрій температуру, а потім лише ініціалізуємо пристрій. Чим усе це може закінчитися – фантазуйте самі. Можливо, нічого страшного і не станеться. А можливо, що півдня просидимо у дебазі.

А от якби ми при створенні класу Brand_C_TemperatureController (точніше, під час дурного перевизначення злощасних гетерів/сеттерів) пам'ятали про принцип підстановки, ми могли б здогадатися, що придумананами модель абстракції у нових реаліях – повне фуфло. Як цю ситуацію виправити? На жаль, це не тема цієї статті. Я думаю, що таким чином усіх стомив.

4. Хочу ще!

За темою можу запропонувати почитати: Стаття у Вікіпедії (я попереджав на самому початку!); * The Liskov Substitution Principle - саме звідси я і вкрав приклад для цього топіка; * Гугл.

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

Успіхів! І нехай баги рідше трапляються на вашому шляху!