Типичные заблуждения об ООП

Автор оригинала: Janos Pasztor
  • Перевод
Привет, Хабр!

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



Мертва ли парадигма ООП? Можно ли сказать, что за функциональным программированием будущее? Кажется, что во многих статьях пишут именно об этом. Склонен с такой точкой зрения не согласиться. Давайте обсудим!

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

Ранее я уже писал о том, что ООП и ФП не противоречат друг другу. Более того, мне удалось весьма успешно сочетать их.

Почему же у авторов этих статей возникает такая масса проблем с ООП, и почему ФП кажется им настолько очевидной альтернативой?

Как обучают ООП


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

Однако, ООП, как и ФП – это инструмент. Для решения задач. Его можно употреблять, им же можно злоупотреблять. Например, создавая неверную абстракцию, вы злоупотребляете ООП.
Так, класс Square никогда не должен наследовать класс Rectangle. В математическом смысле они, конечно же, связаны. Однако, с точки зрения программирования они не находятся в отношениях наследования. Дело в том, что требования к квадрату жестче, чем к прямоугольнику. Тогда как в прямоугольнике – две пары равных сторон, у квадрата обязательно должны быть равны все стороны.

Наследование


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

class BlogController extends FrameworkAbstractController {
}

Предполагается, что таким образом вам будет проще выполнять вызовы вроде this.renderTemplate(...), поскольку такие методы наследуются от класса FrameworkAbstractController.

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

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

class BlogController {
    public BlogController (
        TemplateRenderer templateRenderer
    ) {
    }
}

Вот видите, вы больше не зависите от какого-то туманного FrameworkAbstractController, а зависите от очень хорошо определенной и узкой штуки, TemplateRenderer. На самом деле, BlogController не занимается никаким наследованием от какого-либо другого контроллера, так как не наследует никаких поведений.

Инкапсуляция


Вторая зачастую критикуемая черта ООП – инкапсуляция. На литературном языке смысл инкапсуляции формулируется так: данные и функционал поставляются вместе, а внутреннее состояние класса скрывается от внешнего мира.

Эта возможность, опять же, допускает употребление и злоупотребление. Основной пример злоупотребления в данном случае – дырявое состояние (leaky state).

Условно говоря, предположим, что в классе List<> содержится список элементов, и этот список можно изменить. Давайте создадим класс для обработки корзины заказов следующим образом:

class ShoppingCart {
    private List<ShoppingCartItem> items;
    
    public List<ShoppingCartItem> getItems() {
        return this.items;
    }
}

Здесь в большинстве ООП-ориентированных языков произойдет следующее: переменная items будет возвращаться по ссылке. Поэтому далее можно сделать так:

shoppingCart.getItems().clear();

Таким образом мы фактически очистим список элементов в корзине, а ShoppingCart об этом даже не узнает. Однако, если как следует присмотреться к этому примеру, то становится понятно, что проблема отнюдь не в принципе инкапсуляции. Здесь как раз нарушается этот принцип, поскольку из класса ShoppingCart утекает внутреннее состояние.

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

Неопытные программисты часто нарушают принцип инкапсуляции и другим образом: вводят состояние там, где в нем нет нужды. Такие неопытные программисты часто используют переменные приватного класса для передачи данных от одной функции к другой в пределах одного и того же класса, тогда как правильнее было бы использовать объекты передачи данных (Data Transfer Objects), чтобы передавать иной функции сложную структуру. В результате таких ошибок код излишне усложняется, что может приводить к возникновению багов.

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

Абстракция


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

Если вы так делаете без веской на то причины, то просто ищете неприятностей на свою голову. Не важно, как именно делается абстракция – как абстрактный класс или как интерфейс; в любом случае, в коде появится лишняя сложность. Эта сложность должна быть оправданной.
Проще говоря, интерфейс можно создавать лишь при условии, если вы готовы потратить время и документировать поведение, которое ожидается от реализующего его класса. Да, вы меня верно прочли. Мало просто составить список функций, которые потребуется реализовать – также опишите, как (в идеале) они должны работать.

Полиморфизм


Наконец, давайте поговорим о полиморфизме. Он предполагает, что один класс может реализовывать множество поведений. Плохой хрестоматийный пример — написать, что Square при полиморфизме может быть как Rectangle, так и Parallelogram. Как я уже указывал выше, подобное в ООП решительно невозможно, так как поведения этих сущностей отличаются.

