Написание предсказуемого кода Elixir с помощью редукторов

Это первая часть серии из двух частей о сопровождаемом коде в Elixir. В этой части мы покажем, как предсказуемость кода играет решающую роль в краткосрочном и долгосрочном благополучии проекта. Для этого мы будем использовать встроенные возможности Elixir, такие как оператор pipe, кортежи и блоки with.

Сначала мы объясним, что такое предсказуемость и почему она так важна. Затем мы рассмотрим некоторые инструменты, которые уже есть в Elixir, и как вы можете использовать их для написания более качественного кода.

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

Давайте начнем!

О сложности и предсказуемости

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

Одна функция, одна переменная и одна простая операция.

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

Я читал что-то вроде этого:

…Что? Альфа? Тета с перевернутой шляпой?

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

В замечательной книге «Мозг программиста: What Every Programmer Needs to Know About, Felienne Hermans проделывает удивительную работу, объясняя различные механизмы, которые наш мозг использует для чтения, написания и понимания кода, а также техники, позволяющие выполнять эти задачи как можно эффективнее.

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

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

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

Почему я должен сделать свой код Elixir предсказуемым?

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

Три ключевых элемента делают код предсказуемым:

  1. Структура: различные блоки кода выглядят и ведут себя одинаково. Это основная причина, по которой легко понять цикл for, даже в языках, которые вы никогда раньше не использовали.
  2. Размер: небольшие блоки (например, функции, модули, классы и т.д.) легче запомнить.
  3. Простота: меньшее количество подвижных частей, таких как параметры функции, помогает нашему мозгу отслеживать происходящее.

Одним из хороших примеров предсказуемости в мире Elixir является библиотека Plug because:

  • В ней используются модели поведения, чтобы все реализации имели одинаковую структуру (ожидаемый вход и выход).
  • Предполагается, что каждая заглушка должна быть как можно меньше, поскольку они добавляют задержку в HTTP-ответ.
  • Они имеют довольно простую форму: две функции, каждая с двумя параметрами.

Итак, что если мы сделаем весь наш код предсказуемым?

Инструменты Elixir для написания предсказуемого кода

В Elixir есть пара хороших инструментов, которые при правильном использовании помогают нам создавать предсказуемый код: оператор pipe и оператор with.

Имейте в виду, что ни один из этих инструментов не обеспечивает соблюдение паттерна или принципа проектирования — это просто инструменты.

Оператор pipe в Elixir

Оператор pipe |> — это удивительный инструмент, который помогает нам выразить цепочку вызовов функций в виде простой последовательности действий.

Даже если вы никогда не писали код на языке Elixir, вы наверняка понимаете, что пытается сделать этот фрагмент кода:

form_params
|> validate_form()
|> insert_user()
|> report()
|> write_response()
Войти в полноэкранный режим Выйти из полноэкранного режима

Это просто читать и легко понять, что происходит.

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

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

Утверждение with в Elixir

Операторы Pipe просты, но нам часто нужно проверить возврат, чтобы убедиться, что мы имеем действительное состояние.

Давайте возьмем предыдущий пример и перепишем его с помощью блока with:

with %User{} = user_data <- validate_form(form_params),
     {:ok, user} <- insert_user(user_data),
     :ok <- report(user) do
  write_response(user)
else
  error ->
    report(error)
    handle_error(error)
end
Войти в полноэкранный режим Выход из полноэкранного режима

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

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

with %Payment{} = payment <- fetch_payment_information(params),
     {:ok, user} <- Session.get(conn, :user),
     address when !is_nil(address) <- fetch_address(user, params),
     {:ok, order} <- create_order(user, payment, address) do
  conn
  |> put_flash(:info, "Order completed!")
  |> render("checkout.html")
else
  {:error, :payment_failed} ->
    handle_error(conn, "Payment Error")

  %Store.OrderError{message: message} ->
    handle_error(conn, "Order Error")

  error ->
    handle_error(conn, "Unprocessable order")
end
Вход в полноэкранный режим Выход из полноэкранного режима

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

Кроме того, поскольку существующие функции не следуют шаблону, разработчик, добавляющий новый шаг к этой функции, не будет знать, что должна возвращать новая функция. Структуру? кортеж ok/error? Значение, отличное от nil/nil? Что насчет ошибок?

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

Проектирование лучших конвейеров в Elixir

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

  • Каждая функция на конвейере должна преобразовывать заданное состояние в обновленное состояние, и она должна выполнять только одно преобразование. Это поможет нам сохранить функции небольшими и сфокусированными.
  • Всегда получать два параметра. Первый параметр — это состояние, которое мы хотим преобразовать, а второй содержит любые необязательные или дополнительные данные, которые могут понадобиться для выполнения такого преобразования. Второй параметр является необязательным.
  • Всегда возвращает кортеж ok/error. Если все идет хорошо, возвращается обновленное значение состояния.

Пока что мы не будем требовать структуру или тип для описания ошибки.

Давайте вернемся к нашему коду проверки, теперь применяя эти правила:

options = %{conn: conn}

with {:ok, payment} <- fetch_payment_information(params, options),
     {:ok, user} <- fetch_user(conn),
     {:ok, address} <- fetch_address(%{user: user, params: params}, options),
     {:ok, order} <- create_order(%{user: user, address: address, payment: payment}, options)
  do
  conn
  |> put_flash(:info, "Order completed!")
  |> redirect(to: Routes.order_path(conn, order))
else
  {:error, error_description} ->
    conn
    |> put_flash(:error, parse_error(error_description))
    |> render("checkout.html")
end
Войти в полноэкранный режим Выйти из полноэкранного режима

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

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

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

Возникающий паттерн

Теперь мы видим, что при написании функций возникает паттерн: преобразования цепочки состояний — конвейер. Это почти как функция Enum.reduce/3.

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

Мы рассмотрим этот паттерн более подробно в следующей части этой серии.

В следующий раз: Обеспечение предсказуемости кода Elixir

В этой статье мы показали, как писать код по шаблону, который наш мозг понимает быстрее и точнее. И таким образом мы также можем писать — и продолжать писать! — код, который можно поддерживать в рабочем состоянии в долгосрочной перспективе.

Обратите внимание, что этот паттерн не охватывает организацию модулей, структуру проекта или дизайн API. Для этого я настоятельно рекомендую серию статей Towards Maintainable Elixir Саши Юрича.

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

До тех пор, счастливого кодинга!

P.S. Если вы хотите читать посты Elixir Alchemy сразу после их выхода из печати, подпишитесь на нашу рассылку Elixir Alchemy и не пропустите ни одного поста!

Оцените статью
devanswers.ru
Добавить комментарий