Организация функционального кода

Переведено в Докдоге – системе перевода технических текстов.

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

Принцип притяжения

Позвольте данным притягивать поведение.

Идея, и вряд ли она новая, такова, что модули и функции (поведение) нужно выстраивать вокруг структур и абстракций (данных). В Эликсире этот принцип побуждает строить модули и функции вокруг структур, поведений, и протоколов.

Рассмотрим в качестве примера всем известную библиотеку 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 тоже использует принцип притяжения, так как функции работают с одной и той же частью данных, а словарь статуса определён внутри модуля.

© 2020 / Россия Любые мысли и вопросы пишите на elixir@wunsh.ru.