Говоря о полиморфизме, следует держать в уме поведения, а не код. Хороший пример — класс Soldier в компьютерной игре. Он может реализовывать как поведение Movable (ситуация: он может двигаться), так и поведение Enemy (ситуация: стреляет в вас). Напротив, класс GunEmplacement может реализовывать только поведение Enemy.

Итак, если написать Square implements Rectangle, Parallelogram, это утверждение не становится истинным. Ваши абстракции должны работать в соответствии с бизнес-логикой. Следует подробнее задумываться о поведении, чем о коде.

Почему ФП – не серебряная пуля


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

С точки зрения многих адептов ФП, классы – это кощунство, и код должен быть представлен в виде функций. В зависимости от языка, данные могут передаваться от функции к функции при помощи примитивных типов, либо в виде того или иного структурированного множества данных (массивы, словари, т.д.).

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

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

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

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

ООП или ФП?


ООП и ФП – это инструменты. В конечном счете, не так важно, какой парадигмой программирования вы пользуетесь. Проблемы, описываемые в большинстве статей на эту тему, касаются организации кода.

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

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

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

Книги о ФП и ООП

  • 48.1%Требуется базовая книга о принципах ФП93
  • 33.1%Требуется базовая книга о принципах ООП64
  • 46.6%Статья не впечатлила90
Издательский дом «Питер»
166,55
Компания
Поделиться публикацией

