company_banner

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

    Всем привет, меня зовут Константин. Я занимаюсь разработкой на Java в Tinkoff.ru и люблю SOLID. В этой статье мы сформулируем принцип подстановки Лисков, покажем его связь с принципом Открытости-Закрытости, узнаем, как правильно формировать иерархию наследования и ответим на философский вопрос о том, является ли квадрат прямоугольником.



    Прежде чем перейти непосредственно к рассматриваемой теме, я хочу сделать шаг назад и обозначить очень важный тезис, необходимый для правильного и полного понимания того, о чем будет говориться в статье.


    Вспомним принцип открытости-закрытости. Он говорит о том, что в хорошо спроектированных программах новая функциональность вводится путем добавления нового кода, а не изменением старого, уже работающего. И это есть основа написания поддерживаемого и переиспользуемого кода. Принцип подстановки Лисков (далее LSP) — это по сути гайдлайн того, как реализовать этот принцип при построении иерархии наследования классов в объектно-ориентированных языках программирования. По сути правильная иерархия наследования в ООП — это иерархия, построенная согласно LSP, чтобы отвечать принципу открытости-закрытости.


    Давайте же его сформулируем:


    Функции, использующие указатели или ссылки на базовые классы, должны иметь возможность использовать объекты классов-наследников, не зная об этом.


    В первый раз Барбара Лисков сформулировала его так:


    Для этого нужно, чтобы выполнялось следующее свойство подстановки: если для каждого объекта o1 типа S существует такой объект o2 типа T, что для всех программ P, определенных в терминах T, поведение программы P не изменяется, если вместо o2 подставить o1, то S — подтип T.


    Таким образом, если функция не отвечает LSP, то это фактически значит, что, если она использует указатель или ссылку на базовый класс, то должна знать обо всех наследниках базового класса. Такая функция нарушает принцип открытости-закрытости, потому что ее необходимо изменять каждый раз, когда появляется новый наследник базового класса.


    Простой пример нарушения LSP


    Представим, что у нас есть такая функция:


    void drawShape(Shape shape) {
       if (shape instanceof Square) {
           drawSquare((Square) shape);
       } else {
           drawCircle((Circle) shape);
       }
    }

    В ней используется определение класса во время выполнения и в зависимости от результата вызывается нужная функция с явным приведением класса аргумента. И если добавится новый наследник класса Shape, ее нужно будет изменить, так как иначе произойдет вызов drawCircle, которая в лучшем случае выкинет ошибку, а в худшем — отработает некорректно.


    То есть drawShape должна знать обо всех наследниках класса Shape. Более того, каждый раз, когда мы вводим нового наследника класса Shape либо удаляем его, мы должны вносить в неё изменения. То есть функция не закрыта от изменений в иерархии классов Shape, что по сути и является нарушением принципа открытости-закрытости.


    Квадрат и прямоугольник


    Теперь давайте рассмотрим, как можно нарушить LSP не столь очевидным способом. Предположим, мы разрабатываем программу, которая работает с геометрическими фигурами. В ней есть класс для работы с прямоугольниками:


    public class Rectangle {
        private int width;
        private int height;
    
        public int getWidth() {
            return width;
        }
    
        public void setWidth(int width) {
            this.width = width;
        }
    
        public int getHeight() {
            return height;
        }
    
        public void setHeight(int height) {
            this.height = height;
        }
    
        public int perimeter() {
            return 2 * height + 2 * width;
        }
    }

    Пока все хорошо. Однако заказчик приходит к нам с требованием, что помимо прямоугольников пользователям нужен удобный функционал для работы с квадратами. Так как мы работаем в парадигме ООП, которая, в частности, говорит нам, что классы должны моделировать объекты реального мира, то наша первая мысль — нужно ввести класс Square. Но как именно?


    Известно, что наследование реализует отношение «является» (ISA). То есть, когда мы говорим, что новый тип объекта находится в отношении ISA со старым типом объекта, это значит, что новый класс должен быть наследником старого.


    Квадрат — это очевидно прямоугольник. Следовательно, они находятся в отношении ISA. В свою очередь из этого следует то, что класс Square должен быть наследником класса Rectangle. Казалось бы, все логично, однако в наших рассуждениях есть изъян, который может привести к неочевидным, но серьезным проблемам.


    Для начала обратим внимание на то, что классу Square не нужны оба поля height и width, достаточно одного поля, которое можно было просто назвать side, однако же при такой реализации он их унаследует. Помимо того, что это просто приводит к лишней трате памяти на хранение одного ненужного поля (а в случае, если программа генерирует достаточно много объектов данного класса, это может стать серьезной проблемой), оно также приводит к усложнению кода. Дело в том, что класс Square также наследует setWidth и setHeight. Очевидно, что их необходимо переопределить следующим образом:


        @Override
        public void setWidth(int width) {
            super.setWidth(width);
            super.setHeight(width);
        }
    
        @Override
        public void setHeight(int height) {
            super.setHeight(height);
            super.setWidth(height);
        } 
    

    Теперь при установке ширины объекта Square будет установлена и его длина. То же самое при установке длины. Таким образом гарантируется целостность инвариантов объектов Square. С математической точки зрения они будут оставаться корректными фигурами. Тем не менее, помимо того, что теперь наш код содержит лишние вызовы функций, это еще и страшный выстрел в ногу, и сейчас мы увидим почему.


    Тесты


    Предположим, при написании класса Rectangle мы написали простой тест:


    public class RectangleTest {
    
        @Test
        public void perimeter() {
            Rectangle rectangle = new Rectangle();
            rectangle.setHeight(5);
            rectangle.setWidth(7);
    
            int result = rectangle.perimeter();
    
            assertEquals(24, result);
        }
    
    } 

    Очевидно, что при введении класса Square мы должны написать тесты и для него. Так как класс Square находится в соотношении ISA с классом Rectangle, будет логично предположить, что тесты на Rectangle должны быть справедливы и для Square. А чтобы не дублировать код, мы напишем тесты следующим образом:


    public class RectangleTest {
    
        @Test
        public void perimeter() {
            Rectangle rectangle = initRectangle();
            rectangle.setHeight(5);
            rectangle.setWidth(7);
    
            int result = rectangle.perimeter();
    
            assertEquals(24, result);
        }
    
        protected Rectangle initRectangle() {
            return new Rectangle();
        }
    
    }
    
    public class SquareTest extends RectangleTest {
    
        @Override
        protected Rectangle initRectangle() {
            return new Square();
        }
    
    }

    Очевидно, что тест SquareTest.perimeter провалится, так как результат вызова функции perimeter будет не 24, как написано в тесте, а 28. И тут мы должны задать себе очень важный вопрос: правильно ли написан тест, в котором предполагается, что при изменении длины прямоугольника его ширина не изменяется? Очевидно — да. Наш тест наглядно демонстрирует код, который корректно работает с объектом класса Rectangle, но ломается при работе с объектами класса Square. То есть не для каждого объекта типа Square существует объект типа Rectangle такой, что определённая в терминах Rectangle программа (в данном случае тест) не меняется, если вместо объекта типа Rectangle подставить объект типа Square. Следовательно Square — не подтип Rectangle, следовательно LSP нарушается.


    Так квадрат — это не прямоугольник?


    Квадрат — это, конечно, прямоугольник, но вот объект класса Square — это определенно не объект класса Rectangle. Дело в том, что поведение объекта класса Square не согласовано с поведением объекта класса Rectangle. Ведь квадрат ведет себя иначе, чем прямоугольник.


    Принцип подстановки Лисков наглядно показывает, что в ООП отношение ISA относится именно к поведению. Причем не к внутреннему, а к внешнему поведению, от которого зависят клиенты. Мы всегда должны помнить об этом, когда говорим про моделирование объектов реального мира в парадигме ООП.


    С этой точки зрения важность написания тестов становится еще более очевидной. Ведь тесты — это тоже клиенты нашей модели, которые позволяют проверить ее правильность с точки зрения предположений, которые могут сделать пользователи. Ведь понять, правильна модель или нет, мы можем только через ее использование.


    Ну а что тогда делать?


    Тут я буду банален: все зависит от задачи. Не существует единственно верной модели на все случаи жизни. Мы всегда должны отталкиваться от требований к нашему программному обеспечению и той функциональности, которую хотим предоставить.


    Например, и у квадрата, и у прямоугольника есть периметр и, вероятно, в некоторых частях программы нам нужны его значения независимо от того, с какой фигурой мы работаем. В этом случае легко вынести этот метод в интерфейс Shape и прописать его имплементации в классах Square и Rectangle.


    Также и у квадрата, и у прямоугольника есть четыре стороны и четыре угла. Возможно, мы захотим получать координаты этих углов в пространстве. Тогда можно вынести интерфейс либо абстрактный класс Quadrangle с соответствующими методами, он также может пригодиться, если мы захотим ввести в свою программу ромбы, параллелограммы, трапеции и прочее. Вариантов великое множество.


    Заключение


    Принцип открытости-закрытости говорит нам о том, что в поддерживаемых и переиспользуемых программах новая функциональность вводится путем добавления нового кода, а не изменением уже существующего.


    Принцип подстановки Лисков поясняет нам, как этого добиться, когда мы говорим о построении иерархий классов. Если код, оперирующий ссылками на базовые классы, должен знать обо всех его наследниках и изменяться с появлением каждого нового наследника, то этот код не отвечает принципу подстановки Лисков, а значит, не отвечает и принципу открытости-закрытости.


    На эту статью меня во многом вдохновила статья Роберта Мартина 1996 года — The Liskov Substitution Principle. Очень рекомендую с ней ознакомиться. В ней также разобран пример с квадратом и прямоугольником, но с особенностями, специфическими для C++. Кроме того, рассматривается тема проектирования по контракту, а также дан интересный пример нарушения LSP на примере из реального проекта.

    Tinkoff
    it’s Tinkoff — просто о сложном

    Комментарии 67

      0
      > вынести этот метод в интерфейс Shape и прописать его имплементации в классах Square и Rectangle
      То есть изменить работающий и протестированный класс Rectangle?
      Как же тогда
      > новая функциональность вводится путем добавления нового кода, а не изменением старого, уже работающего?
        +3
        Вынос методов в интерфейс Shape никак не меняет реализацию класса Rectangle
        +14

        Обычная путаница с этим прямоугольником.
        Все потому, что забываете о главном принципе ООП — абстракции.
        Если наши фигуры математические, то они иммутабельные. И да, квадрат это наследник прямоугольника. Без проблем.
        Как только водятся методы set, то это уже не математические фигуры. Это что-то из реального мира.
        И тогда "фигня в фирме прямоугольника, которая может менять размеры и пропорции" это не предок "фигни в форме квадрата, которая может менять размеры".
        Для уточнения иерархии надо понять, что же у нас есть. Модель чего мы строим.
        По сути весь SOLID это уточнение абстракции

          0

          А может квадрат наследник ромба без проблем?

            0
            У ромба есть «длина» и «один из углов» (проще постулировать, что он не более 90 градусов).
            Так что изменяемость ромба ломает наследование.
            И тут интерфейсы лучше, потому что
            public Square extends Square, Rhombus
              0
              public Square extends Rectangle, Rhombus конечно
              0
              тест на площадь провалится
                0
                А может они все наследники параллелограмма?
                  0

                  А может геометрического места точек?

                +2

                Очень хорошее замечание, что неизменяемые прямоугольник и квадрат действительно являются родственными. Однако, для их изменения требуется объявить операции с ними. И именно в этот момент делает их разными в поведении. И запрещает наследование. Абстракция интерфейса по сути говорит, что у них есть одноименные действия, но никак не указывает, что действия будут хоть сколько-то одинаковыми.

                  0

                  Вот так бы сходу я бы не отвергал применение LSP и к интерфейсам. Точнее сходу в голову даже не приходит, когда его однозначно можно отвергнуть.

                    0

                    Если под интерфейсом понимать полное сохранение семантики, то например для квадрата вызов метода setSize(width, height) унаследованный от прямоугольника не может быть вызыван с отличающииися агрументами. Чуть ниже я привел пример, что у квадрата всегда есть вписанная окружность, и эта операция не всегда применима к прямогольнику.


                    Можно пойти дальше и в семантике операций прописывать, что они не всегда применимы и могут вернуть ошибку на этой операции. Но тогда мы отрываемся от всего математического аппарата, который рассматривает только возможные действия и формализует ошибки в виде запретов или ограничений.

                    +1
                    Однако, для их изменения требуется объявить операции с ними. И именно в этот момент делает их разными в поведении. И запрещает наследование.


                    Нет никаких проблем.
                    public Square scale(Square, int) {}
                    public Rectangle scale(Rectangle, int) {}
                    public Rectangle scale(Rectangle, int, int) {}

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

                    При этом да, интерфейсы позволяют абстрагироваться абстрактнее :)
                      0

                      Прямоугольник в такой модели может быть очень похожим на квадрат при определенных условиях и тогда у него может быть например метод isSquare() который ответит является ли фигура квадратом. У двух фигур может быть общий метод scale(factor). Но это будет общим свойством для фигур. Но факт остается фактом, что что у квадрата, что у прямоугольника могут быть свои специфичные сеттеры и даже геттеры. Например геттер получения вписанной окружности. (при этом я подразумеваю не просто совпадение имени но и полное сохранение семантики) не во всякий прямоугольник можно вписать окружность.

                        0
                        Именно что общего решения нет.
                        Но и проблема «наследует ли квадрат от прямоугольника»- надуманная.
                          0

                          Не столько надуманная, сколько показывающая, что в такой формулировке недостаточно данных для ответа и надо рассматривать операции над фигурами и их совместимость. Если все операции совместимы по семантике, то можно. Но в прикладной задаче программирования объекты имеют свойство обрастать все новыми методами и сохранить консистентность не получится на практике. Потому, либо смириться с неконсистентностью, либо пректировать так, чтобы не появлялось таких завязок.

                            0
                            В данном случае всё просто- наследование изначально противоречило изменчивости.
                      +1
                      Для изменяемого прямоугольника вообще «квадрат» это свойство- он может стать на время квадратом, а потом перестать им быть. И это хорошо, что автор сделал размеры целыми, а то с double возникает проблема «с какой погрешностью наш прямоугольник можно считать квадратом» :D
                      +1
                      И да, квадрат это наследник прямоугольника.
                      Как по- по-вашему, красный прямоугольник наследник прямоугольника?
                        0

                        Нельзя отредактировать комментарий с мобильного. Какой же Хабр классный.

                          0
                          Скорее «цветной прямоугольник» может быть наследником прямоугольника. А «красный» это просто значение свойства.
                          Другое дело, что математические фигуры цвета не имеют. Для построение правильной иерархии надо понимать задачу — абстракцию чего нам надо создать.
                            +1

                            Я о том, что равенство сторон — это тоже просто значения свойства.

                              0
                              В целом- да. Но иногда- нет :)
                                +1

                                Продемонстрируйте пример этого "иногда".

                                  0
                                  Ещё раз- когда фигуры у нас это математические (иммутабельные) объекты, то может оказаться полезным иметь отдельный тип (в java лучше интерфейс, в связи с отсутствием множественного наследования — в C++ или Eiffel можно и классом обойтись) «квадрат», который наследует «прямоугольник» и «ромб».
                                  Хотя бы из-за этого наследования — квадрат можно передать туда, где ждут ромб.
                          0
                          >И да, квадрат это наследник прямоугольника
                          Нет.

                          >Все потому, что забываете о главном принципе ООП — абстракции
                          Вы сами напомнили и сами забыли ) Поэтому, по хорошему, мы делаем абстрактный класс Четырёхугольник и наследуем его. Тем самым, в иерархии квадрат и прямоугольник не будут наследоваться друг от друга.
                            +1
                            Какая польза от лишнего класса?
                            Лучший код- его отсутствие.
                            Каждая лишняя строчка должна приносить пользу.
                            Где-то надо иметь интерфейсы и кассы «многоугольник», «четырёхугольник», «выпуклый четырёхугольник» «прямоугольник», «квадрат», «ромб», где-то хватит двух классов.
                              –1
                              >Какая польза от лишнего класса?
                              В классе Квадрат вам будет достаточно одного поля для хранения значения стороны. Я думаю понятно, что это уменьшит расход оперативной памяти и очень драгоценного кэша ЦПУ, если у вас будет очень много квадратов. Не говоря уже о том, что будет меньше путаницы с квадратами и прямоугольниками в коде.

                              >Лучший код- его отсутствие.
                              Нет.

                              >Каждая лишняя строчка должна приносить пользу.
                              Нет. Если вы пишите на высокоуровневом ЯП, то это задача компилятора в уменьшении количества кода для ЦПУ, а не ваша. Ваша задача, как промышленного программиста, делать код понятным для другого человека, подразумевая, что он будет менее образованным, чем вы сами.
                            0

                            Добрый день!


                            1. Почему Абстрация важнее других принципов?
                            2. Почему математические фигуры автоматом иммутабельны? Квадрат с изменяемой стороной перестаёт быть квадратом в математическом смысле?
                              +1
                              Почему Абстрация важнее других принципов?


                              Потому что если посмотреть на ошибки проектирования- то неверная/протекающая абстракция встречается очень и очень часто.

                              Почему математические фигуры автоматом иммутабельны?


                              В математике даже нет понятия «присвоить переменной новое значение»
                              Если уж пишут, что x=5 то оно всегда равно 5, а не до какой-то точки.

                              Квадрат с изменяемой стороной перестаёт быть квадратом в математическом смысле?


                              На мой взгляд- да.
                                0

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

                            0

                            Это что за мужик такой, Лисков?

                              +3
                              его еще зовут Барбара
                                –3

                                Так может, Лискова, Лискову, Лисковой?

                                  +1

                                  и Анжела Меркелева...

                                    +1
                                    Лисков/Лискова — это вполне себе еврейская фамилия на русский лад. Погуглите. Так что всё-таки «Принцип подстановки Лисковой», «Квадрат Малевича», «Рейтинг Пейджа» (да-да, на эту тему даже юристы Гугла несколько раз стебались когда выступали ответчиками)
                                      +1
                                      Если бы она выросла в Бердичеве, была бы Варвара Лискова. Но она ведь родилась и всю жизнь прожила в США :) Поэтому Лисков.
                                        0

                                        Вот именно ;)

                                          0

                                          Если честно, не вижу связи.

                                            0

                                            У неё в документах написано "Liskov", не никаких оснований переводить "Лискова"

                                          +1
                                          Вот интересно получается с фамилиями мужчин и женщин. Ваш пример с «Рейтингом Пейджа» релевантен только потому, что он — Ларри. Будь на его месте Лариса Пейдж, язык бы не повернулся сказать «Рейтинг Пейдж-ы/евой». Думаю, так же и с Лисков не стоит искать еврейских корней.
                                  +5
                                  Так как мы работаем в парадигме ООП, которая, в частности, говорит нам, что классы должны моделировать объекты реального мира

                                  ООП совсем не про моделирование объектов реального мира.
                                    +2
                                    Так как класс Square находится в соотношении ISA с классом Rectangle, будет логично предположить, что тесты на Rectangle должны быть справедливы и для Square.

                                    Если у вас одинаковые тесты для обоих наследников, то зачем вам нужны были разные классы?
                                      0
                                      Пример выглядит очень надуманным. Мы ввели новую сущность, но тесты для неё используем от другой сущности и поэтому всё сломается. Ну да, а вы чего ожидали?
                                        +1
                                        Пример выглядит очень надуманным. Мы ввели новую сущность, но тесты для неё используем от другой сущности и поэтому всё сломается. Ну да, а вы чего ожидали?

                                        Это не просто другая сущность — это наследник, подкласс. И тесты, которые справедливы для родителя должны быть справедливы и для наследника как раз из-за Принципа подстановки.
                                      +5
                                      как правильно формировать иерархию наследования и ответим на философский вопрос о том, является ли квадрат прямоугольником.

                                      Ну что, опять?

                                      Вспомним принцип открытости-закрытости. Он говорит о том, что в хорошо спроектированных программах новая функциональность вводится путем добавления нового кода, а не изменением старого, уже работающего. И это есть основа написания поддерживаемого и переиспользуемого кода. Принцип подстановки Лисков (далее LSP) — это по сути гайдлайн того, как реализовать этот принцип при построении иерархии наследования классов в объектно-ориентированных языках

                                      Open-Close это вовсе не только наследование, лучше уже про композицию рассказывать, в большинстве случаев полезнее будет.
                                      Spoiler header
                                      Или, почему геттеры и сеттеры не ООП, хе-хе.

                                      парадигме ООП, которая, в частности, говорит нам, что классы должны моделировать объекты реального мира

                                      А автор термина говорил(раз говорил, два говорил, тут писал, тут в комментариях писал), что объекты должны быть изолированны как биологические клетки, и общаться друг с другом через посылку сообщений, а про моделирование реального мира — не говорил.
                                      Да и кому нужно реальный мир моделировать(соседа, кстати, надо или нет моделировать?) — не понятно.
                                        0
                                        Да и кому нужно реальный мир моделировать(соседа, кстати, надо или нет моделировать?) — не понятно.

                                        Моделирование есть абстрагирование от неважных для задачи деталей. Так что если сосед в задаче важен, то нам надо его моделировать

                                          0
                                          Хм. Да, вы правы.

                                          В любом случае, относительно определения — важные для бизнеса процессы вполне себе моделируются в любой парадигме, и что есть код который реализует бизнес-требования и не моделирует процессы я не могу себе представить, хотя и термин моделирования не так уж однозначен.

                                          А если пытаться спроецировать какие-то отношения из-реального мира на иерархии наследования(а-ля moderator extends user, или фигуры с геттерами, как выше пытаются) — то такого моделирования уж точно не нужно.
                                            0

                                            Для меня наследование — один способов моделирования отношений между сущностями предметной области и moderator extends user выглядит нормально на первый взгляд, если и то, и другое сущности предметной области.

                                              +1
                                              Для меня наследование — один способов моделирования отношений между сущностями предметной области и moderator extends user выглядит нормально на первый взгляд, если и то, и другое сущности предметной области.

                                              Я думаю если это и нормально где-то, должно быть много допущений, и сходу пример где это оправдано придумать не могу(ну, кроме случаев когда проект маленький и без разницы).

                                              Во первых, если есть какой-то единый класс User в проекте — очень часто(из того что я видел/узнавал/писал) он с запахом, особенно когда кто-то пытается моделировать User'ом живого пользователя из реального мира, и кладёт туда всё, что с этим человеком связано, и логины с паролями, и профиль с форума, и банковские аккаунты, получая в итоге god-object на 100 полей(да и то чаще просто огромную анемичную модель с геттерами-сеттерами т.к. на этом месте люди разочаровываются в рич-моделях, а декомпозировать как-то не принято, не учат).
                                              Думаю понятно что при таком раскладе уже пора решать проблемы, а не множить наследованием.

                                              Далее, сходу я могу набросать лишь пару контекстов, где может быть сущность «модератор» — секьюрити(права, доступы) и что-нибудь вроде юзер-профиля, который у модератора, например, будет особенный.
                                              В первом случае речь уже о конфигах каких-то, а не сущностях, а во втором — проще в контексте профилей завести свойство «тип профиля» или что-то подобное.

                                              Вобщем, мне интересно в каких случаях вы можете предложить такую иерархию, и почему она будет лучше чем вынесение общей логики отдельно.

                                              Я пока что считаю, что использование наследования это исключительно техническая деталь реализации, которая не должна выбираться только ради какой-то схожести с реальным миром. И с технической точки зрения штука эта — весьма опасная.
                                                0

                                                Вот именно потому что с технической точки зрения она довольно опасна, для технических задач типа DRY она должна использоваться только в исключительных случаях. Для моделирования же она вполне подходящая, пока а) отражает отношение "является" в контексте предметной области и б) не возникает для этого отношения проблемы множественного наследования


                                                Moderator extends User вполне может быть нормальным когда есть полное понимание, что модератор это реальный пользователь с дополнительными свойствами и особым поведением, не ограничивающееся просто правами. Например, у модератора может быть список забаненых и собственно метод banUser().

                                          0
                                          Open-Close это вовсе не только наследование, лучше уже про композицию рассказывать, в большинстве случаев полезнее будет.

                                          Да, это вовсе не только наследование, но LSP показывает нам как его соблюсти именно в контексте наследования.
                                          Да и кому нужно реальный мир моделировать(соседа, кстати, надо или нет моделировать?) — не понятно.

                                          Пожалуй я слишком упрощённо сформулировал. Речь идёт о том, что мы оперируем абстракциями, причём не столько непосредственно объектов реального мира сколько понятий. У каждого понятия есть набор признаков. При написании ООП-кода мы выбираем те из них, которые являются наиболее значимыми в конкретной предметной области, то есть формируем абстракцию.
                                          0
                                          С LSP проблема, что он очень чувствителен к формулировке.
                                          если для каждого объекта o1 типа S существует такой объект o2 типа T, что для всех программ P, определенных в терминах T, поведение программы P не изменяется, если вместо o2 подставить o1, то S — подтип T.
                                          если S является подтипом T, тогда объекты типа T в программе могут быть замещены объектами типа S без каких-либо изменений желательных свойств этой программы
                                          Функции, которые используют базовый тип, должны иметь возможность использовать подтипы базового типа, не зная об этом.


                                          То есть во втором и третьем случае требуется только совместимость, в то время как первая формулировка, если её понимать максимально строго, фактически запрещает полиморфизм.

                                          В то время, как в оригинале:
                                          Let phi(x) be a property provable about objects x of type T. Then phi(y) should be true for objects y of type S where S is a subtype of T.
                                          вакуум, сферичнее некуда, и никаких конкретных указаний там нет. Под него больше всего подходит как раз 2-й вариант.
                                            0

                                            Ну насколько красивее оригинальная формулировка

                                            +2
                                            Все становится гораздо интереснее, когда вы узнаете про pattern matching
                                              0
                                              Первый вариант лучше отрефакторить через полиморфизм? Внести метод отрисовки в класс?
                                                0
                                                Если фигуры приходят из сторонней библиотеки — мы не можем их изменять. А отрисовать в нашем приложении мы можем без проблем.
                                                0

                                                За пример с квадрато-прямоугольником — прямо мега, гига, супер респект. Невероятно визуально и интуитивно.

                                                  0
                                                  Так а в чем проблема прямоугольник унаследовать от квадрата?
                                                  Rectangle extends Square
                                                  extends — перев. расширять
                                                  Square — прямоугольник с наложенными на него ограничениями (т.е. прямоугольника «шире»)
                                                  А треугольник это частный случай четырехугольника, а четырехугольник частный случай шестиугольника у которого 2 угла 180 градусов.
                                                  (С) Ваш К.О.
                                                    0

                                                    Я бы еще отметил, что наследование классов — впринципе очень опасная операция, с которой надо обращаться аккуратно. Когда вы наследуюте один класс от другого, то вы подразумеваете, что наследуете то, как там ваш родитель "работает под капотом". Иначе говоря вы наследуете не только семантику, но еще и реализацию этой семантики.


                                                    Выражать конструкции вида "квадрат это прямоугольник" путем наследования класса Square от класса Rectangle — опасно (это как раз то, что приводит к тому, что класс Square в реализации в статье работает не оптимально и требует в два раза больше памяти чем нужно).


                                                    Правильнее определить два интерфейса (я думаю не стоит упоминать, что интерфейсы еще и позволяют Square унаследовать дополнительно от Rhombus...):


                                                    interface Rectangle {
                                                      int getHeight();
                                                      int getWidth();
                                                      int perimeter();
                                                    }
                                                    interface Square extends Rectangle {
                                                      int getSideLength();
                                                      default int getHeight() { return getSideLength(); }
                                                      default int getWidth() { return getSideLength(); }
                                                    }

                                                    И после этого писать реализации RectangleIml и SquareImpl под каждый. Причем на практике допустимо реализовать SquareImpl путем наследования от RectangleIml, это обычный инженерный trade-off между "написать поменьше кода" и "покушать меньше памяти". Просто академически это неправильно.


                                                    Ну а про мутабельность Rectangle и Square сверху умные люди уже написали. В данном случае иммьютабельность — это не просто поклон моде, а вынужденная необходимость, чтобы Rectangle и Square оставались прямоугольником и квадратом в математическом определении соответственно. Если есть желание по существующему объекту интерфейса Rectangle получить другой Rectangle, с измененной высотой, то это можно делать имьютабельно путем добавления метода в интерфейс Rectangle:


                                                      /**
                                                       * Creates new Rectangle which inherits width from {@code this} Rectangle but has {@code newWidth}
                                                       */
                                                      Rectangle rectWithWidth(int newWidth);
                                                      0
                                                      Известно, что наследование реализует отношение «является» (ISA).

                                                      Я не думаю, что много программистов на практике придерживаются этого. В основном это вопрос компоновки кода, а не взаимосвзяей. Т.е. с помощью ООП пытаются реализовать более древнюю мантру "не повторяй себя".

                                                        +2
                                                        Ну почему опять на этом примере? Такое чувство, что принцип был придуман только ради квадрата-прямоугольника.
                                                        Я занимаюсь разработкой на Java в Tinkoff.ru

                                                        И не нашлось примеров из реальной работы?
                                                        Если нет, то либо сами не применяете принцип либо негде применять в реальности:)
                                                          +1

                                                          Как же у меня полыхает, когда из SOLID делают заповеди. Заповедь L гласит: "И не унаследует же никто квадрат от прямоугольника, ибо не будет тому прощения отныне и во веки веков".


                                                          1. Что будет нарушено, если абстракции фигур не будут требовать операций нахождения периметра и площади и прочих нарушающих L в SOLID?
                                                          2. Может всё-таки воздержаться от утверждений "A нельзя моделировать как наследник B"?
                                                          3. Может быть всё-таки всё дело в конкретной выбранной абстрактной модели под конректную задачу?
                                                            0
                                                            Проблема, думаю, в том, что у людей после нынешних статей «про ООП» в голове не объекты с поведением, а мутабельные структурки данных(поле+геттер+сеттер), которые меняют отдельные процедуры.
                                                              0

                                                              На геттеры/сеттеры давно пора начать крестовый(++) поход! OOP VULT!

                                                                0

                                                                Геттеры норм для сущностей, пока сущности не выходят за пределы слоя бизнес-логики

                                                                  0
                                                                  На геттеры/сеттеры давно пора начать крестовый(++) поход! OOP VULT!

                                                                  А в самой лишь войне против них не много смысла. Если воинственно отстаивать их «ненужность», вы будете либо вызывать презрение и недоверие, либо просто один карго культ замените другим.

                                                                  Нужно развивать понимание, а не сеять смуту.

                                                            Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                                            Самое читаемое