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

    Привет, хабрачеловеки!

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

    «Пусть 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. Десерт


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

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

    Удачи! И пусть баги реже встречаются на вашем пути!
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 55

    • UFO just landed and posted this here
        –8
        Немного опередили. Хотел спросить, когда на хабре будем азбуку разжевывать?
        • UFO just landed and posted this here
          +9
          Смех смехом, но небольшой опрос среди знакомых показал, что не все в теме. Многие догадываются, что менять поведение методов — это плохо. А вот объяснить почему не могут. Так что вот такой топик-напоминание получился. Кому-то может быть и в новинку.
          • UFO just landed and posted this here
            +5
            Формулировки вида «2x2=4» просто выносят мой мозг. Какие-то непонятные крестики и черточки.
            Давайте лучше разберемся, что это значит на практике. Пусть у нас есть набор совершенно реальных яблок, которые мы используем в нашем коде. Возьмите из набора сколько-то раз по сколько-то яблок. Если Вы брали 2 раза по 2 яблока, то поздравляю, Ваши действия соответствуют формулировке «2x2=4». Как видите, теперь все просто и понятно.


            Вот как-то так :)
              –8
              тебе череп не жмет?
              • UFO just landed and posted this here
                  –7
                  Ты знаешь гордыня, грех и ты чмо…
                    +1
                    В профиле вы про себя очень правильно написали.
                      –6
                      Говори прямо, юлишь как малолетка…
              +8
              спасибо, мне очень нравятся такие статьи, где рассказывают нормальным языком, а не пытаются вынести мозг!

              к сожалению, про LSP да и многое другое я узнал из более «мозговыносящих» источников…
                –2
                поддерживаю!
                0
                На практике все на много сложнее из-за методов вроде equals. В результате возникают Generics и все сильно усложняется.
                  +5
                  Автору спасибо за познавательную статью.
                  Тем кто говорит насчет азбуки, не забывайте, что не все посетители хабрахабра это опытные программисты со стажем.
                  Если что-то очевидно для нас с вами, то не факт что это будет так же очевидно для человека, который придет сюда из гугла по запросу «подстановка Барбары Лисков». И такая доступная статья может стать первым шагом в деле становления еще одного хорошего программиста :)
                    0
                    «Наследуемый класс должен дополнять» — Наследуемый? т.е. тот, кого наследуют, т.е. базовый?

                    Автор, поправь на «наследующий», плиз.
                      0
                      Ок. Подправил. Мой косяк.
                      –1
                      «B, то если мы заменим все использования класса A на B, ничего не должно измениться в работе программы. Ведь класс B всего лишь расширяет функционал класса A. » — а как же на счёт того, что поведение программы должно «расшириться»? Мы ж заменяем A на B, на расширенную функциональность!
                        +1
                        Насколько я понял смысл, расширенная функциональность должна быть реализована введением дополнительных методов, а не изменением поведения старых.
                          +1
                          скорее внедрением новых классов. а так в точку.
                            0
                            Поясните плз вашу мысль :)
                            В данном случае речь идет о наследовании, т.е. действительно расширенный функционал должен быть реализован введением новых методов в наследованном классе. В смысле внедрение новых классов?

                            что поведение программы должно «расшириться»

                            поведение программы никак не расширяется.
                            Имеется в виду что была программа вида
                            a=new class_a();
                            a->method1();

                            и если написать
                            class class_b extends class_a
                            {
                            }
                            b=new class_a();
                            b->method1();

                            то эта программа будет делать то же самое, что первая.
                            Но ты сможешь использовать новые методы, которые были введены в классе b, естественно.
                            Но вообще говоря поведение программы 2 будет равно поведению программы 1, потому что программа сама никак не расширилась и новый введенный функционал класса в ней никак не задействован.
                            Надеюсь понятно объяснил :)
                              0
                              b=new class_b();
                              имелось в виду, конечно же.
                                0
                                вы изменили код клиента. вообще это очень долго описывать. например создание конкретных классов, подверженных изменениям, есть очень плохо, так как он нарушает принцип DIP(последняя буква в SOLID). Вы уже привязываете код клиента к конкретным классам и вам придется модифицировать работающий код вполне вероятно в большом модуле, и наверняка не один раз. Для защиты используется паттерн Factory. Но допустим мы заменили ваш код на такой IA* a = LalaFactory::make(«A»); // например задаем тип нужного объекта текстом
                                так вот суть LSP в том, что бы классы, реализцющие IA выполняли открытый его интерфейс однозначно и не противоречиво. Если вы сделаете новую модификацию — вы реализуете новый класс, и в фабрику добавите код для создания объекта. Текст «A» в этом примере например можно читать из конфига или константы.

                                Сам же класс, реализующий IA, может вытворять внутри себя что угодно, он также может реализовывать другие интерфейсы, но клиентский код, указанный выше, будет знать только об его поведении в терминах IA и никак иначе. Вот и вся суть OCP + LSP — [b]Зависимость от абстракций.[/b]

                                Попробуйте почитать книжку
                                Agile Principles, Patterns, and Practices in C#
                                By Martin C. Robert, Martin Micah

                                Только на английском, ибо русский перевод ужасен чуть больше, чем полностью. Сгодится разве что для чтения художественной части, а термины и правила лучше переводить из оригинала. По частям SRP, OCP, LSP, ISP, DIP дают мало инфы. А вот когда все вместе описывается — тогда да.
                                  0
                                  А разве не это называется «полиморфизм»?
                            0
                            Тут пропущен важный момент — не упоминается об open close principle (OCP). OCP подразумевает расширение функционала только за счет написания кода, используя полиморфизм. LSP дает нам ограничение, нарушив которое мы не сможем применить расширение за счет создания нового кода, не изменяя старый.
                            +1
                            даешь остальные 4 принципа SOLID =) сам думал написать, но времени чтото нету.
                              +3
                              «Как эту ситуацию исправить? Увы, это не тема данной статьи. Я думаю, что итак всех утомил.» — ну вот, блин. На самом интересном месте… Хоть названия паттернов перечислили бы.
                                0
                                Да, это как раз то, чего я ожидал прочитать в этой статье и был крайне расстроен, что вот так вышло… :-)
                                Как перекрывать методы-то все знают! :-)
                                +1
                                Спасибо, хорошая статья. Наверное мне повезло, что я узнал об этом принципе из нее, так как тут все очень понятно написано.
                                  0
                                  я узнал на много раньше, но статья мне разъяснила более понятнее.
                                  спасибо автору, ждем описание принципа Открытие/Закрытия.
                                    0
                                    интересно, а минуснули за красивые глаза?
                                  +2
                                  Статья понравилась. Действительно приятно, что написано по-человечески (хотя я и так это знал), а не как в Наш мир, возможно, — одна огромная голограмма, от которой у меня было ощущение, что мой мозг изнасиловали в извращённой форме… А самые умные — проходите мимо — тут собрались люди разной степени подготовки и знаний, но объединённые одним общим интересом к IT (по-крайней мере я на это надеюсь) :)
                                    0
                                    «Зачем так много слов, зачем так много треска? Ты мне невестка или не невестка» (с) догадайтесь :)

                                    Достаточно задуматься над такой задачкой — нужны классы для прямоугольника и квадрата, который от которого наследовать? Как корректно поставить эту задачу?
                                      0
                                      ...«нужны классы для прямоугольника и квадрата» — это не задача, а начало решения не поставленной задачи.
                                      Чтобы как-то определиться, нужно понять желаемую функциональность, в первую очередь.
                                        0
                                        Тем не менее, задачу иногда пытаются поставить именно так — нам нужны абстракции того-то и того-то. Вы совершенно правы — без определения интерфейса (функциональности) задачу не поставить. Я именно это и имел ввиду.
                                        +1
                                        Али-Баба! :-)
                                          0
                                          Тут как раз всё понятно, если вспомнить про отношение «является».
                                          Любой квадрат является прямоугольником.
                                          Не любой прямоугольник является квадратом.
                                          Квадрат расширяет Прямоугольник.
                                          +2
                                          Для тех, у кого мозги повернуты думать с точки зрения тестов:

                                          «Пусть тест q(x) выполняется, если в него передать любой x некоторого типа T. Тогда тест q(y) также должен проходить, если передать в него любой объект типа S, где S является подтипом типа T.»
                                            0
                                            Вам плюс за полезный поворот мозгов. А еще за то, что, в отличие от оригинала в википедии, запятые правильно расставлены)
                                              +1
                                              Спасибо :) Надо передать это моей учительнице русского языка, она за грамотность сочинений мне всегда 2 ставила — на 6 страниц пропущенных запятых набиралось достаточно :)
                                            +1
                                            Недавно тоже думал над этим вопросом с точки зрения рефакторинга.
                                            Как такие проблемы решать — самое интересное, а автор умолчал :)

                                            Знаю только одно решение: абстрагирование базового класса и внесение соответствующих методов в наследуемые классы. Но это влечет за собой кучу других проблем (дублирование кода и т.д.).
                                            Может еще кто-то как-то боролся с таким?

                                              +1
                                              Конечно боролись, причем до нас. Вот кратенький список того, что поможет:
                                              igor.quatrocode.com/2008/09/solid-top-5.html

                                              и книжка Мартина, которую указывал выше:
                                              Agile Principles, Patterns, and Practices in C#
                                              By Martin C. Robert, Martin Micah
                                              Русский вариант определений\правил\етц категорически запрещен. Читать инглишь, непонятное сверять с русским, если в художественном языке пробелы(как у меня).
                                                0
                                                Есть хорошая книжка по этому поводу: www.amazon.com/Practical-API-Design-Confessions-Framework/dp/1430209739
                                                +3
                                                Не хватает одной простой иллюстрации — диаграммы Венна:


                                                Принцип в терминах теории множеств будет звучать так:

                                                Свойство q, выполняющееся для объектов множества (типа) S, должно выполняться для объектов подмножества (подтипа) T.

                                                И еще кто-то будет говорить, что в программировании не нужна математика и матан? :-)
                                                  0
                                                  и причём же тут мат анализ?
                                                    +1
                                                    ну если уже минусуете, то обьясняйте причем тут матан, мне тоже интересно :)
                                                      –2
                                                      Просто человек пытается хоть как-то обосновать необходимость предмета, на который было потрачено так много сил…
                                                        0
                                                        Ну, как бы, теорию множеств изучает матан.
                                                          +1
                                                          Боюсь это не так, даже если закрыть глаза на формулировку.
                                                          Дискретная математика имеет некоторое отношение к теории множеств, но никак не математический анализ.
                                                            +3
                                                            Множество.
                                                            Второе множество.
                                                            Каждому элементу одного множества сопоставляем элемент другого множества по определенному правилу.
                                                            Получается функция.
                                                            Математический анализ — совокупность разделов математики, посвященных исследованию функций и их обобщений.
                                                    0
                                                    >>>Опишем это более понятным языком, то есть C++

                                                    Особенно приятно видеть эту фразу в понедельник :)
                                                      0
                                                      Забавно, что для языков с развитой интроспекцией, это правило не работает. Если у вас есть доступ к свойствам именно как к свойствам (на мета-уровне), то легко нарушить LSP.
                                                        0
                                                        интроспекция не является частью ООП, следовательно, не совсем относится и к обсуждаемому принципу. Если на то пошло, интроспекиция и метапрограммирование являются способами обхода жесткой ООП там, где задача плохо сводится к ООП, то есть являются костылями. Не хочу сказать, что эти метода плохи — наоборот, мне они очень нравятся — но это именно костыли для другого костыля под названием ООП )
                                                        +2
                                                        Следование данному принципу довольно таки спорный вопрос по-моему, так как некоторые паттерны проектирования напрочь его нарушают, к примеру Decorator.

                                                        А вот создатели языка Go видимо решили его гарантировано воплотить запретив наследование )
                                                          0
                                                          Статья понравилась.
                                                          PS. Минутка занудства. У Вас виртуальная функция называется «Initialize», а в наследуемых классах везде используется «InitializeDevice».

                                                          Only users with full accounts can post comments. Log in, please.