Функциональное программирование или ООП?
Часто встречаю статьи и доклады от функциональщиков, что функциональное программирование рулит, а объекты это треш. Не будем здесь говорить о процедурщиках, которые думают, что они функциональщики. Не будем их разочаровывать, что ни к кому из вышеперечисленных они порой не относятся (и по этой причине иногда идут лесом). Разберёмся, чем функциональное программирование отличается от других парадигм и для чего это всё вообще нужно.
Парадигмы придумывают людьми для каких-то специфических целей и для упрощения работы. Да-да, как я и сказал когда-то на форуме:
Архитектуру придумали для упрощения сложного кода, а не для усложнения лёгкого.
Какие же парадигмы придумали? На ассемблере мы пишем нечасто, поэтому в самый низ опускаться не будем. Лапшекод и последующий процедурный подход тоже, так как с ними всё понятно. Остановимся на высокоуровневых парадигмах, призванных структурировать этот лапшекод.
Объектно-ориентированный подход
Когда кода становится много, его нужно как-то разбить по процедурам. Когда процедур становится много, то их нужно как-то сгруппировать по обязанностям и разнести по модулям. Если несколько процедур и функций как-то связаны работой с одними и теми же данными, то их удобнее вместе с этими данными сгруппировать в объект.
Но если просто возьмёте груду кода и просто перенесёте процедуры в классы, как я говорил на итенсиве, то не станете сразу объектно-ориентированным программистом. Это другой подход к компоновке кода. Целый отдельный образ жизни и мыслей.
Настоящее ООП нацелено на разделение обязанностей и сокрытие информации.
Это парадигма, придуманная для моделирования объектов реального мира. Как она это делает? Удобно показывать на метафорах и аналогиях, поэтому рассмотрим ситуацию с тостером или микроволновкой:
Есть контроллер, управляющий всеми запчастями чёрными стрелками и подписанный на состояние кнопок по голубым проводам. Как этот агрегат работает?
Кнопка включения передаёт сообщение Меня нажали
. Контроллер передаёт сообщение Включись
нагревателю и Запустись на 10 секунд
таймеру, подписываясь при этом на его сигналы. Через указанное время таймер уведомляет Я истёк
и контроллер передаёт Выключись
печке. А по сигналу с кнопки выключения контроллер передаёт сигналы Выключись
нагревателю и Стоп
таймеру.
Если вдруг надо будет перепрограммировать логику, то всего лишь доработаем «прошивку» главного контроллера.
А если нужно добавить термометр, дисплей и GSM-модуль для отправки SMS-уведомлений? Запросто подключаем их к контроллеру своими «родными» разъёмами и в обработчике события истечения от таймера мы после остановки печки отправляем SMS о готовности. Или автоматически фотографируем обед и постим в Instagram. Но суть здесь одна:
Контроллер при нажатии кнопки включает дисплей и подписывает его на события таймера через себя. Может и напрямую, но тогда дисплей и таймер должны быть совместимы. Что это напоминает?
Это классический подход Model-View-Controller (MVC), часто используемый в оконных приложениях, где есть много кнопок, дисплеев и прочих элементов.
В данном случае все связи идут не хаотически, а от контроллера. Нагреватель, таймер, дисплей и кнопки не знают друг о друге. Кнопки умеют только нажиматься, таймер только считать. Каждый делает только свою работу. Каждую специализированную запчасть легко проверить и поменять.
В такой системе можно вместо нагревателя даже поставить холодильник или шлагбаум. И у контроллера может быть возможность подключить что угодно:
class Controller { private $devices = []; function addDevice(ВклВыклInterface $device) { $this->devices[] = $device; } ... }
лишь бы это «что угодно» поддерживало указанный интерфейс:
interface ВклВыклInterface { public function вкл(); public function выкл(); }
как-то так:
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 → результат
Пример: если Вы есть в соцсетях, то вас постоянно парсят маркетологи:
И получим результат:
[ '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 скрипт считывает цену и количество. А как собирается результат? Потоком:
Товары -> фильтр() -> товары -> расчёт() -> скидки -> сумма() -> результат
По этому примеру придумал скринкаст о подсчёте скидок. За ним ещё будет о написании многопоточного парсера, показывающий пользу неизменяемых данных при распараллеливании процессов. Кто ещё не подписался на вебинары, тот, как обычно, будет в пролёте.
Работа сайта
Во-вторых, можно отследить нить исполнения самого сайта. Он выглядит как сложная функция от запроса:
Теперь вызываем что-то вроде этого:
print_r(handle(request(GET, POST)))
и видим сгенерированный ответ в виде массива:
[ 'status' => [ 'code' => 200, 'message' => 'OK', ], 'headers' => [ 'Content-Type' => 'text/html', ], 'content' => '<html><head>...</body></html>', ]
или в виде объекта Response.
Вы это могли заметить во входном скрипте проекта web/app.php
на Symfony:
$kernel = new AppKernel('prod', false); $request = Request::createFromGlobals(); $response = $kernel->handle($request); $response->send();
в public/index.php
в Laravel:
$kernel = $app->make(Illuminate\Contracts\Http\Kernel::class); $response = $kernel->handle( $request = Illuminate\Http\Request::capture() ); $response->send();
и в методе run()
класса приложения yii\base\Application
в Yii2:
public function run() { try { ... $response = $this->handleRequest($this->getRequest()); $response->send(); ... } catch (ExitException $e) { ... } }
Та же конвертация константы request
в результат response
, как и списка профилей в список сообществ.
И аналогично каждое действие контроллера и каждый middleware принимает Request
и возвращает Response
.
Соответственно, здесь фактически используется функциональный подход с чтением из базы текущих данных, прогоном их через шаблонизатор и возвратом из контроллера результата в виде Response-объекта. Сложности возникают лишь при изменении значений, поэтому сущности вроде User
или Product
можно оставить обычными объектами с изменяемым состоянием.
Вывод
Если рассматриваем проект как совокупность «ящиков», то удобно использовать ООП. Если как потоки преобразований данных, то удобнее ФП. Если это не подходит, то делаем гибрид или придумываем что-нибудь другое. Весьма забавно выглядят порой попытки спрограммировать «ящики» на функциональном подходе или реализовать гибкие преобразователи данных на ООП. И какой из этого главный вывод?
Чем больше парадигм и фреймворков знаешь (и умеешь), тем адекватнее можешь их сравнивать и выбирать.
Пока на этом всё. А попрактикуемся в функциональном программировании уже на вебинарах. Записывайтесь на эфир, чтобы там задать свои вопросы и раньше всех получить записи скринкастов. И оставляйте комментарии, если хотите что-то сказать или спросить.
Привет, спасибо за статью.
Спасибо Дмитрий. Отличная статья.
Спасибо за интересную статью! Довольно легко читается
Что то мне на почту ничего не пришло. Во сколько сегодня вебинар и по какому адресу?
Посмотрите в спаме.
Дима, спасибо за новые знания, у тебя классное умение объяснить сложные вещи доступным языком !!!
В разделе ООП где описано месиво неплохо би вспомнить о законе Деметры, следование которому (продумывание связей) так или иначе приводит к нормальной структуре.
Чем это помешает наделать месиво прямых связей всех объектов со всеми?
Человеку которому все по барабану, вообще ничего не помешает.
Человеку которий будет держать в голове намекнет, что что то пошло не так, если в каком нибудь своем сервисе будет вызов зависимости от своей зависимости.
>>связей всех объектов со всеми
согласитесь, что єто не совсем реальный кейс и явно указующий что все пошло не так.
> что то пошло не так, если в каком нибудь своем сервисе будет вызов зависимости от своей зависимости
В месиве кнопка напрямую вызывает таймер, печку и дисплей. Здесь нет никакой «зависимости от своей зависимости».
> согласитесь, что єто не совсем реальный кейс и явно указующий что все пошло не так.
Открываем реальный и популярный кейс и видим:
Это просто «моделька», которая:
- используется в формах;
- лезет в модуль;
- генерирует пароль;
- отправляет письма;
- пишет в лог;
- ищет в базе;
- логинит пользователя;
- кидает алерты в сессию.
Здесь всё напрямую через Yii::$app. Без всякой «зависимости от своей зависимости».
>В месиве кнопка напрямую вызывает таймер, печку и дисплей. Здесь нет никакой «зависимости от своей зависимости».
Да, в том примере нету. Но коммент бил не к примеру, а скорее как дополнение.
>Открываем реальный и популярный кейс и видим
ActiveRecord и его проблема с SRP во всей своей 'красе'.
>Здесь всё напрямую через Yii::$app
Да потому что ServiceLocator из-за 'удобства' любят размазать по всему приложению. Хотя место ему в контролерах/командах.
Но я вообще то о другом говорил - о проблемах зависимостей.
отличная статья, спасибо за ваш труд.
Юмор по теме от создателей Java. Они создали метод count() у интерфейса Stream )))))
Ваш пример с пилами, вкл, выкл объясняет, что такое классы и как они работают лучше чем любая книжка.
На подобных примерах и посложнее мы весь интенсив по ООП построили. Тоже рекомендую.
На мой взгляд ООП наоборот даже в этом примере сильно усложняет весь код. Представьте, будет 5млн строк в одном файле. Пока найдете нужный объект, пока разберетесь, как вообще всё устроено, пока найдете другие функции, объекты и пр. Можно даже вообще забыть, что необходимо было сделать.
В функциональном программировании всё гораздо проще. Сделать одну функцию для установки компонентов, все компоненты выносим в отдельные файлы и папки. Если нужно что-то изменить - нашли папку, файл. Там всего 2-3 функции, связанные между собой. Подредактировали и готово.
В ООП компоненты тоже можно сделать и вынести в отдельные файлы. Но смысл? Идея получается из функционального программирования, которая гораздо облегчит весь процесс программирования. Забудьте про ООП. Пора переходить на новое программировании с помощью функций!
Что 5млн строк в одном файле с ООП, что 5млн строк в одном файле с ФП, что 5млн строк в одном файле с ПП. Объекты тоже разносят в разные файлы и папки по 2-3 метода в объекте. Так что разницы никакой.
В вашем случае я бы контроллер и все выше кнопки, таймеры, дисплеи вынес бы в отдельные файлы. В качестве передачи параметров можно использовать массивы. Таким образом если изменится или добавятся свойства у одного из компонентов, например, кнопки - добавляешь в массив это значение. Провел все работы и в качестве обновления отправляешь только два файла - кнопку и контроллер.
А если вдруг вместо кнопки нужно сделать переключатель - создаешь в новой папке новый компонент и через этот же массив всё подключаешь.
Таким образом при необходимости любые компоненты легко включаются и выключаются, легко и быстро модифицируются, легко обновляются. Плюс гораздо больше, чем в ООП.
А если немного модифицировать код - можно сделать, чтобы не программист, а пользователь сам выбирал нужный интерфейс кнопки, добавлял новую функциональность, или отключал то, что не нужно.
Они и так в отдельных файлах. Легко включаются и выключаются, легко и быстро модифицируются, легко обновляются хоть в ООП, хоть в ФП.
Да, только вот непонятно, зачем тогда изучать ООП, если всё это то же самое можно сделать и в ФП??? Пока не убедили:)))
Всё можно делать в ООП, ФП и ПП и даже совмещать. Зачем кого-то переубеждать?
(На всякий случай), я не против ООП. Просто не понимаю, зачем его при трудоустройстве все требуют в обязательном порядке?
Ну переделал я пару проектов из ООП в ФП, но зато проект дальше развивается, теперь работает в 10 раз быстрее и пр. Подумаешь, кроме этого компонента всё остальное перестало работать. Но со временем всё же можно исправить.. Потом, кому надо, можно обратно в ООП переделать..
> теперь работает в 10 раз быстрее
Это правда?