Привет, хабрачеловеки!
Захотелось вот поделиться сокровенным знанием по этой теме. К тому же материалов по этому, достаточно важному принципу проектирования классов и их наследования, в Рунете как-то негусто. Имеются формулировки вида:
«Пусть q(x) является свойством верным относительно объектов x некоторого типа T. Тогда q(y) также должно быть верным для объектов y типа S, где S является подтипом типа T.» © Wikipedia
Но онивыносят мой мозг меня совершенно не радуют.
Если хочется услышать объяснение этойхрени умной фразы — прошу под кат.
Итак, принцип подстановки Барбары Лисков. Он же Liskov Substitution Principle. Он же LSP. Простыми словами принцип звучит так:
Наследующий класс должен дополнять, а не замещать поведение базового класса.
Если у нас есть класс A (не виртуальный, а вполне реально используемый в коде) и отнаследованный от него класс B, то если мы заменим все использования класса A на B, ничего не должно измениться в работе программы. Ведь класс B всего лишь расширяет функционал класса A. Если эта проверка работает, то поздравляю: ваша программа соответствует принципу подстановки Лисков! Если нет, стоитуволить ведущего программиста задуматься: «а правильно ли спроектированы классы?».
Надеюсь, всем понятно, что принцип Лисков — это из области теории ООП. На практике, никто не заставляет следовать ему под дулом пистолета. Более того, могут быть случаи, когда следовать ему сложно и никому не нужно.
Словом, прям как с валидным HTML: сайт прошёл проверку на W3C валидаторе — плюсадин в карму верстальщика. Не прошёл — нужно чётко понимать почему он не прошёл: это ошибка или же очередной выкрунтас другими способами реализовать невозможно?
Из этого можно сделать выводы:
* следование принципу подстановки Лисков делает ваш проект ближе к духу ООП;
* это позволит избежать ряда ошибок (о них ниже).
Я решил не изобретать очередной велосипед, а к тому же мне очень понравился пример отсюда. Его то я и буду использовать (с лёгкими модификациями).
Итак, ситуация: мы проектируем программку для управления термостатами. Программа должна уметь работать с несколькими разными моделями устройств. Программа циклически проверяет температуру и пытается выправить её до требуемой. Саму программу мы, разумеется, писать не будем, а остановимся на проектировании иерархии классов-интерфейсов для термостатов.
Первым делом — базовый класс. У него должны быть следующие методы:
* InitializeDevice: инициализация подключенного термостата. Понятное дело, метод pure virtual: для разных устройств могут потребоваться разные предварительные ласки, чтобы оно заработало как следует.
* Get/Set Reference: геттер/сеттер для требуемой (опорной) температуры. Вполне себе конкретные методы (не виртуальные) для установки переменной.
* GetTemperature: чтение температуры из устройства. Опять чисто вирутальный метод.
* AdjustTemperature: снова чисто виртуальный метод. Собственно, для установки температуры.
Опишем это более понятным языком, то есть C++:
А теперь нарисуем 2 конкретных класса для работы с «реальными» термостатами широко известных и популярных фирм Brand_A и Brand_B (Как? Вы их не знаете? Я тоже):
Вуаля! Осталось написать пару строчек в нашу программу:
И всё круто! Программка работает, заказчик доволен, мы читаем Хабр.
Проходит какое-то время и маркетологи (они не зря хлеб же едят!) придумали новый стильный термостат с большим сенсорным экраном и FM тюнером. Наш заказчик, приобрёв новый девайс, снова объявляется и с порога заявляет: «Хочу, понимаешь ли, чтобы программа поддерживала мою прелессссть!».
Ок, добавим ещё один девайс. Написать Brand_C_TemperatureController нам же труда не составит? В процессе доработки неожиданно выясняется, что новый термостат кроме своего сенсорного экрана имеет и продвинутую автоматику: т.е. его не надо вручную проверять и подгонять температуры. Достаточно скормить один раз требуемую температуру (у нас это ReferenceTemperature), а всё остальное он сделает сам. Это и хорошо (меньше возни), и плохо (наши классы не особо то приспособлены для такой ситуации).
Выход находим в 5 минут: Get/Set Reference в базовом классе объявляем виртуальными, а в классе для нового Brand_C термостата мы просто переопределяем эти методы для прямого чтения/записи температуры в термостат. Красота, не так ли? Сказано — сделано:
По закону жанра, становится понятно, что сейчас будет кульминация. Самое время сказать: «Шах и мат!»
Перед разбором полётов ещё раз вспомним принцип подстановки Лисков: Наследующий класс должен дополнять, а не замещать поведение базового класса. А что мы только что сделали? Правильно! Мы заместили методы GetReferenceTemperature и SetReferenceTemperature. Мы изменили поведение класса. Чем это чревато? Процитирую ещё раз использование наших классов, дабы не изнашивать колесо вашей мышки:
Ещё не понятно? В случае работы с оборудованием Brand_A и Brand_B — всё отлично. А вот в случае использования Brand_C мы сначала пишем в устройство температуру, а потом только инициализируем устройство. Чем всё это может законичиться — фантазируйте сами. Возможно, что ничего страшного и не случится. А возможно, что полдня просидим в дебаге.
А вот если бы мы при создании класса Brand_C_TemperatureController (точнее, во время глупого переопределении злополучных геттеров/сеттеров) помнили про принцип подстановки, мы бы могли догадаться, что придуманная нами модель абстракции в новых реалиях — полное фуфло. Как эту ситуацию исправить? Увы, это не тема данной статьи. Я думаю, что итак всех утомил.
По теме могу предложить почитать:
* Статья в Википедии (я предупреждал в самом начале!);
* The Liskov Substitution Principle — именно отсюда я и украл пример для этого топика;
* Гугл.
О! Вспомнил! Статью положено разбавлять картинками. Вот:
©
Если этот демотиватор заставил вас улыбнуться, значит вам понятно, про что я настрогал столько текста.
Удачи! И пусть баги реже встречаются на вашем пути!
Захотелось вот поделиться сокровенным знанием по этой теме. К тому же материалов по этому, достаточно важному принципу проектирования классов и их наследования, в Рунете как-то негусто. Имеются формулировки вида:
«Пусть q(x) является свойством верным относительно объектов x некоторого типа T. Тогда q(y) также должно быть верным для объектов y типа S, где S является подтипом типа T.» © Wikipedia
Но они
Если хочется услышать объяснение этой
Итак, принцип подстановки Барбары Лисков. Он же Liskov Substitution Principle. Он же LSP. Простыми словами принцип звучит так:
Наследующий класс должен дополнять, а не замещать поведение базового класса.
1. Что это значит на практике?
Если у нас есть класс A (не виртуальный, а вполне реально используемый в коде) и отнаследованный от него класс B, то если мы заменим все использования класса A на B, ничего не должно измениться в работе программы. Ведь класс B всего лишь расширяет функционал класса A. Если эта проверка работает, то поздравляю: ваша программа соответствует принципу подстановки Лисков! Если нет, стоит
2. Ну и зачем это нужно?
Надеюсь, всем понятно, что принцип Лисков — это из области теории ООП. На практике, никто не заставляет следовать ему под дулом пистолета. Более того, могут быть случаи, когда следовать ему сложно и никому не нужно.
Словом, прям как с валидным HTML: сайт прошёл проверку на W3C валидаторе — плюсадин в карму верстальщика. Не прошёл — нужно чётко понимать почему он не прошёл: это ошибка или же очередной выкрунтас другими способами реализовать невозможно?
Из этого можно сделать выводы:
* следование принципу подстановки Лисков делает ваш проект ближе к духу ООП;
* это позволит избежать ряда ошибок (о них ниже).
3. Пример
Я решил не изобретать очередной велосипед, а к тому же мне очень понравился пример отсюда. Его то я и буду использовать (с лёгкими модификациями).
Итак, ситуация: мы проектируем программку для управления термостатами. Программа должна уметь работать с несколькими разными моделями устройств. Программа циклически проверяет температуру и пытается выправить её до требуемой. Саму программу мы, разумеется, писать не будем, а остановимся на проектировании иерархии классов-интерфейсов для термостатов.
3.1. Дебют
Первым делом — базовый класс. У него должны быть следующие методы:
* 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;
- }
-
- virtual 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));
- }
-
- void 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);
- }
-
- void 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;
- }
-
- virtual 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));
- }
-
- void AdjustTemperature(int temperature)
- {
- // Нафиг ненужный метод: мы температурой управляем в другом месте
- }
- void InitializeDevice()
- {
- // Тут шаманские пляски, чтобы термостат ниспослал нам хорошую погоду
- }
- };
По закону жанра, становится понятно, что сейчас будет кульминация. Самое время сказать: «Шах и мат!»
3.4. Ой, а что это было?
Перед разбором полётов ещё раз вспомним принцип подстановки Лисков: Наследующий класс должен дополнять, а не замещать поведение базового класса. А что мы только что сделали? Правильно! Мы заместили методы GetReferenceTemperature и SetReferenceTemperature. Мы изменили поведение класса. Чем это чревато? Процитирую ещё раз использование наших классов, дабы не изнашивать колесо вашей мышки:
- . . .
- TemperatureController *pTempCtrl = GetNextTempController();
- pTempCtrl->SetReferenceTemperature(10);
- pTempCtrl->InitializeDevice();
- . . .
Ещё не понятно? В случае работы с оборудованием Brand_A и Brand_B — всё отлично. А вот в случае использования Brand_C мы сначала пишем в устройство температуру, а потом только инициализируем устройство. Чем всё это может законичиться — фантазируйте сами. Возможно, что ничего страшного и не случится. А возможно, что полдня просидим в дебаге.
А вот если бы мы при создании класса Brand_C_TemperatureController (точнее, во время глупого переопределении злополучных геттеров/сеттеров) помнили про принцип подстановки, мы бы могли догадаться, что придуманная нами модель абстракции в новых реалиях — полное фуфло. Как эту ситуацию исправить? Увы, это не тема данной статьи. Я думаю, что итак всех утомил.
4. Хочу ещё!
По теме могу предложить почитать:
* Статья в Википедии (я предупреждал в самом начале!);
* The Liskov Substitution Principle — именно отсюда я и украл пример для этого топика;
* Гугл.
5. Десерт
О! Вспомнил! Статью положено разбавлять картинками. Вот:
©
Если этот демотиватор заставил вас улыбнуться, значит вам понятно, про что я настрогал столько текста.
Удачи! И пусть баги реже встречаются на вашем пути!