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

За последние несколько лет мне неоднократно доводилось программировать в паре с людьми, изучающими Функциональное Программирование, которые были предвзято настроены по отношению к ООП. Обычно это выражалось в формe утверждений типа: “Ну это слишком похоже на что-то объектное.”
Я думаю это происходит из убеждения, что ФП и ООП взаимно исключают друг друга. Многие похоже думают, что если программа функциональная, то она не объектно ориентированная. Полагаю, формирование такого мнения — логичное следствие изучения чего-то нового.
Когда мы берёмся за новую технику, мы часто начинаем избегать старых техник, которые использовали раньше. Это естественно, потому что мы верим, что новая техника “лучше” и следовательно старая техника наверное “хуже”.
В этом посте я обосную мнение, что хотя ООП и ФП ортогональны, это не взаимно исключающие понятия. Что хорошая функциональная программа может (и должна) быть объектно ориентированной. И что хорошая объектно ориентированная программа может (и должна) быть функциональной. Но для того, чтобы это сделать, нам придётся определиться с терминами.
Что такое ООП?
Я подойду к вопросу с редукционистских позиций. Есть много правильных определений ООП которые покрывают множество концепций, принципов, техник, паттернов и философий. Я намерен проигнорировать их и сосредоточиться на самой соли. Редукционизм тут нужен из-за того, что всё это богатство возможностей, окружающее ООП на самом деле не является чем-то специфичным для ООП; это просто часть богатства возможностей встречающихся в разработке программного обеспечения в целом. Тут я сосредоточусь на части ООП, которая является определяющей и неустранимой.
Посмотрите на два выражения:
Никакой семантической разницы явно нет. Вся разница целиком и полностью в синтаксисе. Но одно выглядит процедурным, а другое объектно ориентированным. Это потому что мы привыкли к тому, что для выражения 2. неявно подразумевается особая семантика поведения, которой нет у выражения 1. Эта особая семантика поведения — полиморфизм.
Когда мы видим выражение 1. мы видим функцию f, которая вызывается в которую передаётся объект o. При этом подразумевается, что есть только одна функция с именем f, и не факт, что она является членом стандартной когорты функций, окружающих o.
С другой стороны, когда мы видим выражение 2. мы видим объект с именем o которому посылают сообщение с именем f. Мы ожидаем, что могут быть другие виды объектов, котоые принимают сообщение f и поэтому мы не знаем, какого конкретно поведения ожидать от f после вызова. Поведение зависит от типа o. то есть f — полиморфен.
Вот этот факт, что мы ожидаем от методов полиморфного поведения — суть объектно ориентированного программирования. Это редукционистское определение и это свойство неустранимо из ООП. ООП без полиморфизма это не ООП. Все другие свойства ООП, такие как инкапсуляция данных и методы привязанные к этим данным и даже наследование имеют больше отношения к выражению 1. чем к выражению 2.
Программисты, использующие Си и Паскаль (и до некоторой степени даже Фортран и Кобол) всегда создавали системы инкапсулированных функций и структур. Чтобы создать такие структуры даже не нужен объектно ориентированный язык программирования. Инкапсуляция и даже простое наследование в таких языках очевидны и естественны. (В Си и Паскале более естественно, чем в других)
Поэтому то, что действительно отличает ООП программы от не ООП программ это полиморфизм.
Возможно вы захотите возразить, что полифорфизм можно сделать просто используя внутри f switch или длинные цепочки if/else. Это правда, поэтому мне нужно задать для ООП ещё одно ограничение.
Использование полиморфизма не должно создавать зависимости вызывающего от вызываемого.
Чтобы это объяснить, давайте ещё раз посмотрим на выражения. Выражение 1: f(o), похоже зависит от функции f на уровне исходного кода. Мы делаем такой вывод потому что мы также предполагаем, что f только одна и что поэтому вызывающий должен знать о вызываемом.
Однако, когда мы смотрим на Выражение 2. o.f() мы предполагаем что-то другое. Мы знаем, что может быть много реализаций f и мы не знаем какая из этих функций f будет вызвана на самом деле. Следовательно исходный код, содержащий выражение 2 не зависит от вызываемой функции на уровне исходного кода.
Если конкретнее, то это означает, что модули (файлы с исходным кодом), которые содержат полиморфные вызовы функций не должны ссылаться на модули (файлы с исходным кодом), которые содержат реализацию этих функций. Не может быть никаких include или use или require или каких-то других ключевых слов, которые создают зависимость одних файлов с исходным кодом от других.
Итак, наше редукционистское определение ООП это:
Техника использующая динамический полиморфизм чтобы вызывать функции и не создающая зависимостей вызвающего от вызываемого на уровне исходного кода.
Что такое ФП?
И опять я буду использовать редукционистский подход. У ФП есть богатые традиции и история, корни которых глубже, чем само программирование. Существуют принципы, техники, теоремы, философии и концепции, которыми пронизана эта парадигма. Я всё это проигнорирую и перейду сразу к самой сути, к неотъемлемому свойству которое отделяет ФП от других стилей. Вот оно:
f(a) == f(b) если a == b.
В функциональной программе вызов функции с тем же аргументом даёт тот же результат независимо от того, как долго работала программа. Это иногда называют референциальной прозрачностью.
Из сказанного выше вытекает следствие, что f не должна менять части глобального состояния, которые влияют на поведение f. Более того, если мы скажем, что f представляет все функции в системе — то есть все функции в системе должны быть референциально прозрачными — тогда ни одна функция в системе не может изменить глобальное состояние. Ни одна функция не может сделать ничего, что может привести к тому, что другая функция из системы вернёт другое значение при тех же аргументах.
У этого есть и более глубокое следствие — ни одно именованное значение нельзя менять. То есть оператора присваивания нет.
Если тщательно обдумать это утверждение, то можно прийти к заключению, что программа, состоящая только из референциально прозрачных функций ничего не может сделать — так как любое полезное поведение системы меняет состояние чего-нибудь; даже если это просто состояние принтера или дисплея. Однако, если из требований к референциальной прозрачности исключить железо и все элементы окружающего мира оказывается, что мы можем создавать очень полезные системы.
Фокус, конечно, в рекурсии. Рассмотрим функцию которая принимает структуру с состоянием в качестве аргумента. Этот аргумент состоит из всей информации о состоянии, которая нужна функции для работы. Когда работа окончена, функция создаёт новую структуру с состоянием, содержимое которой отличается от предыдущей. И последним действием функция вызывает саму себя с новой структурой в качестве аргумента.
Это только один из простых трюков, которые фукциональная программа может использовать чтобы хранить изменения состояния без необходимости изменять состояние [1].
Итак, редукционистское определение функционального программирования:
Референциальная Прозрачность — переприсваивать значения нельзя.
ФП против ООП
К этому моменту и сторонники ООП и сторонники ФП уже смотрят на меня через оптические прицелы. Редукционизм не лучший способ завести друзей. Но иногда он полезен. В данном случае, я думаю что полезно пролить свет на никак не утихающий холивар ФП против ООП.
Ясно, что два редукционистских определения, которые я выбрал, совершенно ортогональны. Полиморфизм и Референциальная Прозрачность не имеют никакого отношения друг к другу. Они никак не пересекаются.
Но ортогональность не подразумевает взаимного исключения (спросите Джеймса Клерка Максвелла). Вполне можно создать систему, которая использует и динамический полиморфизм и референциальную прозрачность. Это не только возможно, это правильно и хорошо!
Почему эта комбинация хороша? По точно тем же причинам, что оба её компонента! Системы построенные на динамическом полиморфизме хороши, потому что они обладают низкой связностью. Зависимости можно инвертировать и расположить по разные стороны архитектурных границ. Эти системы можно тестировать используя Моки и Фейки и другие виды Тестовых Дублей. Модули можно модифицировать не внося изменения в другие модули. Поэтому такие системы проще изменять и улучшать.
Системы, построенные на референциальной прозрачности тоже хороши, потому что они предсказуемы. Неизменяемость состояния делает такие системы проще для понимания, изменения и улучшения. Это значительно уменьшает вероятность возникновения гонок и других проблем, связанных с многопоточностью.
Главная мысль тут такая:
Нет никакого холивара ФП против ООП
ФП и ООП хорошо работают вместе. И то и другое хорошо и правильно использовать в современных системах. Система, которая построена на комбинации принципов ООП и ФП максимизирует гибкость, поддерживаемость, тестируемость, простоту и прочность. Если убрать одно ради добавления другого это только ухудшит структуру системы.
[1] Так как мы используем машины с архитектурой Фон Неймана мы предполагаем, что в них есть ячейки памяти, состояние которых на самом деле изменяется. В механизме рекурсии, который я описал, оптимизация хвостовой рекурсии не даст создавать новые стекфреймы и будет использоваться первоначальный стекфрейм. Но это нарушение референциальной прозрачности (обычно) скрыто от программиста и ни на что не влияет.
- Программирование
- Java
- Совершенный код
- Функциональное программирование
Когда применять функциональное программирование, а когда ООП — отвечают эксперты
ООП или функциональное программирование? А может, всё сразу? Узнаём у экспертов, когда нужно применять ту или иную парадигму программирования.
Многие слышали про функциональное программирование и, возможно, задавались вопросом: «А зачем оно, когда есть ООП?». Мы спросили у экспертов, когда стоит использовать ту или иную парадигму.
Когда применять функциональное программирование, а когда ООП?
На данный момент этот блок не поддерживается, но мы не забыли о нём! Наша команда уже занята его разработкой, он будет доступен в ближайшее время.
На данный момент этот блок не поддерживается, но мы не забыли о нём! Наша команда уже занята его разработкой, он будет доступен в ближайшее время.
На данный момент этот блок не поддерживается, но мы не забыли о нём! Наша команда уже занята его разработкой, он будет доступен в ближайшее время.
На данный момент этот блок не поддерживается, но мы не забыли о нём! Наша команда уже занята его разработкой, он будет доступен в ближайшее время.
На данный момент этот блок не поддерживается, но мы не забыли о нём! Наша команда уже занята его разработкой, он будет доступен в ближайшее время.
Чем функциональное программирование отличается от объектно ориентированного

