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

Мертва ли парадигма ООП? Можно ли сказать, что за функциональным программированием будущее? Кажется, что во многих статьях пишут именно об этом. Склонен с такой точкой зрения не согласиться. Давайте обсудим!
Раз в несколько месяцев мне попадается пост в каком-нибудь блоге, где автор выдвигает, казалось бы, обоснованные претензии к объектно-ориентированному программированию, после чего объявляет ООП пережитком прошлого, а все мы должны переключиться на функциональное программирование.
Ранее я уже писал о том, что ООП и ФП не противоречат друг другу. Более того, мне удалось весьма успешно сочетать их.
Почему же у авторов этих статей возникает такая масса проблем с ООП, и почему ФП кажется им настолько очевидной альтернативой?
Как обучают ООП
Когда нам преподают ООП, обычно подчеркивают, что оно зиждется на четырех принципах: инкапсуляция, наследование, абстракция, полиморфизм. Именно эти четыре принципа обычно критикуются в статьях, где авторы рассуждают об упадке ООП.
Однако, ООП, как и ФП – это инструмент. Для решения задач. Его можно употреблять, им же можно злоупотреблять. Например, создавая неверную абстракцию, вы злоупотребляете ООП.
Так, класс
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)
disgusting
27.09.2019 11:59Так, класс Square никогда не должен наследовать класс Rectangle.
а что мешает? раз математически / логически квадрат — частный случай прямоугольника, программирование продиктовано логикой.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); } }
Но если в списке прямоугольников будут и квадраты, то поведение становится неправильным.alexxisr
27.09.2019 12:56А если сделать квадрат иммутабельным, то он сможет заменить иммутабельный прямоугольник?
andrikeev
27.09.2019 15:13Иммутабельные объекты это уже больше про ФП, тут я привел пример классического объяснения проблемы, возникающей при неправильном наследовании.
VolCh
28.09.2019 17:46Мутабельными могут быть, например, координаты прямоугольник, а ширина и высота иммутабельными. Нельзя говорить что-то вроде "нельзя на следовать Б от А" не объявляя ожидаемого поведения А. Особенно, не добавляя "не нарушая принципа постановки Дисков"
hd_keeper
27.09.2019 15:04Можно для квадрата переопределить и setWidth, и setHeight.
Например, так, чтобы каждый изменял сразу обе стороны.
Тогда квадрат останется квадратом.andrikeev
27.09.2019 15:11Я кажется так и написал, у квадрата переопределены оба метода и увеличатся в два раза и ширина и высота, а значит площадь увеличится в четыре раза, а не в два. При этом он останется квадратом, но контракт метода будет нарушен.
pvsur
01.10.2019 10:43Это просто означает что ваша функция doubleSquare неверна. И это не проблема квадрата :) Это проблема функции, которая могла бы сначала узнать тип фигуры и применять соответствующий метод.
qw1
01.10.2019 11:34Тип фигуры функции уже передан — это Rectangle.
pvsur
01.10.2019 12:32Да, и эта функция по хорошему, обязана вызвать соотвествующий виртуальный метод объекта doubleSquare, которые для обычного рестангле и для квадрата будут каждый своим.
Понамешают функциональщины с ООП, потом сами разобраться не могут и кричат, что ООП профнепригодно :)
Функция doubleSquare в том виде как представлена, нарушает принцип инкапсуляции ООП (все изменения объекта только средствами самого объекта). Программист — ССЗБ в этом случае, ООП ни при чем.
BOM
27.09.2019 18:35А теперь предположим есть отверстие с прямоугольными углами, которое принимает Rectangle, и чтобы наш класс туда поместился необходимо задать ему нужную ширину и высоту и тогда все туда пролезет. Square же попадает под условие сигнатуры, но когда мы попытаемся присвоить ему setWidth(1) и setHeight(2) (что является достаточным условием для метода отверстия), он почему-то туда все равно не пролезет. Налицо нарушение принципа Барбары Лисков. Вызывая один из методов setWidth или setHeight у класса Rectangle, мы ожидаем, что изменим каждую из сторон независимо, но тут внезапно врывается Square, который меняет стороны имплицитно.
slonopotamus
27.09.2019 22:08мы ожидаем, что изменим каждую из сторон независимо
Почему вы этого ожидаете? Контракт к setWidth/setHeight вам этого не обещал.
BOM
27.09.2019 22:51Что вы подразумеваете под контрактом? Можем создать класс Weirdtangle, который будет иметь setWidth/setHeight, но имплицитно второе свойство будет устанавливать в ноль. Мы нарушили контракт ожидаемого поведения Rectangle? Сигнатуры в норме, проект собирается, вот только вся логика написанная для работы с Rectangle с этим классом летит как фанера.
slonopotamus
27.09.2019 22:58Мы нарушили контракт ожидаемого поведения Rectangle?
Без понятия, я не знаю что мне обещает документация к Rectangle. Вы на основании чего-то считаете что после вызова setWidth метод getHeight будет возвращать то же, что и до вызова. Но на основании чего вы так решили?
0xd34df00d
27.09.2019 23:35На том, что я просил менять ширину, а не высоту.
В частности, удобно ожидать, что если я поменял ширину, то площадь и периметр поменялись только соответственно ширине (в частности, например, что увеличение ширины вдвое меняет площадь вдвое, а не вчетверо или в любое другое произвольное число раз).
Ломать этот контракт для сохранения мутабельности как-то странно. Если с таким контрактом смириться, то завтра у вас ширина начнет ещё и от цвета фигуры зависеть.
vintage
29.09.2019 05:20+1Если контракт фигуры заключается в том, что визуальный размер при изменении цвета остаётся неизменным, то да, при изменении цвета будут меняться и размеры, и даже координаты.
BOM
28.09.2019 00:09Без понятия, я не знаю что мне обещает документация к Rectangle.
Учебник геометрии за пятый класс достаточно точно дает определение тому, что есть прямоугольник. Обсуждается как раз таки классический случай из геометрии. Контекст обсуждения предельно ясен и прозрачен. Вы можете, конечно, его пытаться искусственно усложнить в угоду своей позиции, но боюсь на этом дальнейшее обсуждение скатится к софистике.
Вы на основании чего-то считаете что после вызова setWidth метод getHeight будет возвращать то же, что и до вызова.
Не знаю даже. Может быть вот en.wikipedia.org/wiki/Rectangle
На основании того, что стороны прямоугольника являются не связанными друг с другом величинами, но в совокупности придающими прямоугольнику дополнительные свойства. Например, периметр, выведение которого производится общеизвестным методом.vintage
29.09.2019 05:40А может быть и: https://en.wikipedia.org/wiki/Golden_rectangle
Вы зря в обсуждении программной модели пытаетесь сослаться на математические абстракции, которые даже в самой математике имеют множество разных интерпретаций. Классический пример: https://en.wikipedia.org/wiki/0#Mathematics
TheRikipm
28.09.2019 10:17Потому-что контракт к прямоугольнику это обещал.
Наследники должны расширять поведение родителей, а не замещать.VolCh
28.09.2019 17:51Если обещал. Например если есть тесты, которые проверяют, что ширина после изменения длины не изменилась. С другой стороны, должны быть тесты, которые проверяют, что длина квадрата изменяется синхронно с шириной, чтобы квадрат не прошёл тесты такого прямоугольника
TheRikipm
28.09.2019 19:10Если обещал.
Ну собственно само названиеsetWidth
это подразумевает
Если квадрат является потомком прямоугольника то он обязан проходить все тесты, которые проходит прямоугольник. Гуглите LSPqw1
28.09.2019 19:15Если наследник проходит абсолютно все тесты предка, то его поведение не меняется. Но в этом случае, какой смысл создавать новый класс?
TheRikipm
28.09.2019 19:29+2Если наследник проходит абсолютно все тесты предка, то его поведение не меняется.
Неверно. Его поведение может меняться без нарушения прохождения тестов. Например могут появиться новые методы или свойства. Могут появиться опциональные аргументы в старых методах.0xd34df00d
28.09.2019 20:47+1Неверно. Его поведение может меняться без нарушения прохождения тестов.
Мне тут, кстати, подумалось, что LSP не выполняется вообще никогда, если, например, у вас есть рантайм-рефлексия. Он же сформулирован в терминах любого предиката, а с рантайм-рефлексией вы, очевидно, можете отличить класс от его наследника (даже если наследник не добавляет вообще ничего).
VolCh
28.09.2019 21:31Зависит от уровня рефлекси. Какой-то user instanceof User обычно возвращает true как для самого User, так и для его наследников. Главное не лезть в имя класса и не проверять отсутствие членов.
TheRikipm
28.09.2019 23:25Да, забавный артефакт.
Но думаю тут можно простить его нарушение, все-таки instanceof чаще всего возвращает true для подклассов, а если разработчик городит свои костыли, то он ССЗБ.
vintage
29.09.2019 05:49+1Да, с setWidth и setHeight всё просто — объект может обеспечить свои инварианты изменя связанные состояния. А вот с методом прямоугольника
setDimensions( width , height )
уже сложнее, так как реализовать этот контракт квадрат в принципе не сможет.
На всякий случай — если он начнёт кидать исключение или тупо игнорировать в случае неравных параметров, то это тоже нарушение контракта.
myemuk
27.09.2019 21:52Обычно на практике в таких случаях методы setWidth и setHeight перезаписываются таким образом, что фигура все-таки остается квадратом. Если используется setWidth, то он вызовет setHeight, и наоборот.
Что же касается функции, которая увеличивает площадь в 2 раза, первое, что мне пришло в голову — не ваш пример, а увеличение каждой из сторон вsqrt(2)
раза, тогда одна и та же функция подойдет как для прямоугольника, так и для квадрата. С одной стороны увеличить одну из сторон проще, но тогда меняются пропорции прямоугольника, что в некоторых случаях неприемлемо.
В истории квадрата и прямоугольника функции нахождения периметра и площади одинаковы в общем случае, этот факт и побуждает приводить этот пример, когда речь заходит про ООП.
А в целом вы говорите верно, ООП и ФП — всего лишь инструменты для достижения поставленной задачи. Вот тут хотелось бы увидеть больше конкретики и примеров что в каких случаях больше использовать. Дарю идею дляхоливаравашей следующей статьи.
P.S. Мне в JS поначалу сильно не хватало трейтов из php для организации поведений и «мультинаследования» в ООП. Но со временем я научился думать по-другому.
P.P.S. В своих проектах использую оба подхода, но в крупных чаще ООП.Deosis
30.09.2019 14:06Что же касается функции, которая увеличивает площадь в 2 раза, первое, что мне пришло в голову — не ваш пример, а увеличение каждой из сторон в sqrt(2) раза
Это будет работать, пока компилятор не решит поменять порядок чтения:
- прочитать длину, увеличить длину, записать длину,
- прочитать ширину, увеличить ширину, записать ширину.
В таком варианте квадрат все равно увеличится в 4 раза.
VolCh
28.09.2019 17:43Этот классический пример рассматривает только один из возможных контекстов использования квадратов и прямоугольников из, наверное, бесконечного множества.
Antervis
30.09.2019 17:04Это же классический пример из литературы: у прямоугольника есть методы setWidth и setHeight, которые поидее работают независимо друг от друга. Но когда вы наследуете от него квардрат, вам нужно сделать так, чтобы при изменении ширины/высоты он оставался квдратом, то есть менялась и вторая величина. Кажется, что в этом нет ничего страшного, но нарушается LSP.
а наследуйте прямоугольник от квадрата да и всё )
cudu
27.09.2019 12:59Таки квадрат — это четырехугольник. И наследует полностью только его поведение, тогда как поведение прямоугольника и квадрата — различаются между собой.Свойства больше разных у них, чем одинаковых, т.е. и в самом деле этот хрестоматийный пример предполагает, что нет смысла наследоваться именно от прямоугольника(в википедии сказано, что в квадрат — частный случай в том числе и ромба, почем ромб не сделать родителем?)
VolCh
28.09.2019 17:52Если подходить к задаче моделирования геометрии, то там они в принципе иммутабельны.
bgnx
27.09.2019 14:24+1Логически квадрат это не частный случай прямоугольника а вычисляемое свойство, то есть сущность прямоугольника которая позволяет менять ширину и длину время от времени может становится квадратом при совпадении длины и ширины
class Square { isRectangle() { return this.getWidth() == this.getHeight() } }
disgusting
27.09.2019 14:33тогда, вероятно, моё утверждение стоит исправить на «квадрат можно наследовать от четырёхугольника».
VolCh
28.09.2019 17:58Нет никакого "логически является" пока нет конкретной задачи или, хотя бы, контекста. Для графического редактора квадрат может вообще с прямоугольником быть не связанным, а для геометрических задач сама операция изменения размеров не применима обычно.
klevercat
27.09.2019 20:32struct 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 { //... }
vintage
28.09.2019 17:42+1С точки зрения операций чтения квадрат является подтипом прямоугольника. А вот с точки зрения операций записи уже прямоугольник является подтипом квадрата. В системе, где возможны и чтение и запись, очевидно, ни один из них не сможет быть подтипом другого.
Kanut
27.09.2019 12:32Статья на мой взгляд какая-то немного странная.
С одной стороны лично я как-то не вижу чтобы прямо куча людей считали что "парадигма ООП умерла" или что она теперь не особо актуальна или ещё что-то в этом роде.
С другой стороны если такие люди и есть, то этой статьёй и приведёнными в ней примерами таких людей вряд ли убедишь в обратном.
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)
Aldrog
27.09.2019 17:55А потом какой-нибудь код, не знающий разницы между квадратом и прямоугольником меняет созданному Square width.
BOM
27.09.2019 18:40Контракт Rectangle подразумевает изменение только одной стороны одной функцией. Если будет логика, которая полагается на независимое изменение сторон, то Square автоматически становится невалидным для этой функции.
slonopotamus
27.09.2019 22:17Контракт Rectangle подразумевает изменение только одной стороны одной функцией.
Кто сказал?
BOM
27.09.2019 22:55Название метода setWidth, в котором нет слова Height или Both или какого-нибудь DeleteThisProjectToHell.
Kanut
27.09.2019 23:06У вас контракты описываются исключительно через имя метода?
BOM
28.09.2019 00:23В числе прочих. Naming convention никто не отменял.
Kanut
28.09.2019 08:18Не расскажете тогда какой у вас в данном случае Naming convention что он позволяет понять что метод setWidth меняет ещё скажем и площадь фигуры?
BOM
28.09.2019 10:06Площадь это вычисляемое свойство. Следует из контракта самого класса.
Kanut
28.09.2019 11:08А контракт класса тоже идёт по naming convention? А что мешает кому-то в контракте класса прописать что высота у данного конкретного класса тоже "вычисляется" и зависит от ширины?
qw1
28.09.2019 15:20Для этого придётся сделать изменения в базовом классе Rectangle. Это не всегда возможно — либо его кодом владеет другая команда, либо он уже используется в 100500 функциях, и переименование setWidth в setWidthAndPossiblyHeightChanges потребует рефакторинга их всех.
Kanut
28.09.2019 15:37Дело не в этом. Ясное дело что наш пример примитивен и достаточно просто понять как он работает. И как он по идее должен работать мы вроде себе тоже представляем.
Но это не мешает кому-то написать свою собственную реализацию этого класса, в которой этот класс будет для него интуитивен и понятен, а для нас нет. Но при этом всё ещё будет нормально описывать геометрические фигуры.
А если взять не такой банальный пример, а какую-нибудь сложную бизнес-логику из реальных процессов, то там ещё "опаснее" полагаться на интуицию. И только на naming conventions тоже опасно полагаться.
qw1
28.09.2019 15:56В идеале, naming conventions должны быть подмножеством контракта. То есть, все требования в них невозможно запихать, но наименования не должны противоречить контракту.
Kanut
28.09.2019 16:11И для того чтобы определить противоречат наименования или нет надо иметь на руках этот самый контракт.
И даже в примере с квадратами/прямоугольниками этот "контракт" похоже немного разный у разных людей.
VolCh
28.09.2019 18:07+1Контракт конвеншина set об ычно подразумевает исключительно что get вернёт то же самое. Ни разу не видел тестов, проверяющих, что результаты других get не изменились после вызова конкретного set.
Более того, как основным аргументом введения сеттеров вместо публичных свойств является соблюдение инвариантов класса посредством сколь угодно сложной логики
VolCh
28.09.2019 18:01Вы постулируете, что площадь вычисляемое свойство в комментарии Н-го уровня. Не спортивно.
sshikov
28.09.2019 19:11+1Вычисляемая площадь — это одно из решений. Совершенно ничем не хуже того, когда setArea это примитивный сеттер, и можно его установить любым, совершенно независимо от ширины и высоты.
VolCh
28.09.2019 21:34Все три сеттера могут быть непримитивными и выставлять значения и других приватных свойств, все три геттера могут быть вычислимыми. Например, приватные свойства могут отражать размеры прямоугольник в полярной системе координат, типа радиуса описанной окружности и угла диагонали.
sshikov
28.09.2019 21:41Ну так и я про тоже. Вариантов, что именно является вычисляемым, может быть полно. Нет тут одного контракта, единственно верного. Вариант, когда вы площадь задаете, а размеры вычисляются соответствнно, тоже вполне можно вообразить.
piton_nsk
01.10.2019 13:11А как может быть такое, что изменяя ширину, площадь не меняется?
Kanut
01.10.2019 14:33Ну мы же говорим о различных вариантах определения классов в ООП, а не о математике как таковой. И теоретически никто не запрещает вам сделать класс у которого площадь это будет просто обычный проперти. И если вы меняете ширину, то вам надо самому вычислять новую площадь и тоже её менять "вручную".
Это в общем случае не особо логично, но возможно.
0xd34df00d
27.09.2019 17:43Проблемы начинаются, когда одни функции должны опираться на другие. Когда функция A вызывает функцию B, а функция B вызывает еще пять-шесть функций, а в самом конце обнаруживается функция заполнения нулями, которая может сломаться – вот тут-то вам не позавидуешь.
Не понял, а специфичные для ФП проблемы-то тут где?
в частности, применять инверсию зависимостей, при которой ФП на практике также значительно усложняется.
Просто берёшь и заворачиваешься в монаду. Хорошо, явно, композабельно, проверяемо компилятором.
sshikov
27.09.2019 19:14+1>Не понял, а специфичные для ФП проблемы-то тут где?
Да нигде, что вы. Вы на ссылку-то посмотрите — она ведет, для начала, на обсуждение функции left_pad в NPM. С каких-это пор Javascript (node) и его NPM стали хоть сколько-то репрезентативным примером применения ФП?
>Просто берёшь и заворачиваешься в монаду.
Ну да. Правильнее было бы сказать как-то так: инверсия зависимостей в ФП реализуется и действует иногда совсем не так, как это принято в ООП, что может вводить в заблуждение неопытных разработчиков (я бы сюда включил и автора оригинала). Это было бы правильнее.VolCh
28.09.2019 18:16Почему совсем не так? Вы не путаете инверсию зависимостей и иньекцией зависимостей?
sshikov
28.09.2019 19:03>Вы не путаете инверсию зависимостей и иньекцией зависимостей?
Не, не путаю. В ООП инверсия зависимостей в том, что зависеть вы должны от интерфейсов, а не от реализаций. В ФП это выглядит иначе. В принципе, мое замечание касается обоих: как инверсии, так и иньекции.
Автор оригинала не удосужился пояснить, что именно он имел в виду, а переводчик усугубил:
Если же вы хотите наработать базу кода, удобную в поддержке, то лучше придерживаться принципов чистого кода, в частности, применять инверсию зависимостей, при которой ФП на практике также значительно усложняется.
и почему это оно вдруг усложняется. При этом ссылка в данном абзаце в переводе ведет совсем не туда, как в оригинале:
Оригинал: pasztor.at/blog/clean-code-dependencies
Перевод: www.piter.com/collection/all/product/spring-vse-patterny-proektirovaniya
Ну т.е. автор ссылается на себя, а переводчик — рекламирует свои переводы.VolCh
28.09.2019 21:37В ФП по большому счёту это выглядит так же, по-моему. Просто вместо классового интерфейса или абстрактного класса, ФП функция будет зависеть от функционального типа, сигнатуры "колбэка"
sshikov
28.09.2019 21:50Я не помню, где бы в ФП имели место абстрактные классы. Ну то есть с моей точки зрения, все несколько проще, чем бывает в ООП. Но однако же, если мы начнем про композицию программы из частей — то она все-таки будет устроена несколько иначе.
Antervis
28.09.2019 02:09когда наследование заменяется композицией, можно получить проблему первого типа из пункта про инкапсуляцию, а обходится она обычно довольно-таки большим объемом кода.
В целом, хорошо бы вообще обходиться без состояния – хранить в классах изменяемые данные, когда только возможно
вы наверно имели в виду «неизменяемые» данные?
При правильном использовании ООП (да и вообще это одна из главных фишек подхода) объекты в программе всегда находятся в одном из множества корректных состояний. Согласен, энтропию объекта стоит уменьшать. Однако в большинстве случаев иммутабельность для ООП — оверкилл.
clayly
28.09.2019 10:17- Rectangle { setHeigth, setArea; getHeight, getArea, getWidth }
- Square: Rectangle
- Profit
Vlad800
28.09.2019 10:17Я бы зашел с другой стороны…
В природе есть два типа полезных данных — статические и динамические. Пример первых — фотография, пример вторых — видео. Для статических данных (СД) важно их хранение. Например, для фото надо хранить: координаты пикселя, его яркость, его RGB. А для динамических данных (ДД) ничего хранить не надо, единственная задача — это успевать их правильно обрабатывать и выдавать «в никуда», где они могут бесследно и безболезненно исчезать. Пример — видео и смотрящий его человек. Для работы с СД придумали ООП, а для работы с ДД — ФП.
И всё работало более менее нормально, пока не возникла необходимость взаимодействия одновременно с двумя типами данных. И тут пошло поехало…
В чем я вижу проблемы ООП.
а) Идея, что «всё есть объект» (слава Богу, встречается редко). Так как объект — это не просто данные, а нечто, обладающее своим поведением/характером (привет инкапсуляция).
б) Идея, что надо объединять данные и связанные с ним методы в одну сущность. Фактически современный класс — это монолитная программа допроцедурного программирования. А нам достаточно, чтобы объект хранил переменную под присмотром своего «характера». А возможные операции с этими данными надо выносить за пределы класса как такового (привет функциям из ФП).
в) бесконечные попытки выдать наследование и полиморфизм как признаки ООП. Ведь первое — это просто переиспользование кода, а второе — на самом деле ближе к ФП (так как обработка данных).
Проблема ФП.
а) попытка хранить состояние (то есть СД) способами, разработанными для операций с ДД (привет монады).
Мой идеальный мир…
Объекты хранят только состояния, а за пределами объектов — чистое ФП.
BD9
28.09.2019 14:32+1ООП и ФП — уже довольно старые технологии. Т.ч. вместо неких «новых» книг, которые создаются ухудшением старых, предпочёл бы старые.
Хорошие книги написал Бертран Мейер, но они у него объёмные и сложные, т.ч. могут быть трудности с их продажей.
По поводу жалоб на ООП: почти везде эта технология воплощена урезанно и криво, и жалобы больше относятся к языкам программирования, чем к ООП.VolCh
28.09.2019 21:25Они не столько сложные, сколько оторванные от реалий условно современной разработки
easimonenko
29.09.2019 14:24+1Мочи мочало, начинай сначала. Автор статьи неграмотен: ни что из перечисленного не является неотъемлемой чертой ООП, ни наследование, ни инкапсуляция, ни полиморфизм, ни абстракция. И всё это есть в чисто функциональном Haskell. Так что, объявим Haskell объектно-ориентированным?
E_STRICT
BOM
Повторение само по себе не является чем-то плохим.
VolCh
Оно является чем-то плохим, если надо синхронизировать изменения во всех местах.
Представьте, что сигнатура рендерера изменилась, например, в качестве данных шаблона стал ожидаться мэп, а не список туплов. В случае абстрактного контроллера достаточно изменить один метод в нём…