Функциональное программирование или ООП?
Часто встречаю статьи и доклады от функциональщиков, что функциональное программирование рулит, а объекты это треш. Не будем здесь говорить о процедурщиках, которые думают, что они функциональщики. Не будем их разочаровывать, что ни к кому из вышеперечисленных они порой не относятся (и по этой причине иногда идут лесом). Разберёмся, чем функциональное программирование отличается от других парадигм и для чего это всё вообще нужно.
Парадигмы придумывают людьми для каких-то специфических целей и для упрощения работы. Да-да, как я и сказал когда-то на форуме:
Архитектуру придумали для упрощения сложного кода, а не для усложнения лёгкого.
Какие же парадигмы придумали? На ассемблере мы пишем нечасто, поэтому в самый низ опускаться не будем. Лапшекод и последующий процедурный подход тоже, так как с ними всё понятно. Остановимся на высокоуровневых парадигмах, призванных структурировать этот лапшекод.
Объектно-ориентированный подход
Когда кода становится много, его нужно как-то разбить по процедурам. Когда процедур становится много, то их нужно как-то сгруппировать по обязанностям и разнести по модулям. Если несколько процедур и функций как-то связаны работой с одними и теми же данными, то их удобнее вместе с этими данными сгруппировать в объект.
Но если просто возьмёте груду кода и просто перенесёте процедуры в классы, как я говорил на итенсиве, то не станете сразу объектно-ориентированным программистом. Это другой подход к компоновке кода. Целый отдельный образ жизни и мыслей.
Настоящее ООП нацелено на разделение обязанностей и сокрытие информации.
Это парадигма, придуманная для моделирования объектов реального мира. Как она это делает? Удобно показывать на метафорах и аналогиях, поэтому рассмотрим ситуацию с тостером или микроволновкой:
Есть контроллер, управляющий всеми запчастями чёрными стрелками и подписанный на состояние кнопок по голубым проводам. Как этот агрегат работает?
Кнопка включения передаёт сообщение Меня нажали . Контроллер передаёт сообщение Включись нагревателю и Запустись на 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' => '.