Рефакторинг в NodeJS с помощью функций высшего порядка

Sergey Ufocoder
devSchacht
Published in
5 min readNov 2, 2018

Перевод статьи Sean May: Refactoring Node with Higher-Order Functions.

До праздничных дней мой коллега Ник написал отменную статью о том, как можно улучшить навыки функционального программирования, выйдя из зоны комфорта. Он написал том, как горят корабли у берегов новых и неизведанных языков, и если вы уже нашли свое призвание в завоевании таких земель как Erlang, Elm, Haskell или в покорении островов Akka/Scala, то я снимаю перед вами шляпу.

Но на этот раз я хотел бы, чтобы наша баталия перенеслась поближе к дому, где я бы показал вам, как использовать функции высшего порядка для устранения одного из наиболее известных полей битвы в JavaScript: колбэки и маршрутизация в NodeJS-фреймворках (например, таких как Express и ему подобные).

Теперь давайте представим на мгновение, что мы имеем дело с очень простым описанием маршрутизации, которое возвращает статически заданный файл по запросу.

Согласитесь, что это не такое уж и громадное количество кода, с которым нам приходится иметь дело, плюс этот код не такой плохой, каким он мог бы быть.

Существуют десятки (возможно даже сотни) библиотек, на которые мы можем опереться, чтобы писать как можно меньше кода. Но если посмотреть на исходный код этих библиотек, то окажется, что на самом деле его там не так много.

Более того, у нас есть вложенная маршрутизация, где мы можем использовать Promise во вложенных маршрутах, а это сразу усложняет некоторые вещи.

Я обычный поклонник дяди Боба, и здесь его предложение, возможно, заключалось бы в том, чтобы разделить код на блоки по их назначению. Один блок кода контролировал бы ответ на запрос, а другой бы контролировал бизнес-логику.

Это может быть несколько опережающим на текущий момент, но код в JS должен быть небольшим и удобным для чтения.

Возможно, это можно считать преимуществом, и есть веские аргументы, чтобы убрать этот большой и неприятный беспорядок в коде, до тех пор, пока он не будет переписан с целью повторного его использования (а не простое разбиение алгоритма на последовательные фазы; давайте не будем заставлять дядю Боба плакать). Но цель в первую очередь состоит в том, чтобы предотвратить его появление.

С этой целью мы можем увидеть, что данный паттерн быстро станет шаблонным кодом.

Это намного меньше того, что могло бы получиться на языке Java, но, тем не менее, этот код всё равно присутствует. Однако, все три функции по-прежнему глубоко поражены проблемой необходимости держать их внутри обработчика маршрута.

Поскольку функции сильно связаны друг с другом, этот паттерн пригоден для каких-то единичных случаев, но он по-прежнему не пригоден для тестирования или переиспользования. Это не внушает особого оптимизма, потому что, даже если наш код стал капельку чище, у нас по прежнему останется много вещей, скрытых внутри этого маршрута.

Но у нас есть секретное оружие в JavaScript, да и в целом в функциональных языках. До сих пор нам было больно, но существует инструмент, который мы просто не понимали до этого момента.

Функции высшего порядка

Функция высшего порядка — это функция, которая принимает другую функцию в качестве аргумента, или это функция, которая будет возвращать в результате другую функцию (или и то, и другое).

В большинстве языков, даже если вы можете вернуть функцию из функции, это не принесет вам много пользы. Но в большинстве функциональных языков, благодаря концепции лексической области видимости, у нас есть возможность использования замыканий.

На самом деле вы использовали их все время, в колбэке для маршрутизации и в колбэке для readFile.

Теперь мы можем перейти на следующий уровень, чтобы сделать этот код более чистым и простым в тестировании. Во-первых, нам нужно убедиться, что мы понимаем, с чем мы имеем дело, и какой ожидать результат.

Задумайтесь над этим. Сначала мы передаем значение x, а после возвращаем обратно функцию. Но зачем это делать?

Обратите внимание, что значение x внутри функции защищено от изменения значения x за ее пределами. Это потому, что значение х, на которое мы ссылаемся — это значение, которое доступно в момент создания функции, а не когда функция вызывается. Таким образом, передавая этот вновь созданный колбэк в вызывающий код, мы запоминаем значение, которое передавалось как аргумент.

Вся сила функций этого типа заключается в сохранении переданных значений, и в дальнейшем использовании этой функции в разных местах.

Для использования ссылок на конфигурацию существует много возможностей для использования этих типов функций, но передайте функцию, которая будет использоваться в нескольких местах.

Зная это, мы должны иметь возможность найти способ создания функции, которая содержит некоторую информацию, и возвращает предварительно сконфигурированную функцию, готовую к использованию чем-то другим.

Если вы посмотрите на возвращаемую функцию, то станет ясно, что она делает. То есть, теперь она занимается тем то, что раньше делала функция nodeCallback.

Внешняя функция имеет колбэк для обработки успешного и ошибочного поведения.

Она конфигурирует экземпляр внутренней функции, чтобы внутренняя функция имела доступ к этим функциям, поэтому, когда возвращаемая функция вызывается, она может обратиться к ним, так же, как и в случае с rememberX.

Мы успешно удалили ранее созданый шаблонный код, заменив его на настраиваемую функцию высшего порядка. Если бы мы хотели пойти еще дальше, то мы бы создали аналогичные функции для создания обработчиков успеха и ошибок.

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

Если вашей команде вполне комфортно использовать подход функционального программирования, то возможно, вместо предыдущего варианта, ваш код мог бы быть таким:

Конечно, вам не нужно делать свой JavaScript-код похожим на LISP, но если это нравится вашей команде…

Этот подход имеет некоторые забавные особенности. Не принимая ничего из внешнего мира, кроме того что передается явный образом, мы создали код, который, к счастью, является:

  • Тестируемым
  • Переиспользуемым
  • Почти соответствует SOLID

Да, хорошо. Дядя Боб никогда не подразумевал, чтобы его принципы применимы к функциональному программированию, и он бы почувствовал здесь себя почти что обманутым, потому что большая часть нашего пути оказалось довольно простой… и мы даже не использовали наследование, более того, в этом не будет никакой необходимости и для оставшегося пути к нашей цели.

Конечно, это не конец. Вы можете с радостью использовать свои скомпонованные функции внутри других функций высшего порядка.

«Но почему я должен использовать этот подход, когда есть десятки библиотек, которые могут сделать это за меня?»

Реальность такова, что библиотеки действительно делают код более чистым и упрощают разработку. И если они у вас уже есть и они делают ваше жизнь проще, то продолжайте использовать их.

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

Этот подход можно применять на любом уровне и он особенно хорош для разделения кода, который сохраняет его ценность и поддерживает работу системы.

Существуют даже методы для автоматического создания конфигурируемых функций: «Частичное применение» и «Каррирование». Но об этом как-нибудь в другой раз.

Слушайте наш подкаст в iTunes и SoundCloud, читайте нас на Medium, контрибьютьте на GitHub, общайтесь в группе Telegram, следите в Twitter и канале Telegram, рекомендуйте в VK и Facebook.

Статья на GitHub

--

--