Привет, Хабр!

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



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

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

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

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

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


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

Однако, ООП, как и ФП – это инструмент. Для решения задач. Его можно употреблять, им же можно злоупотреблять. Например, создавая неверную абстракцию, вы злоупотребляете ООП.
Так, класс 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 вызывает еще пять-шесть функций, а в самом конце обнаруживается функция заполнения нулями, которая может сломаться – вот тут-то вам не позавидуешь.

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

ООП или ФП?


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

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

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

Комментарии (80)


  1. E_STRICT
    27.09.2019 11:01

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

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


    1. BOM
      27.09.2019 18:31
      +1

      Повторение само по себе не является чем-то плохим.


      1. VolCh
        28.09.2019 17:41
        +2

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


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


  1. disgusting
    27.09.2019 11:59

    Так, класс Square никогда не должен наследовать класс Rectangle.


    а что мешает? раз математически / логически квадрат — частный случай прямоугольника, программирование продиктовано логикой.


    1. andrikeev
      27.09.2019 12:21
      +1

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

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


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


      1. alexxisr
        27.09.2019 12:56

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


        1. andrikeev
          27.09.2019 15:13

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


          1. VolCh
            28.09.2019 17:46

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


      1. hd_keeper
        27.09.2019 15:04

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


        1. andrikeev
          27.09.2019 15:11

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


          1. VolCh
            28.09.2019 17:47

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


          1. pvsur
            01.10.2019 10:43

            Это просто означает что ваша функция doubleSquare неверна. И это не проблема квадрата :) Это проблема функции, которая могла бы сначала узнать тип фигуры и применять соответствующий метод.


            1. qw1
              01.10.2019 11:34

              Тип фигуры функции уже передан — это Rectangle.


              1. pvsur
                01.10.2019 12:32

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

                Функция doubleSquare в том виде как представлена, нарушает принцип инкапсуляции ООП (все изменения объекта только средствами самого объекта). Программист — ССЗБ в этом случае, ООП ни при чем.


        1. BOM
          27.09.2019 18:35

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


          1. slonopotamus
            27.09.2019 22:08

            мы ожидаем, что изменим каждую из сторон независимо

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


            1. BOM
              27.09.2019 22:51

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


              1. slonopotamus
                27.09.2019 22:58

                Мы нарушили контракт ожидаемого поведения Rectangle?

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


                1. 0xd34df00d
                  27.09.2019 23:35

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


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


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


                  1. vintage
                    29.09.2019 05:20
                    +1

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


                1. BOM
                  28.09.2019 00:09

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

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

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


                  1. vintage
                    29.09.2019 05:40

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


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


            1. TheRikipm
              28.09.2019 10:17

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

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


              1. VolCh
                28.09.2019 17:51

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


                1. TheRikipm
                  28.09.2019 19:10

                  Если обещал.

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

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


                  1. qw1
                    28.09.2019 19:15

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


                    1. TheRikipm
                      28.09.2019 19:29
                      +2

                      Если наследник проходит абсолютно все тесты предка, то его поведение не меняется.

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


                      1. 0xd34df00d
                        28.09.2019 20:47
                        +1

                        Неверно. Его поведение может меняться без нарушения прохождения тестов.

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


                        1. VolCh
                          28.09.2019 21:31

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


                        1. TheRikipm
                          28.09.2019 23:25

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

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


        1. vintage
          29.09.2019 05:49
          +1

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


      1. myemuk
        27.09.2019 21:52

        Обычно на практике в таких случаях методы setWidth и setHeight перезаписываются таким образом, что фигура все-таки остается квадратом. Если используется setWidth, то он вызовет setHeight, и наоборот.

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

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

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

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

        P.P.S. В своих проектах использую оба подхода, но в крупных чаще ООП.


        1. Deosis
          30.09.2019 14:06

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

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


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

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


      1. VolCh
        28.09.2019 17:43

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


      1. Antervis
        30.09.2019 17:04

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

        а наследуйте прямоугольник от квадрата да и всё )


    1. cudu
      27.09.2019 12:59

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


      1. VolCh
        28.09.2019 17:52

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


    1. bgnx
      27.09.2019 14:24
      +1

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


      class Square {
        isRectangle() {
          return this.getWidth() == this.getHeight()
        }
      }


      1. disgusting
        27.09.2019 14:33

        тогда, вероятно, моё утверждение стоит исправить на «квадрат можно наследовать от четырёхугольника».


      1. brain_tyrin
        27.09.2019 20:32

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


      1. VolCh
        28.09.2019 17:58

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


    1. klevercat
      27.09.2019 20:32

      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. vintage
      28.09.2019 17:42
      +1

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


  1. Kanut
    27.09.2019 12:32

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


  1. AstarothAst
    27.09.2019 13:22

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


  1. WhiteBlackGoose
    27.09.2019 16:55

    Интересно, но не объяснено почему нельзя наследовать 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)


    1. Aldrog
      27.09.2019 17:55

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


    1. BOM
      27.09.2019 18:40

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


      1. slonopotamus
        27.09.2019 22:17

        Контракт Rectangle подразумевает изменение только одной стороны одной функцией.

        Кто сказал?


        1. BOM
          27.09.2019 22:55

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


          1. Kanut
            27.09.2019 23:06

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


            1. BOM
              28.09.2019 00:23

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


              1. Kanut
                28.09.2019 08:18

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


                1. BOM
                  28.09.2019 10:06

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


                  1. Kanut
                    28.09.2019 11:08

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


                    1. qw1
                      28.09.2019 15:20

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


                      1. Kanut
                        28.09.2019 15:37

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


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


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


                        1. qw1
                          28.09.2019 15:56

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


                          1. Kanut
                            28.09.2019 16:11

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


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


                          1. VolCh
                            28.09.2019 18:07
                            +1

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


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


                  1. VolCh
                    28.09.2019 18:01

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


                    1. sshikov
                      28.09.2019 19:11
                      +1

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


                      1. VolCh
                        28.09.2019 21:34

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


                        1. sshikov
                          28.09.2019 21:41

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


                1. piton_nsk
                  01.10.2019 13:11

                  А как может быть такое, что изменяя ширину, площадь не меняется?


                  1. Kanut
                    01.10.2019 14:33

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


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


  1. 0xd34df00d
    27.09.2019 17:43

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

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


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

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


    1. sshikov
      27.09.2019 19:14
      +1

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

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


      1. VolCh
        28.09.2019 18:16

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


        1. sshikov
          28.09.2019 19:03

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

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

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


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

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

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


          1. VolCh
            28.09.2019 21:37

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


            1. sshikov
              28.09.2019 21:50

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


  1. Antervis
    28.09.2019 02:09

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

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

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

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


  1. clayly
    28.09.2019 10:17

    1. Rectangle { setHeigth, setArea; getHeight, getArea, getWidth }
    2. Square: Rectangle
    3. Profit


  1. Vlad800
    28.09.2019 10:17

    Я бы зашел с другой стороны…
    В природе есть два типа полезных данных — статические и динамические. Пример первых — фотография, пример вторых — видео. Для статических данных (СД) важно их хранение. Например, для фото надо хранить: координаты пикселя, его яркость, его RGB. А для динамических данных (ДД) ничего хранить не надо, единственная задача — это успевать их правильно обрабатывать и выдавать «в никуда», где они могут бесследно и безболезненно исчезать. Пример — видео и смотрящий его человек. Для работы с СД придумали ООП, а для работы с ДД — ФП.
    И всё работало более менее нормально, пока не возникла необходимость взаимодействия одновременно с двумя типами данных. И тут пошло поехало…

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

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

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


    1. VolCh
      28.09.2019 21:23

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


      1. Vlad800
        29.09.2019 01:44

        Поведение/характер должны быть, конечно


  1. BD9
    28.09.2019 14:32
    +1

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


    1. VolCh
      28.09.2019 21:25

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


  1. easimonenko
    29.09.2019 14:24
    +1

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