Принцип підстановки Барбари Лисків
Захотілося поділитися потаємним знанням з цієї теми. До того ж матеріалів за цим, досить важливим принципом проектування класів та їх успадкування, в Рунеті якось негусто. Є формулювання виду:
Але вони виносять мій мозок мене зовсім не тішать.
Якщо хочеться почути пояснення цієї марі розумної фрази — прошу під кат. Отже, принцип підстановки Барбари Лисков. Він же Liskov Substitution Principle. Він же є LSP. Простими словами принцип звучить так:
Спадковий клас повинен доповнювати, а не замінювати поведінку базового класу.
1. Що це означає на практиці?
Якщо у нас є клас A (не віртуальний, а цілком реально використовуваний у коді) і успадкований від нього клас B, то якщо ми замінимо всі використання класу A на B, нічого не повинно змінитися в роботі програми. Адже клас B лише розширює функціонал класу A. Якщо ця перевірка працює, то вітаю: ваша програма відповідає принципу підстановки Лисків! Якщо ні, варто звільнити провідного програміста задуматися: «Чи правильно спроектовані класи?».
2. Ну і навіщо це потрібне?
Сподіваюся, всім зрозуміло, що принцип Лісков — це в галузі теорії ОВП. На практиці ніхто не змушує слідувати йому під дулом пістолета. Більше того, можуть бути випадки, коли слідувати йому складно і нікому не потрібно.
Словом, прямий як з валідним HTML: сайт пройшов перевірку на W3C валідаторе — плюсадин у карму верстальника. Не пройшов — треба чітко розуміти чому він не пройшов: це помилка чи черговий викрунтас іншими способами реалізувати неможливо?
З цього можна зробити висновки: * дотримання принципу підстановки Лисков робить ваш проект ближче до духу ОВП; * це дозволить уникнути низки помилок (про них нижче).
Я вирішив невинаходити черговий велосипед, а до того мені дуже сподобався приклад звідси. Його то я і використовуватиму (з легкими модифікаціями).
Отже, ситуація: ми проектуємо програму для керування термостатами. Програма повинна вміти працювати з кількома моделями пристроїв. Програма циклічно перевіряє температуру та намагається виправити її до необхідної. Саму програму ми, зрозуміло, не писатимемо, а зупинимося на проектуванні ієрархії класів-інтерфейсів для термостатів.
Насамперед — базовий клас. У нього мають бути такі методи: *InitializeDevice : ініціалізація підключеного термостата. Зрозуміло, метод pure virtual: для різних пристроїв можуть знадобитися різні попередні ласки, щоб воно запрацювало як слід. *Get/Set Reference : гетер/сеттер для необхідної (опорної) температури. Цілком собі конкретні методи (не віртуальні) для встановлення змінної. *GetTemperature : читання температури пристрою. Знову суто віртуальний метод. *AdjustTemperature : знову чисто віртуальний метод. Власне, для встановлення температури.
Опишемо це зрозумілішою мовою, тобто C++:
- class TemperatureController
- // Змінна для зберігання опорної температури
- int m_referenceTemperature;
- public :
- int GetReferenceTemperature() const
- return m_referenceTemperature;
- >
- void SetReferenceTemperature( int referenceTemperature)
- m_referenceTemperature = referenceTemperature;
- >
- віртуальний int GetTemperature() const = 0;
- virtual void AdjustTemperature(int temperature) = 0;
- virtual void InitializeDevice() = 0;
- >;
3.2. Міттельшпіль
Атепер намалюємо 2 конкретні класи для роботи з «реальними» термостатами широко відомих і популярних фірм Brand_A і Brand_B (Як? Ви їх не знаєте? Я теж):
- class Brand_A_TemperatureController : public TemperatureController
- public :
- int GetTemperature() const
- return (io_read(TEMP_REGISTER));
- >
- adjustTemperature( int temperature)
- io_write(TEMP_CHANGE_REGISTER, temperature);
- >
- void InitializeDevice()
- // Умовляємо девайс товаришувати з нами
- >
- >;
- class Brand_B_TemperatureController : public TemperatureController
- public :
- int GetTemperature() const
- return (io_read(STATUS_REGISTER) & TEMP_MASK);
- >
- adjustTemperature( int temperature)
- // Аж надто хитрий девайс попався: йому температуру в треба
- // Кельвін надати! Добре, що не у Фаренгейтах.
- io_write(CHANGE_REGISTER, temperature + 273);
- >
- void InitializeDevice()
- // Схиляємо термостат до співпраці
- >
- >;
- . . .
- TemperatureController *pTempCtrl = GetNextTempController();
- pTempCtrl->SetReferenceTemperature(10);
- pTempCtrl->InitializeDevice();
- . . .
3.3. Ендшпіль
Проходить якийсь час і маркетологи (вони не дарма хліб їдять!) придумали новий стильний термостат з великим сенсорним екраном і FM тюнером. Наш замовник, придбавши новий девайс, знову оголошується і з порога заявляє: «Хочу, чи розумієш, щобпрограма підтримувала мою прелесть!».
Ок, додамо ще один девайс. Написати Brand_C_TemperatureController нам же не складе труднощів? У процесі доопрацювання несподівано з'ясовується, новий термостат крім свого сенсорного екрана має й просунуту автоматику: тобто. його не треба вручну перевіряти та підганяти температури. Достатньо згодувати один раз потрібну температуру (у нас це ReferenceTemperature), а все інше він зробить сам. Це і добре (менше метушні), і погано (наші класи не особливо пристосовані для такої ситуації).
Вихід знаходимо в 5 хвилин: Get/Set Reference у базовому класі оголошуємо віртуальними, а в класі для нового Brand_C термостата ми просто перевизначаємо ці методи для прямого читання/запису температури термостата. Краса, чи не так? Сказано зроблено:
- class TemperatureController
- // Змінна для зберігання опорної температури
- int m_referenceTemperature;
- public :
- // Геттер/Сетер тепер віртуальний
- // Наш новий концепт
- virtual int GetReferenceTemperature() const
- return m_referenceTemperature;
- >
- virtual void SetReferenceTemperature( int referenceTemperature)
- m_referenceTemperature = referenceTemperature;
- >
- віртуальний int GetTemperature() const = 0;
- virtual void AdjustTemperature(int temperature) = 0;
- virtual void InitializeDevice() = 0;
- >;
- class Brand_C_TemperatureController : public TemperatureController
- public :
- // Геттер/сеттер спілкується безпосередньо з девайсом
- int GetReferenceTemperature() const
- return (io_read (REFERENCE_REGISTER);
- >
- void SetReferenceTemperature( int referenceTemperature)
- io_write(REFERENCE_REGISTER, referenceTemperature);
- >
- int GetTemperature() const
- return (io_read(TEMP_MONITORING_REGISTER));
- >
- adjustTemperature( int temperature)
- // Нафіг непотрібний метод: ми керуємо температурою в іншому місці
- >
- void InitializeDevice()
- // Тут шаманські танці, щоб термостат послав нам гарну погоду
- >
- >;
3.4. Ой, а що це було?
Перед розбором польотів ще раз згадаємо принцип підстановки Лисків:Спадковий клас має доповнювати, а не замінювати поведінку базового класу. А що ми щойно зробили? Правильно! Ми замінили методи GetReferenceTemperature та SetReferenceTemperature. Ми змінили поведінку класу. Чим це загрожує? Процитую ще раз використання наших класів, щоб не зношувати колесо вашої мишки:
- . . .
- TemperatureController *pTempCtrl = GetNextTempController();
- pTempCtrl->SetReferenceTemperature(10);
- pTempCtrl->InitializeDevice();
- . . .
А от якби ми при створенні класу Brand_C_TemperatureController (точніше, під час дурного перевизначення злощасних гетерів/сеттерів) пам'ятали про принцип підстановки, ми могли б здогадатися, що придумананами модель абстракції у нових реаліях – повне фуфло. Як цю ситуацію виправити? На жаль, це не тема цієї статті. Я думаю, що таким чином усіх стомив.
4. Хочу ще!
За темою можу запропонувати почитати: Стаття у Вікіпедії (я попереджав на самому початку!); * The Liskov Substitution Principle - саме звідси я і вкрав приклад для цього топіка; * Гугл.
Якщо цей демотиватор змусив вас посміхнутися, то вам зрозуміло, про що я налаштував стільки тексту.
Успіхів! І нехай баги рідше трапляються на вашому шляху!