Привет, Хабр!
Сегодня вас ждет переводная публикация, в некоторой степени отражающая наши поиски, связанные с новыми книгами об ООП и ФП. Просим поучаствовать в голосовании.
Мертва ли парадигма ООП? Можно ли сказать, что за функциональным программированием будущее? Кажется, что во многих статьях пишут именно об этом. Склонен с такой точкой зрения не согласиться. Давайте обсудим!
Раз в несколько месяцев мне попадается пост в каком-нибудь блоге, где автор выдвигает, казалось бы, обоснованные претензии к объектно-ориентированному программированию, после чего объявляет ООП пережитком прошлого, а все мы должны переключиться на функциональное программирование.
Ранее я уже писал о том, что ООП и ФП не противоречат друг другу. Более того, мне удалось весьма успешно сочетать их.
Почему же у авторов этих статей возникает такая масса проблем с ООП, и почему ФП кажется им настолько очевидной альтернативой?
Когда нам преподают ООП, обычно подчеркивают, что оно зиждется на четырех принципах: инкапсуляция, наследование, абстракция, полиморфизм. Именно эти четыре принципа обычно критикуются в статьях, где авторы рассуждают об упадке ООП.
Однако, ООП, как и ФП – это инструмент. Для решения задач. Его можно употреблять, им же можно злоупотреблять. Например, создавая неверную абстракцию, вы злоупотребляете ООП.
Так, класс
Давайте подробнее обсудим наследование. Вероятно, вам вспоминаются хрестоматийные примеры с красивыми иерархиями унаследованных классов, и все эти структуры работают на решение задачи. Однако, на практике наследование применяется не так часто как композиция.
Рассмотрим пример. Допустим, у нас есть очень простой класс, контроллер в веб-приложении. В большинстве современных фреймворков предполагается, что вы будете работать с ним так:
Предполагается, что таким образом вам будет проще выполнять вызовы вроде
Как указывается во многих статьях на эту тему, здесь возникает ряд ощутимых проблем. Любая внутренняя функция в базовом классе фактически превращается в API. Она больше не может меняться. Любые защищенные переменные базового контроллера теперь будут в большей или меньшей степени относиться к API.
В этом ничего не стоит запутаться. А если бы мы избрали подход с композицией и внедрением зависимостей, то получилось бы так:
Вот видите, вы больше не зависите от какого-то туманного
Вторая зачастую критикуемая черта ООП – инкапсуляция. На литературном языке смысл инкапсуляции формулируется так: данные и функционал поставляются вместе, а внутреннее состояние класса скрывается от внешнего мира.
Эта возможность, опять же, допускает употребление и злоупотребление. Основной пример злоупотребления в данном случае – дырявое состояние (leaky state).
Условно говоря, предположим, что в классе
Здесь в большинстве ООП-ориентированных языков произойдет следующее: переменная items будет возвращаться по ссылке. Поэтому далее можно сделать так:
Таким образом мы фактически очистим список элементов в корзине, а ShoppingCart об этом даже не узнает. Однако, если как следует присмотреться к этому примеру, то становится понятно, что проблема отнюдь не в принципе инкапсуляции. Здесь как раз нарушается этот принцип, поскольку из класса
В данном конкретном примере автор класса
Неопытные программисты часто нарушают принцип инкапсуляции и другим образом: вводят состояние там, где в нем нет нужды. Такие неопытные программисты часто используют переменные приватного класса для передачи данных от одной функции к другой в пределах одного и того же класса, тогда как правильнее было бы использовать объекты передачи данных (Data Transfer Objects), чтобы передавать иной функции сложную структуру. В результате таких ошибок код излишне усложняется, что может приводить к возникновению багов.
В целом, хорошо бы вообще обходиться без состояния – хранить в классах изменяемые данные, когда только возможно. Делая так, нужно обеспечить надежную инкапсуляцию и убедиться, что нигде не возникает утечек.
Абстракцию, опять же, понимают во многом неправильно. Ни в коем случае не следует нашпиговывать код абстрактными классами и делать в нем глубокие иерархии.
Если вы так делаете без веской на то причины, то просто ищете неприятностей на свою голову. Не важно, как именно делается абстракция – как абстрактный класс или как интерфейс; в любом случае, в коде появится лишняя сложность. Эта сложность должна быть оправданной.
Проще говоря, интерфейс можно создавать лишь при условии, если вы готовы потратить время и документировать поведение, которое ожидается от реализующего его класса. Да, вы меня верно прочли. Мало просто составить список функций, которые потребуется реализовать – также опишите, как (в идеале) они должны работать.
Наконец, давайте поговорим о полиморфизме. Он предполагает, что один класс может реализовывать множество поведений. Плохой хрестоматийный пример — написать, что
Говоря о полиморфизме, следует держать в уме поведения, а не код. Хороший пример — класс
Итак, если написать
Итак, когда мы повторили четыре основных принципа ООП, давайте задумаемся, в чем же особенность функционального программирования, и почему с его помощью не решить всех проблем в вашем коде?
С точки зрения многих адептов ФП, классы – это кощунство, и код должен быть представлен в виде функций. В зависимости от языка, данные могут передаваться от функции к функции при помощи примитивных типов, либо в виде того или иного структурированного множества данных (массивы, словари, т.д.).
Кроме того, большинство функций не должны иметь побочных эффектов. Иными словами, они не должны изменять данные в каком-либо непредвиденном месте в фоновом режиме, а работать только с входными параметрами и продуцировать вывод.
Такой подход отделяет данные от функционала – на первый взгляд, этим ФП кардинально отличается от ООП. ФП упирает на то, что таким образом код сохраняет простоту. Вы хотите что-то сделать, пишете функцию для этой цели – вот и все.
Проблемы начинаются, когда одни функции должны опираться на другие. Когда функция A вызывает функцию B, а функция B вызывает еще пять-шесть функций, а в самом конце обнаруживается функция заполнения нулями, которая может сломаться – вот тут-то вам не позавидуешь.
Большинство программистов, считающих себя сторонниками ФП, любят ФП за его простоту и не считают такие проблемы серьезными. Это достаточно честно, если ваша задача – просто сдать код и больше никогда о нем не задумываться. Если же вы хотите наработать базу кода, удобную в поддержке, то лучше придерживаться принципов чистого кода, в частности, применять инверсию зависимостей, при которой ФП на практике также значительно усложняется.
ООП и ФП – это инструменты. В конечном счете, не так важно, какой парадигмой программирования вы пользуетесь. Проблемы, описываемые в большинстве статей на эту тему, касаются организации кода.
На мой взгляд, гораздо важнее макроструктура приложения. Какие в нем модули? Как они обмениваются информацией друг с другом? Какие структуры данных у вас наиболее распространены? Как они документированы? Какие объекты наиболее важны с точки зрения бизнес-логики?
Все эти вопросы никак не связаны с используемой парадигмой программирования, на уровне такой парадигмы их даже не решить. Хороший программист изучает парадигму, чтобы освоить предлагаемые ею инструменты, а затем выбирает, какие из них лучше всего подходят для решения поставленной задачи.
Сегодня вас ждет переводная публикация, в некоторой степени отражающая наши поиски, связанные с новыми книгами об ООП и ФП. Просим поучаствовать в голосовании.
Мертва ли парадигма ООП? Можно ли сказать, что за функциональным программированием будущее? Кажется, что во многих статьях пишут именно об этом. Склонен с такой точкой зрения не согласиться. Давайте обсудим!
Раз в несколько месяцев мне попадается пост в каком-нибудь блоге, где автор выдвигает, казалось бы, обоснованные претензии к объектно-ориентированному программированию, после чего объявляет ООП пережитком прошлого, а все мы должны переключиться на функциональное программирование.
Ранее я уже писал о том, что ООП и ФП не противоречат друг другу. Более того, мне удалось весьма успешно сочетать их.
Почему же у авторов этих статей возникает такая масса проблем с ООП, и почему ФП кажется им настолько очевидной альтернативой?
Как обучают ООП
Когда нам преподают ООП, обычно подчеркивают, что оно зиждется на четырех принципах: инкапсуляция, наследование, абстракция, полиморфизм. Именно эти четыре принципа обычно критикуются в статьях, где авторы рассуждают об упадке ООП.
Однако, ООП, как и ФП – это инструмент. Для решения задач. Его можно употреблять, им же можно злоупотреблять. Например, создавая неверную абстракцию, вы злоупотребляете ООП.
Так, класс
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 вызывает еще пять-шесть функций, а в самом конце обнаруживается функция заполнения нулями, которая может сломаться – вот тут-то вам не позавидуешь.
Большинство программистов, считающих себя сторонниками ФП, любят ФП за его простоту и не считают такие проблемы серьезными. Это достаточно честно, если ваша задача – просто сдать код и больше никогда о нем не задумываться. Если же вы хотите наработать базу кода, удобную в поддержке, то лучше придерживаться принципов чистого кода, в частности, применять инверсию зависимостей, при которой ФП на практике также значительно усложняется.
ООП или ФП?
ООП и ФП – это инструменты. В конечном счете, не так важно, какой парадигмой программирования вы пользуетесь. Проблемы, описываемые в большинстве статей на эту тему, касаются организации кода.
На мой взгляд, гораздо важнее макроструктура приложения. Какие в нем модули? Как они обмениваются информацией друг с другом? Какие структуры данных у вас наиболее распространены? Как они документированы? Какие объекты наиболее важны с точки зрения бизнес-логики?
Все эти вопросы никак не связаны с используемой парадигмой программирования, на уровне такой парадигмы их даже не решить. Хороший программист изучает парадигму, чтобы освоить предлагаемые ею инструменты, а затем выбирает, какие из них лучше всего подходят для решения поставленной задачи.
Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Книги о ФП и ООП
47.6% Требуется базовая книга о принципах ФП99
34.13% Требуется базовая книга о принципах ООП71
46.63% Статья не впечатлила97
Проголосовали 208 пользователей. Воздержались 68 пользователей.