Pull to refresh

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

Reading time 6 min
Views 138K
Привет, хабрачеловеки!

Захотелось вот поделиться сокровенным знанием по этой теме. К тому же материалов по этому, достаточно важному принципу проектирования классов и их наследования, в Рунете как-то негусто. Имеются формулировки вида:

«Пусть 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++:
  1. class TemperatureController
  2. {
  3.   // Переменная для хранения опорной температуры
  4.   int m_referenceTemperature;
  5. public:
  6.    
  7.   int GetReferenceTemperature() const
  8.   {
  9.    return m_referenceTemperature;
  10.   }
  11.  
  12.   void SetReferenceTemperature(int referenceTemperature)
  13.   {
  14.     m_referenceTemperature = referenceTemperature;
  15.   }
  16.  
  17.   virtual int GetTemperature() const = 0;
  18.  
  19.   virtual void AdjustTemperature(int temperature) = 0;
  20.  
  21.   virtual void InitializeDevice() = 0;
  22. };


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

А теперь нарисуем 2 конкретных класса для работы с «реальными» термостатами широко известных и популярных фирм Brand_A и Brand_B (Как? Вы их не знаете? Я тоже):
  1. class Brand_A_TemperatureController : public TemperatureController
  2. {
  3. public:
  4.  
  5.   int GetTemperature() const
  6.   {
  7.    return (io_read(TEMP_REGISTER));
  8.   }
  9.  
  10.   void AdjustTemperature(int temperature)
  11.   {
  12.     io_write(TEMP_CHANGE_REGISTER, temperature);
  13.   }
  14.  
  15.   void InitializeDevice()
  16.   {
  17.     // Уговариваем девайс дружить с нами
  18.   }
  19. };
  20.  
  21. class Brand_B_TemperatureController : public TemperatureController
  22. {
  23. public:
  24.  
  25.   int GetTemperature() const
  26.   {
  27.    return (io_read(STATUS_REGISTER) & TEMP_MASK);
  28.   }
  29.  
  30.   void AdjustTemperature(int temperature)
  31.   {
  32.    // Уж больно хитрый девайс попался: ему температуру в надо
  33.    // Кельвинах предоставить! Хорошо, что не в Фаренгейтах.
  34.    io_write(CHANGE_REGISTER, temperature + 273);
  35.   } 
  36.  
  37.   void InitializeDevice()
  38.   {
  39.    // Склоняем термостат к сотрудничеству
  40.   }
  41. };

Вуаля! Осталось написать пару строчек в нашу программу:
  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.   // Переменная для хранения опорной температуры
  4.   int m_referenceTemperature;
  5. public:
  6.  
  7.   // Геттер/сеттер теперь виртуальный
  8.   // Наш новый концепт
  9.   virtual int GetReferenceTemperature() const
  10.   {
  11.    return m_referenceTemperature;
  12.   }
  13.  
  14.   virtual void SetReferenceTemperature(int referenceTemperature)
  15.   {
  16.     m_referenceTemperature = referenceTemperature;
  17.   }
  18.  
  19.   virtual int GetTemperature() const = 0;
  20.  
  21.   virtual void AdjustTemperature(int temperature) = 0;
  22.  
  23.   virtual void InitializeDevice() = 0; 
  24. };
  25.  
  26. class Brand_C_TemperatureController : public TemperatureController
  27. {
  28. public:
  29.  
  30.   // Геттер/сеттер общается непосредственно с девайсом
  31.   int GetReferenceTemperature() const
  32.   {
  33.    return (io_read(REFERENCE_REGISTER);
  34.   }
  35.  
  36.   void SetReferenceTemperature(int referenceTemperature)
  37.   {
  38.     io_write(REFERENCE_REGISTER, referenceTemperature);
  39.   }
  40.  
  41.   int GetTemperature() const
  42.   {
  43.    return (io_read(TEMP_MONITORING_REGISTER));
  44.   }
  45.  
  46.   void AdjustTemperature(int temperature)
  47.   {
  48.    // Нафиг ненужный метод: мы температурой управляем в другом месте
  49.   } 
  50.   void InitializeDevice()
  51.   {
  52.    // Тут шаманские пляски, чтобы термостат ниспослал нам хорошую погоду
  53.   }  
  54. };

По закону жанра, становится понятно, что сейчас будет кульминация. Самое время сказать: «Шах и мат!»

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 — именно отсюда я и украл пример для этого топика;
* Гугл.

5. Десерт


О! Вспомнил! Статью положено разбавлять картинками. Вот:
Принцип подстановки Барбары Лисков©

Если этот демотиватор заставил вас улыбнуться, значит вам понятно, про что я настрогал столько текста.

Удачи! И пусть баги реже встречаются на вашем пути!
Tags:
Hubs:
+65
Comments 55
Comments Comments 55

Articles