Похожие публикации

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

    –1
    А если бы мы избрали подход с композицией и внедрением зависимостей, то получилось бы так:
    Обратная сторона копозиции это повторяющийся код. В вашем примере фактически каждый контроллер в системе должен иметь такой конструктор.

    class BlogController {
        public BlogController (
            TemplateRenderer templateRenderer
        ) {
        }
    }
      +2
      Повторение само по себе не является чем-то плохим.
        +1

        Оно является чем-то плохим, если надо синхронизировать изменения во всех местах.


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

          0
          А в геймдеве композиция выглядит примерно так:
          class Container {
              public List<Component> Components;
              public Container(Component[] сomponents);
          }
          
          class TemplateRenderer extends Component;
          
          // использование:
          var blogController = new Container([templateRenderer]);
          

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

            blogController обычно имеет специфически только для него методы типа showPosts(page = 1), showPost(id), createPost(postData) и т. п. которые и дергают templateRenderer с теми или иными параметрами. Можно, конечно, вместо наследования от базового класса контроллера и явной их имплементации как-то собирать их через лямбды, переданные в конструктор, но, скорее всего, потеряем, как минbмум, автодополнение, контроль типов, удобную навигацию в IDE и т. п.

              0
              навскидку:
              Component templateRenderer = new TemplateRenderer();
              Component postController = new PostController();
              postController.templateRenderer = templateRenderer;
              Container blogController = new Container([templateRenderer, postController]);
              

              Еще в класс Component можно добавить ссылку на объект-контейнер
              и реализовать в нем метод поиска нужного компонента, например по типу.
              Тогда вместо строки postController.templateRenderer = templateRenderer;
              можно в самом postController сделать поиск templateRenderer среди компонентов связанного объекта-контейнера.

              Откуда я такой подход взял:
              docs.unity3d.com/ru/current/Manual/UsingComponents.html
              martalex.gitbooks.io/gameprogrammingpatterns/content/chapter-5/5.1-component.html
      –1
      Так, класс Square никогда не должен наследовать класс Rectangle.


      а что мешает? раз математически / логически квадрат — частный случай прямоугольника, программирование продиктовано логикой.
        +4
        Это же классический пример из литературы: у прямоугольника есть методы setWidth и setHeight, которые поидее работают независимо друг от друга. Но когда вы наследуете от него квардрат, вам нужно сделать так, чтобы при изменении ширины/высоты он оставался квдратом, то есть менялась и вторая величина. Кажется, что в этом нет ничего страшного, но нарушается LSP.
        Например, вам нужна функция которая увеличивает площадь прямоугольников в 2 раза и подходящее решеним, например, увеличить их ширину в 2 раза:

        public static void doubleSquare(List<Rectangle> rectangles) {
            for (Rectangle rectangle : rectangles) {
                rectangle.setWidth(rectangle.getWidth() * 2);
            }
        }
        


        Но если в списке прямоугольников будут и квадраты, то поведение становится неправильным.
          0
          А если сделать квадрат иммутабельным, то он сможет заменить иммутабельный прямоугольник?
            0
            Иммутабельные объекты это уже больше про ФП, тут я привел пример классического объяснения проблемы, возникающей при неправильном наследовании.
              0

              Мутабельными могут быть, например, координаты прямоугольник, а ширина и высота иммутабельными. Нельзя говорить что-то вроде "нельзя на следовать Б от А" не объявляя ожидаемого поведения А. Особенно, не добавляя "не нарушая принципа постановки Дисков"

            –1
            Можно для квадрата переопределить и setWidth, и setHeight.
            Например, так, чтобы каждый изменял сразу обе стороны.
            Тогда квадрат останется квадратом.
              0
              Я кажется так и написал, у квадрата переопределены оба метода и увеличатся в два раза и ширина и высота, а значит площадь увеличится в четыре раза, а не в два. При этом он останется квадратом, но контракт метода будет нарушен.
                0

                Сначала нужно зафиксировать контракт. В посте он не зафиксирован.

                  0
                  Это просто означает что ваша функция doubleSquare неверна. И это не проблема квадрата :) Это проблема функции, которая могла бы сначала узнать тип фигуры и применять соответствующий метод.
                    0
                    Тип фигуры функции уже передан — это Rectangle.
                      0
                      Да, и эта функция по хорошему, обязана вызвать соотвествующий виртуальный метод объекта doubleSquare, которые для обычного рестангле и для квадрата будут каждый своим.
                      Понамешают функциональщины с ООП, потом сами разобраться не могут и кричат, что ООП профнепригодно :)

                      Функция doubleSquare в том виде как представлена, нарушает принцип инкапсуляции ООП (все изменения объекта только средствами самого объекта). Программист — ССЗБ в этом случае, ООП ни при чем.
                  0
                  А теперь предположим есть отверстие с прямоугольными углами, которое принимает Rectangle, и чтобы наш класс туда поместился необходимо задать ему нужную ширину и высоту и тогда все туда пролезет. Square же попадает под условие сигнатуры, но когда мы попытаемся присвоить ему setWidth(1) и setHeight(2) (что является достаточным условием для метода отверстия), он почему-то туда все равно не пролезет. Налицо нарушение принципа Барбары Лисков. Вызывая один из методов setWidth или setHeight у класса Rectangle, мы ожидаем, что изменим каждую из сторон независимо, но тут внезапно врывается Square, который меняет стороны имплицитно.
                    0
                    мы ожидаем, что изменим каждую из сторон независимо

                    Почему вы этого ожидаете? Контракт к setWidth/setHeight вам этого не обещал.

                      0
                      Что вы подразумеваете под контрактом? Можем создать класс Weirdtangle, который будет иметь setWidth/setHeight, но имплицитно второе свойство будет устанавливать в ноль. Мы нарушили контракт ожидаемого поведения Rectangle? Сигнатуры в норме, проект собирается, вот только вся логика написанная для работы с Rectangle с этим классом летит как фанера.
                        +1
                        Мы нарушили контракт ожидаемого поведения Rectangle?

                        Без понятия, я не знаю что мне обещает документация к Rectangle. Вы на основании чего-то считаете что после вызова setWidth метод getHeight будет возвращать то же, что и до вызова. Но на основании чего вы так решили?

                          0

                          На том, что я просил менять ширину, а не высоту.


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


                          Ломать этот контракт для сохранения мутабельности как-то странно. Если с таким контрактом смириться, то завтра у вас ширина начнет ещё и от цвета фигуры зависеть.

                            +1

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

                            0
                            Без понятия, я не знаю что мне обещает документация к Rectangle.

                            Учебник геометрии за пятый класс достаточно точно дает определение тому, что есть прямоугольник. Обсуждается как раз таки классический случай из геометрии. Контекст обсуждения предельно ясен и прозрачен. Вы можете, конечно, его пытаться искусственно усложнить в угоду своей позиции, но боюсь на этом дальнейшее обсуждение скатится к софистике.
                            Вы на основании чего-то считаете что после вызова setWidth метод getHeight будет возвращать то же, что и до вызова.

                            Не знаю даже. Может быть вот en.wikipedia.org/wiki/Rectangle
                            На основании того, что стороны прямоугольника являются не связанными друг с другом величинами, но в совокупности придающими прямоугольнику дополнительные свойства. Например, периметр, выведение которого производится общеизвестным методом.
                              0

                              А может быть и: https://en.wikipedia.org/wiki/Golden_rectangle


                              Вы зря в обсуждении программной модели пытаетесь сослаться на математические абстракции, которые даже в самой математике имеют множество разных интерпретаций. Классический пример: https://en.wikipedia.org/wiki/0#Mathematics

                          0
                          Потому-что контракт к прямоугольнику это обещал.

                          Наследники должны расширять поведение родителей, а не замещать.
                            0

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

                              0
                              Если обещал.

                              Ну собственно само название setWidth это подразумевает

                              Если квадрат является потомком прямоугольника то он обязан проходить все тесты, которые проходит прямоугольник. Гуглите LSP
                                0
                                Если наследник проходит абсолютно все тесты предка, то его поведение не меняется. Но в этом случае, какой смысл создавать новый класс?
                                  +2
                                  Если наследник проходит абсолютно все тесты предка, то его поведение не меняется.

                                  Неверно. Его поведение может меняться без нарушения прохождения тестов. Например могут появиться новые методы или свойства. Могут появиться опциональные аргументы в старых методах.
                                    +1
                                    Неверно. Его поведение может меняться без нарушения прохождения тестов.

                                    Мне тут, кстати, подумалось, что LSP не выполняется вообще никогда, если, например, у вас есть рантайм-рефлексия. Он же сформулирован в терминах любого предиката, а с рантайм-рефлексией вы, очевидно, можете отличить класс от его наследника (даже если наследник не добавляет вообще ничего).

                                      0

                                      Зависит от уровня рефлекси. Какой-то user instanceof User обычно возвращает true как для самого User, так и для его наследников. Главное не лезть в имя класса и не проверять отсутствие членов.

                                        0
                                        Да, забавный артефакт.

                                        Но думаю тут можно простить его нарушение, все-таки instanceof чаще всего возвращает true для подклассов, а если разработчик городит свои костыли, то он ССЗБ.
                          +1

                          Да, с setWidth и setHeight всё просто — объект может обеспечить свои инварианты изменя связанные состояния. А вот с методом прямоугольника setDimensions( width , height ) уже сложнее, так как реализовать этот контракт квадрат в принципе не сможет.
                          На всякий случай — если он начнёт кидать исключение или тупо игнорировать в случае неравных параметров, то это тоже нарушение контракта.

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

                            Что же касается функции, которая увеличивает площадь в 2 раза, первое, что мне пришло в голову — не ваш пример, а увеличение каждой из сторон в sqrt(2) раза, тогда одна и та же функция подойдет как для прямоугольника, так и для квадрата. С одной стороны увеличить одну из сторон проще, но тогда меняются пропорции прямоугольника, что в некоторых случаях неприемлемо.

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

                            А в целом вы говорите верно, ООП и ФП — всего лишь инструменты для достижения поставленной задачи. Вот тут хотелось бы увидеть больше конкретики и примеров что в каких случаях больше использовать. Дарю идею для холивара вашей следующей статьи.

                            P.S. Мне в JS поначалу сильно не хватало трейтов из php для организации поведений и «мультинаследования» в ООП. Но со временем я научился думать по-другому.

                            P.P.S. В своих проектах использую оба подхода, но в крупных чаще ООП.
                              +1
                              Что же касается функции, которая увеличивает площадь в 2 раза, первое, что мне пришло в голову — не ваш пример, а увеличение каждой из сторон в sqrt(2) раза

                              Это будет работать, пока компилятор не решит поменять порядок чтения:


                              1. прочитать длину, увеличить длину, записать длину,
                              2. прочитать ширину, увеличить ширину, записать ширину.

                              В таком варианте квадрат все равно увеличится в 4 раза.

                              0

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

                                0
                                Это же классический пример из литературы: у прямоугольника есть методы setWidth и setHeight, которые поидее работают независимо друг от друга. Но когда вы наследуете от него квардрат, вам нужно сделать так, чтобы при изменении ширины/высоты он оставался квдратом, то есть менялась и вторая величина. Кажется, что в этом нет ничего страшного, но нарушается LSP.

                                а наследуйте прямоугольник от квадрата да и всё )
                                0
                                Таки квадрат — это четырехугольник. И наследует полностью только его поведение, тогда как поведение прямоугольника и квадрата — различаются между собой.Свойства больше разных у них, чем одинаковых, т.е. и в самом деле этот хрестоматийный пример предполагает, что нет смысла наследоваться именно от прямоугольника(в википедии сказано, что в квадрат — частный случай в том числе и ромба, почем ромб не сделать родителем?)
                                  0

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

                                  +1

                                  Логически квадрат это не частный случай прямоугольника а вычисляемое свойство, то есть сущность прямоугольника которая позволяет менять ширину и длину время от времени может становится квадратом при совпадении длины и ширины


                                  class Square {
                                    isRectangle() {
                                      return this.getWidth() == this.getHeight()
                                    }
                                  }
                                    0
                                    тогда, вероятно, моё утверждение стоит исправить на «квадрат можно наследовать от четырёхугольника».
                                      +1

                                      Только наоборот, Rectangle и isSquare

                                        0

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

                                        0
                                        struct Square {                     struct Rectangele 
                                                                            : Square {
                                            float a;                          //float a
                                                                                float b;
                                            float perimeter() {                 float perimeter() {
                                                 return a * 4                       return (a + b) * 2
                                            }                                   }
                                            float area() {                      float area() {
                                                 return a * a                       return a * b;
                                            }                                   }          
                                        }                                   }
                                        
                                        struct Diamond
                                        : Square {
                                            float angle;
                                            
                                            float area() {
                                                return a * a / 2;
                                            }
                                        }
                                        
                                        struct Parallelogram
                                        : Diamond , Rectangele {
                                            //...
                                        }                  
                                        
                                          +1

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

                                            0
                                            Обычно мешает то, что квадрат не соблюдает контракт прямоугольника для многих операций.
                                            Наследование должно удовлетворять требованию X является Y, а квадрат выполняет его не всегда.
                                              0

                                              Зависит от набора операций, определённых над прямоугольником. Точнее от его инвариантов и прочих *условий.


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


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

                                                0

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

                                                  +1

                                                  Ну вот обычно непонятен точный контекст. Особенно учитывая, что в геометрии как-то не принято изменять фигуры.

                                            +2

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

                                              0
                                              Статья странная, а книга по принципам ФП — нужна.
                                                0

                                                Интересно, но не объяснено почему нельзя наследовать Square от Rectangle ("ведут себя по-разному" так себе аргумент). Один комментатор предложил, что у Rectangle меняются обе стороны, а у Square — одна. Так-то оно так, но я бы сказал, что можно реализовать по-другому. Раз Square — частный случай Rectangle (в математике), то я бы написал так:


                                                class Rectangle:
                                                    def __init__(self, width, height):
                                                        self.width = width
                                                        self.height = height
                                                
                                                class Square(Rectangle):
                                                    def __init__(self, size):
                                                        super().__init__(size, size)
                                                  0

                                                  А потом какой-нибудь код, не знающий разницы между квадратом и прямоугольником меняет созданному Square width.

                                                    0
                                                    Контракт Rectangle подразумевает изменение только одной стороны одной функцией. Если будет логика, которая полагается на независимое изменение сторон, то Square автоматически становится невалидным для этой функции.
                                                      0
                                                      Контракт Rectangle подразумевает изменение только одной стороны одной функцией.

                                                      Кто сказал?

                                                        0
                                                        Название метода setWidth, в котором нет слова Height или Both или какого-нибудь DeleteThisProjectToHell.
                                                          0

                                                          У вас контракты описываются исключительно через имя метода?

                                                            0
                                                            В числе прочих. Naming convention никто не отменял.
                                                              0

                                                              Не расскажете тогда какой у вас в данном случае Naming convention что он позволяет понять что метод setWidth меняет ещё скажем и площадь фигуры?

                                                                0
                                                                Площадь это вычисляемое свойство. Следует из контракта самого класса.

                                                                Как бы вы отнеслись к коллекции, у которой метод sort вместо сортировки всех данных, удалял бы все содержимое? Не посчитали ли бы вы naming convention такой библиотеки, как минимум, странным? Или если бы коллекция ImmutableItems на самом деле была бы мутабельной? Если вы будете игнорировать naming convention во время составления контракта, то вы потеряете очень важную составляющую.
                                                                  0

                                                                  А контракт класса тоже идёт по naming convention? А что мешает кому-то в контракте класса прописать что высота у данного конкретного класса тоже "вычисляется" и зависит от ширины?

                                                                    0
                                                                    Для этого придётся сделать изменения в базовом классе Rectangle. Это не всегда возможно — либо его кодом владеет другая команда, либо он уже используется в 100500 функциях, и переименование setWidth в setWidthAndPossiblyHeightChanges потребует рефакторинга их всех.
                                                                      0

                                                                      Дело не в этом. Ясное дело что наш пример примитивен и достаточно просто понять как он работает. И как он по идее должен работать мы вроде себе тоже представляем.


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


                                                                      А если взять не такой банальный пример, а какую-нибудь сложную бизнес-логику из реальных процессов, то там ещё "опаснее" полагаться на интуицию. И только на naming conventions тоже опасно полагаться.

                                                                        0
                                                                        В идеале, naming conventions должны быть подмножеством контракта. То есть, все требования в них невозможно запихать, но наименования не должны противоречить контракту.
                                                                          0

                                                                          И для того чтобы определить противоречат наименования или нет надо иметь на руках этот самый контракт.


                                                                          И даже в примере с квадратами/прямоугольниками этот "контракт" похоже немного разный у разных людей.

                                                                            +1

                                                                            Контракт конвеншина set об ычно подразумевает исключительно что get вернёт то же самое. Ни разу не видел тестов, проверяющих, что результаты других get не изменились после вызова конкретного set.


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

                                                                      0

                                                                      Вы постулируете, что площадь вычисляемое свойство в комментарии Н-го уровня. Не спортивно.

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

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

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

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


                                                                        Это в общем случае не особо логично, но возможно.

                                                          +1
                                                          Проблемы начинаются, когда одни функции должны опираться на другие. Когда функция A вызывает функцию B, а функция B вызывает еще пять-шесть функций, а в самом конце обнаруживается функция заполнения нулями, которая может сломаться – вот тут-то вам не позавидуешь.

                                                          Не понял, а специфичные для ФП проблемы-то тут где?


                                                          в частности, применять инверсию зависимостей, при которой ФП на практике также значительно усложняется.

                                                          Просто берёшь и заворачиваешься в монаду. Хорошо, явно, композабельно, проверяемо компилятором.

                                                            +1
                                                            >Не понял, а специфичные для ФП проблемы-то тут где?
                                                            Да нигде, что вы. Вы на ссылку-то посмотрите — она ведет, для начала, на обсуждение функции left_pad в NPM. С каких-это пор Javascript (node) и его NPM стали хоть сколько-то репрезентативным примером применения ФП?

                                                            >Просто берёшь и заворачиваешься в монаду.
                                                            Ну да. Правильнее было бы сказать как-то так: инверсия зависимостей в ФП реализуется и действует иногда совсем не так, как это принято в ООП, что может вводить в заблуждение неопытных разработчиков (я бы сюда включил и автора оригинала). Это было бы правильнее.
                                                              0

                                                              Почему совсем не так? Вы не путаете инверсию зависимостей и иньекцией зависимостей?

                                                                0
                                                                >Вы не путаете инверсию зависимостей и иньекцией зависимостей?
                                                                Не, не путаю. В ООП инверсия зависимостей в том, что зависеть вы должны от интерфейсов, а не от реализаций. В ФП это выглядит иначе. В принципе, мое замечание касается обоих: как инверсии, так и иньекции.

                                                                Автор оригинала не удосужился пояснить, что именно он имел в виду, а переводчик усугубил:

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


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

                                                                Оригинал: pasztor.at/blog/clean-code-dependencies
                                                                Перевод: www.piter.com/collection/all/product/spring-vse-patterny-proektirovaniya

                                                                Ну т.е. автор ссылается на себя, а переводчик — рекламирует свои переводы.
                                                                  0

                                                                  В ФП по большому счёту это выглядит так же, по-моему. Просто вместо классового интерфейса или абстрактного класса, ФП функция будет зависеть от функционального типа, сигнатуры "колбэка"

                                                                    0
                                                                    Я не помню, где бы в ФП имели место абстрактные классы. Ну то есть с моей точки зрения, все несколько проще, чем бывает в ООП. Но однако же, если мы начнем про композицию программы из частей — то она все-таки будет устроена несколько иначе.
                                                            0
                                                            когда наследование заменяется композицией, можно получить проблему первого типа из пункта про инкапсуляцию, а обходится она обычно довольно-таки большим объемом кода.

                                                            В целом, хорошо бы вообще обходиться без состояния – хранить в классах изменяемые данные, когда только возможно

                                                            вы наверно имели в виду «неизменяемые» данные?

                                                            При правильном использовании ООП (да и вообще это одна из главных фишек подхода) объекты в программе всегда находятся в одном из множества корректных состояний. Согласен, энтропию объекта стоит уменьшать. Однако в большинстве случаев иммутабельность для ООП — оверкилл.
                                                              –2
                                                              1. Rectangle { setHeigth, setArea; getHeight, getArea, getWidth }
                                                              2. Square: Rectangle
                                                              3. Profit
                                                                0
                                                                Я бы зашел с другой стороны…
                                                                В природе есть два типа полезных данных — статические и динамические. Пример первых — фотография, пример вторых — видео. Для статических данных (СД) важно их хранение. Например, для фото надо хранить: координаты пикселя, его яркость, его RGB. А для динамических данных (ДД) ничего хранить не надо, единственная задача — это успевать их правильно обрабатывать и выдавать «в никуда», где они могут бесследно и безболезненно исчезать. Пример — видео и смотрящий его человек. Для работы с СД придумали ООП, а для работы с ДД — ФП.
                                                                И всё работало более менее нормально, пока не возникла необходимость взаимодействия одновременно с двумя типами данных. И тут пошло поехало…

                                                                В чем я вижу проблемы ООП.
                                                                а) Идея, что «всё есть объект» (слава Богу, встречается редко). Так как объект — это не просто данные, а нечто, обладающее своим поведением/характером (привет инкапсуляция).
                                                                б) Идея, что надо объединять данные и связанные с ним методы в одну сущность. Фактически современный класс — это монолитная программа допроцедурного программирования. А нам достаточно, чтобы объект хранил переменную под присмотром своего «характера». А возможные операции с этими данными надо выносить за пределы класса как такового (привет функциям из ФП).
                                                                в) бесконечные попытки выдать наследование и полиморфизм как признаки ООП. Ведь первое — это просто переиспользование кода, а второе — на самом деле ближе к ФП (так как обработка данных).

                                                                Проблема ФП.
                                                                а) попытка хранить состояние (то есть СД) способами, разработанными для операций с ДД (привет монады).

                                                                Мой идеальный мир…
                                                                Объекты хранят только состояния, а за пределами объектов — чистое ФП.
                                                                  0

                                                                  Объекты без поведения — это просто структуры данных типа сишных struct

                                                                    0
                                                                    Поведение/характер должны быть, конечно
                                                                  +1
                                                                  ООП и ФП — уже довольно старые технологии. Т.ч. вместо неких «новых» книг, которые создаются ухудшением старых, предпочёл бы старые.
                                                                  Хорошие книги написал Бертран Мейер, но они у него объёмные и сложные, т.ч. могут быть трудности с их продажей.
                                                                  По поводу жалоб на ООП: почти везде эта технология воплощена урезанно и криво, и жалобы больше относятся к языкам программирования, чем к ООП.
                                                                    0

                                                                    Они не столько сложные, сколько оторванные от реалий условно современной разработки

                                                                    +2

                                                                    Мочи мочало, начинай сначала. Автор статьи неграмотен: ни что из перечисленного не является неотъемлемой чертой ООП, ни наследование, ни инкапсуляция, ни полиморфизм, ни абстракция. И всё это есть в чисто функциональном Haskell. Так что, объявим Haskell объектно-ориентированным?

                                                                      0

                                                                      А почему никто не сказал, что полиморфизм в статье определен в корне неверно? Автор банально не знает самых основ ООП.

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

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