Если вы хотите инициализировать ученика, вы можете сделать что-то вроде этого:

Purity » от xkcd распространяется под лицензией CC BY-NC 2.5.» />
Когда у вас есть такие маленькие, «чистые» функции, то использовать их повторно намного проще, чем вашу традиционную объектно-ориентированную программу. Это спорный подход (если вы посмотрите «повторное использование в функциональном программировании», вы найдете много дискуссий и подкастов на эту тему), так что, я думаю, вы согласитесь со мной в том, что когда вы хотите повторно использовать класс в ООП и добавить функцию, вы добавляете условия и параметры, и ваши функции становятся больше. Ваши абстрактные классы и интерфейсы становятся довольно надежными. Вы должны уделять пристальное внимание более крупной архитектуре приложения из-за побочных эффектов и других факторов, которые повлияют на вашу программу (как мы говорили ранее).
В функциональном программировании все наоборот: ваши функции становятся меньше и гораздо более подходят для того, что вы хотите получить. Каждая функция делает что-то одно, и всякий раз, когда вы захотите сделать что-то одно, вы будете использовать конкретную функцию.
Конечно, в каждой системе есть исключения, но обычно это то, что вы видите в различных базах исходного кода во всем мире!
Ладно, ладно, я заинтригован. Как начать?
Если вы уже хорошо разбираетесь в JavaScript или Python, вы можете сразу приступить к изучению концепций функционального программирования, о которых мы говорили здесь. Если вы хотите больше узнать о «чистых» языках, предназначенных для функционального программирования, вы можете попробовать семейство Lisp (включая Common Lisp, Scheme и Clojure), семейство ML (включая OCaml и F#), Erlang, Elixir, Elm или Haskell.
Функциональное программирование может немного сбивать с толку, пока вы не привыкнете к нему. Но если вы дадите ему шанс и попробуете, ваше программное обеспечение будет надежным, его будет легче отлаживать, и вы будете наслаждаться гарантиями, которые дает прочная основа функционального программирования!
Материалы по теме
- Функциональное программирование: рефакторинг, замыкания и функции высшего порядка
- Функциональное программирование и его применение в JavaScript
Источники
Функциональное программирование или ООП?
Часто встречаю статьи и доклады от функциональщиков, что функциональное программирование рулит, а объекты это треш. Не будем здесь говорить о процедурщиках, которые думают, что они функциональщики. Не будем их разочаровывать, что ни к кому из вышеперечисленных они порой не относятся (и по этой причине иногда идут лесом). Разберёмся, чем функциональное программирование отличается от других парадигм и для чего это всё вообще нужно.
Парадигмы придумывают людьми для каких-то специфических целей и для упрощения работы. Да-да, как я и сказал когда-то на форуме:
Архитектуру придумали для упрощения сложного кода, а не для усложнения лёгкого.
Какие же парадигмы придумали? На ассемблере мы пишем нечасто, поэтому в самый низ опускаться не будем. Лапшекод и последующий процедурный подход тоже, так как с ними всё понятно. Остановимся на высокоуровневых парадигмах, призванных структурировать этот лапшекод.
Объектно-ориентированный подход
Когда кода становится много, его нужно как-то разбить по процедурам. Когда процедур становится много, то их нужно как-то сгруппировать по обязанностям и разнести по модулям. Если несколько процедур и функций как-то связаны работой с одними и теми же данными, то их удобнее вместе с этими данными сгруппировать в объект.
Но если просто возьмёте груду кода и просто перенесёте процедуры в классы, как я говорил на итенсиве, то не станете сразу объектно-ориентированным программистом. Это другой подход к компоновке кода. Целый отдельный образ жизни и мыслей.
Настоящее ООП нацелено на разделение обязанностей и сокрытие информации.
Это парадигма, придуманная для моделирования объектов реального мира. Как она это делает? Удобно показывать на метафорах и аналогиях, поэтому рассмотрим ситуацию с тостером или микроволновкой:

Есть контроллер, управляющий всеми запчастями чёрными стрелками и подписанный на состояние кнопок по голубым проводам. Как этот агрегат работает?
Кнопка включения передаёт сообщение Меня нажали . Контроллер передаёт сообщение Включись нагревателю и Запустись на 10 секунд таймеру, подписываясь при этом на его сигналы. Через указанное время таймер уведомляет Я истёк и контроллер передаёт Выключись печке. А по сигналу с кнопки выключения контроллер передаёт сигналы Выключись нагревателю и Стоп таймеру.
Если вдруг надо будет перепрограммировать логику, то всего лишь доработаем «прошивку» главного контроллера.
А если нужно добавить термометр, дисплей и GSM-модуль для отправки SMS-уведомлений? Запросто подключаем их к контроллеру своими «родными» разъёмами и в обработчике события истечения от таймера мы после остановки печки отправляем SMS о готовности. Или автоматически фотографируем обед и постим в Instagram. Но суть здесь одна:

Контроллер при нажатии кнопки включает дисплей и подписывает его на события таймера через себя. Может и напрямую, но тогда дисплей и таймер должны быть совместимы. Что это напоминает?
Это классический подход Model-View-Controller (MVC), часто используемый в оконных приложениях, где есть много кнопок, дисплеев и прочих элементов.
В данном случае все связи идут не хаотически, а от контроллера. Нагреватель, таймер, дисплей и кнопки не знают друг о друге. Кнопки умеют только нажиматься, таймер только считать. Каждый делает только свою работу. Каждую специализированную запчасть легко проверить и поменять.
В такой системе можно вместо нагревателя даже поставить холодильник или шлагбаум. И у контроллера может быть возможность подключить что угодно:
class Controller < private $devices = []; function addDevice(ВклВыклInterface $device) < $this->devices[] = $device; > . >
лишь бы это «что угодно» поддерживало указанный интерфейс:
interface ВклВыклInterface
class Пылесос implements ВклВыклInterface < public function вкл() < . >public function выкл() < . >>
Тогда просто создаём все устройства и закидываем их в контроллер:
$controller->addDevice(new Шлагбаум()); $controller->addDevice(new Пылесос());
И такой контроллер будет всех их включать и отключать по таймеру.
В хозяйстве это вещь весьма полезная. Даже продвинутые варианты таких контроллеров уже есть:

В такой можно включить даже электропилу, реализующую интерфейс ЕвроВилкаInterface .
А что если у нас в хозяйстве появилась бензопила? Она заводится особым образом и имеет свои методы:
class Бензопила < public function включитьЗажигание() < . >public function открытьЗаслонку() < . >public function закрытьЗаслонку() < . >public function дёрнутьСтартер() < . >public function работает() < . >>
Если у бензопилы нет кнопок вкл и выкл как у электропилы, то просто напишем адаптер:
class БензопилаАдаптер implements ВклВыклInterface < private $пила; public function __construct(Бензопила $пила) < $this->пила = $пила; > public function вкл() < $пила = $this->пила; $пила->включитьЗажигание(); $пила->закрытьЗаслонку(); while (!$пила->работает()) < $пила->дёрнутьСтартер(); > $пила->открытьЗаслонку(); > public function выкл() < $this->пила->выключитьЗажигание(); > >
Он снаружи будет выглядеть так, как нужно нашему контроллеру, а внутри себя будет скрывать весь этот сложный процесс. Так можно и для ядерного реактора адаптер написать, если вдруг это понадобится.
В реальности нам пригодился бы скромный набор деталей:

И теперь одним махом включаем бензопилу в разъём контроллера:
$controller->addDevice(new БензопилаАдаптер(new Бензопила()));
Бензопила с Arduino-адаптером теперь ничем не отличается от нагревателя. Мощь полиморфизма 🙂
Всё как в жизни. Абстрагируясь от реализации всего этого, для нас каждый модуль это всего лишь ящик с парой проводов. Груда проводов и транзисторов в виде элементов открытого ассоциативного массива причинит много проблем, так как нечаянно можно замкнуть не тот провод. А закрытый толстым кожухом объект с парой видимых кнопок или разъёмов с этим справится идеально.
Приятно и удобно программировать специализированными ящиками, обменивающимися сообщениями.
Умело разделяя систему на объекты и продумывая сообщения между ними можно достичь нирваны в ООП. А влезая в это кривыми руками можно забыть о структуре и сделать месиво:

Здесь компоненты соединены кучами проводов и нужно всюду впаивать логику, чтобы термометр умел работать с дисплеем и включать печку. Бензопилу сюда уже так просто не включишь. Мы уже упоминали эту проблему при организации независимых модулей сайта с интересными картинками.
Функциональный подход
Если просто программируете процедурно и ещё не успели изучить хотя бы тот же ООП, то вы не обязательно получите функциональное программирование. ФП тоже о разделении обязанностей и тоже призвано структурировать лапшекод, но немного по-другому. Это так:
данные → функция1 → данные → функция2 → данные → функция3 → результат
Пример: если Вы есть в соцсетях, то вас постоянно парсят маркетологи:
Профили ВК → f1 → сообщества профилей → f2 → общие сообщества → f3 → число участников → f4 → сообщества больше 1000 → f5 → статистика сообществ → f6 → разбор по демографии → f7 → разбор по возрасту.
И получим результат:
[ 'male' => [ '18-24' => [ [ 'Id' => 123456, 'name' => 'Как купить Lamborghini студенту', 'population' => 152000, 'demography' => [ 'male' => 67, 'female' => 33 ], 'age' => [ '0-18' => 19, '18-24' => 23, '24-30' => 18, . ] ] ] ] ]
Можно добавить город вначале:
Город -> f1 -> Профили ВК -> f2 -> Сообщества -> .
и фильтры с группировками менять местами. Объекты с методами здесь никуда не впишешь.
Здесь вместо объектов, объединяющих данные с поведением, всё разнесено раздельно на сами данные и на их обработчики. Каждый обработчик представляет из себя функцию, принимающую исходные данные и возвращающую результат.
Здесь идеально подходят ассоциативные массивы и другие примитивные структуры. Они не нагружают оперативку и процессор созданием тысяч и миллионов объектов для каждого элемента. Но что если фильтраций будет много? Дабы не копировать миллионные массивы снова и снова, удобнее передавать все значения по ссылке. Или сделать структуры в виде классов с полями, чтобы все значения хранились в памяти в одном экземпляре и передавались по указателю.
Чем это отличается от обычного процедурного подхода?
Разбиение императивного кода на процедуры и функции в процедурной парадигме служит как инструмент абстракции в руках умелых или только для избавления от копипасты в руках обычных. Функции рассчитывают результат, а процедуры что-то куда-то записывают. Ведь нет смысла вызывать процедуру, которая ничего не возвращает и ничего при этом не делает.
В нашем парсере ничего записывать не надо и императивная пошаговость не нужна. Мы просто в потоке преобразуем одни данные в другие, не перезаписывая старые значения. Поэтому в функциональной парадигме можно выкинуть процедуры и переменные за ненадобностью и оставить лишь константы и функции.
Приятно и удобно работать с данными, прогоняя их через специализированные конвертеры.
Умело разделяя расчёты на данные и функции можно достичь нирваны в ФП. А влезая в это кривыми руками можно забыть о структуре и сделать месиво.
Но как на ФП пишут сайты?
Поток вычислений
Во-первых, не обязательно делать весь сайт на ФП. На сайте с логикой могут быть некие комбинированные расчёты, где ООП неудобен. Именно эти фрагменты можно реализовать функционально.
Например, нам нужно к товарам в корзине начислить скидку на один экземпляр каждого, которого заказали больше трёх. Вместо возни с циклами, методами и прочим низкоуровневым мусором мы просто определяем, какие фильтры и преобразователи нам нужны:
$countCondition = function (CartItem $item) return $item->getCount() > 3; >; $getDiscount = function (CartItem $item) return $item->getPrice() * 0.1: >;
и теперь просто прогоняем массив наших товаров $items поштучно через эти операторы:
$discount = array_sum( // суммируем array_map($getDiscount, // расчитанные скидки array_filter($items, $countCondition))) // отфильтрованных элементов
Если же работать с коллекциями вместо простых массивов, то можно реализовать и так:
$discount = $items ->filter($countCondition) ->map($getDiscount) ->sum();
Здесь у объектов класса CartItem скрипт считывает цену и количество. А как собирается результат? Потоком:
Товары -> фильтр() -> товары -> расчёт() -> скидки -> сумма() -> результат
По этому примеру придумал скринкаст о подсчёте скидок. За ним ещё будет о написании многопоточного парсера, показывающий пользу неизменяемых данных при распараллеливании процессов. Кто ещё не подписался на вебинары, тот, как обычно, будет в пролёте.
Работа сайта
Во-вторых, можно отследить нить исполнения самого сайта. Он выглядит как сложная функция от запроса:
GET, POST, FILES, SERVER → request() → router(request) → controller(request) → viewFile, viewData → render(. ) → response → send(response) → HTTP/1.1 200 OK
Теперь вызываем что-то вроде этого:
print_r(handle(request(GET, POST)))
и видим сгенерированный ответ в виде массива:
[ 'status' => [ 'code' => 200, 'message' => 'OK', ], 'headers' => [ 'Content-Type' => 'text/html', ], 'content' => '.