Организация функционального кода
Переведено в Докдоге – системе перевода технических текстов.
Переходя на функциональные языки, разработчики часто задаются вопросом, как организовать код, если модули – всего лишь набор функций. Такой же вопрос беспокоил и автора, но поработав какое-то время на Эликсире и Элме, он осознал, что всё время использует один принцип организации кода, который также можно встретить в общей практике. Назовём его принципом притяжения.
Принцип притяжения
Позвольте данным притягивать поведение.
Идея, и вряд ли она новая, такова, что модули и функции (поведение) нужно выстраивать вокруг структур и абстракций (данных). В Эликсире этот принцип побуждает строить модули и функции вокруг структур, поведений, и протоколов.
Рассмотрим в качестве примера всем известную библиотеку Plug
. Plug
– отличный помощник в создании веб-приложений. С помощью данной библиотеки можно с лёгкостью организовать работу с запросами. Рубистам она может чем-то напомнить Rack
.
В основе Plug
лежат две вещи – структура соединения Plug.Conn
и спецификация, объясняющая, что такое плаг. Познакомившись с ними, вы поймёте, что все модули и функции вращаются вокруг одной из них.
Модуль Plug.Conn
Во-первых, обратите внимание, что все функции в модуле Plug.Conn
относятся к структуре соединения, определённой в этом же модуле. Но если присмотреться, то можно увидеть, что каждая функция принимает структуру соединения в качестве аргумента.
Вот несколько функций, определённых в модуле Plug.Conn
:
assign(conn, key, value)
clear_session(conn)
put_resp_content_type(conn, content_type, charset)
put_resp_header(conn, key, value)
put_session(conn, key, value)
send_resp(conn, status, body)
Видите? Все они принимают структуру соединения conn
первым аргументом и, хоть это и не очевидно, возвращают изменённую структуру соединения.
А всё потому, что структура Plug.Conn
является основой данного модуля, и, передавая её в качестве первого аргумента, можно строить пайплайны.
Рассмотрим пример обработки запроса:
conn
|> put_session(:current_user, user)
|> put_resp_header(location, "/")
|> put_resp_content_type("text/html")
|> send_resp(302, "You are being redirected")
Какой чистый код, не правда ли?
Итак, первое, что нужно сделать в рамках принципа притяжения, – это построить функции и модули вокруг структуры, с которой они работают. Структура притягивает функции.
Plug
в качестве спецификации
Во-вторых, модуль Plug
базируется на концепции «плага». Звучит как масло масляное, но имеется в виду, что во многих модулях библиотеки Plug
используются плаги или они сами являются плагами (а значит, придерживаются спецификации Plug
).
– Что за спецификация
Plug
такая?
Хороший вопрос.
Чтобы превратить модуль в работающий плаг, необходимо определить в нём функцию init/1
, которая будет принимать и возвращать набор параметров, а также функцию call/2
, принимающую структуру соединения в качестве первого аргумента, набор параметров – в качестве второго и возвращающую структуру соединения. А плаг-функция попросту должна соблюдать ту же спецификацию, что и функция call/2
в модуле, принимая структуру соединения и параметры в качестве аргументов и возвращая структуру соединения.
Пример работающего плага-модуля:
defmodule CustomPlug do
def init(opts) do
opts
end
def call(conn, _opts) do
conn
end
end
А теперь давайте посмотрим, каким образом модули в библиотеке Plug
строятся на концепции плагов.
Plug.Builder
– модуль библиотеки Plug
, позволяющий определять пайплайн плагов, которые будут выполняться последовательно в том порядке, в котором они были объявлены. Самое интересное здесь то, что многие модули библиотеки Plug
сами являются плагами, поэтому их тоже можно объединить в похожий пайплайн.
Рассмотрим пример:
defmodule MyPlugPipeline do
use Plug.Builder
plug Plug.Logger
plug Plug.RequestId
plug Plug.Head
plug :hello
def hello(conn, _) do
Plug.Conn.send_resp(conn, 200, "Hello world!")
end
end
Обратите внимание на то, что модули Plug.Logger
, Plug.RequestId
, и Plug.Head
– это плаги, и их можно использовать в пайплайне в Plug.Builder
. Объявив свою функцию-плаг в модуле (часть с plug :hello
), можно также смешивать и сочетать её с другими плагами.
Благодаря организации модулей и функций на абстракции плагов, Plug.Builder
позволяет объединять объёмные пайплайны с другими модулями и функции, что не противоречит абстракции.
Что дальше?
Определённо существуют и другие принципы организации функционального кода, но попробуйте внедрить хотя бы этот. Это имеет смысл. Принцип работает даже при наличии вложенных друг в друга модулей. Например, модуль Plug.Conn
не содержит в себе всей логики структуры соединения как таковой. Иногда он использует другие модули, содержащие в своём названии Plug.Conn
, например, Plug.Conn.Status
. Но если разобраться, Plug.Conn.Status
тоже использует принцип притяжения, так как функции работают с одной и той же частью данных, а словарь статуса определён внутри модуля.