Spec-Zone.ru › Elixir 1.7

Рекомендации по разработке библиотек

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

Начало работы

Вы можете создать новую библиотеку Elixir, выполнив команду mix new:

$ mix new my_library

Имя проекта указывается в формате snake_case (все буквы строчные, слова разделены символом нижнего подчеркивания). Этот же формат используется для переменных, имён функций и атомов в Elixir. Более подробная информация представлена в документе Рекомендации по именованию.

Каждый проект содержит файл mix.exs, содержащий инструкции по сборке, компиляции, запуску тестов и т. д. Библиотеки обычно имеют директорию lib, содержащую исходный код Elixir, и директорию test. Также может существовать директория src для исходного кода Erlang.

Дополнительную информацию о запуске проекта можно найти в официальном руководстве Mix & OTP или в документации по Mix Mix.

Приложения с деревом надзора

Команда mix new также позволяет использовать флаг --sup для создания приложения с деревом надзора по умолчанию. Мы подробнее рассмотрим деревья надзора позже, когда будем обсуждать распространённые антипаттерны при написании библиотек.

Публикация

Написание кода — это лишь первый шаг в публикации пакета. Мы настоятельно рекомендуем разработчикам:

  • Выбрать схему версионирования. Elixir требует, чтобы версии имели формат MAJOR.MINOR.PATCH, но значение этих чисел зависит от вас. Большинство проектов используют Semantic Versioning.

  • Выбрать лицензию. Наиболее распространённые лицензии в сообществе Elixir — MIT License и Apache 2.0 License. Последняя также используется в самом Elixir.

  • Выполнить форматирование кода. Форматировщик кода форматирует код в соответствии с согласованным стилем вашей библиотеки и всего сообщества, что облегчает понимание кода другими разработчиками и их вклад.

  • Написать тесты. Elixir поставляется со фреймворком для тестирования ExUnit. Сгенерированный проектом mix new включает примеры тестов и doctests.

  • Написать документацию. Сообщество Elixir гордится тем, что документация является первоочередной задачей и доступна для лёгкого использования. Библиотеки поддерживают эту практику, предоставляя полную документацию API с примерами для модулей, типов и функций. Дополнительная информация доступна в руководстве Написание документации. Такие проекты, как ExDoc, могут использоваться для генерации HTML и EPUB документов на основе документации. ExDoc также поддерживает «дополнительные страницы», такие как эта, которую вы сейчас читаете. Эти страницы дополняют документацию обучающими материалами, руководствами и справочными данными.

Проекты часто становятся доступными другим разработчикам путём публикации пакета Hex. Hex также поддерживает частные пакеты для организаций. Если ExDoc настроен для проекта Mix, публикация пакета на Hex автоматически опубликует сгенерированную документацию на HexDocs.

Антипаттерны

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

Избегайте использования исключений для управления потоком

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

try do
  contents = File.read!("some_path_that_may_or_may_not_exist")
  {:it_worked, contents}
rescue
  File.Error ->
    :it_failed
end

предпочтительнее использовать:

case File.read("some_path_that_may_or_may_not_exist") do
  {:ok, contents} -> {:it_worked, contents}
  {:error, _} -> :it_failed
end

Как автор библиотеки, вы несете ответственность за то, чтобы пользователям не приходилось использовать исключения для управления потоком в своих приложениях. Вы можете следовать той же конвенции, что и в Elixir, используя имя без ! для возвращаемых :ok/:error кортежей и добавляя ! для версии функции, которая вызывает исключение.

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

iex> File.read(1)
** (FunctionClauseError) no function clause matching in IO.chardata_to_string/1

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

Избегайте работы с некорректными данными

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

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

def my_fun(some_arg, file_to_write_to, options \\ []) do
  ...some code...
  AnotherModuleInLib.invoke_something_that_will_eventually_write_to_file(file_to_write_to)
  ...more code...
end

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

Более подходящее определение функции было бы:

def my_fun(some_arg, file_to_write_to, options \\ []) when is_binary(file_to_write_to) do

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

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

Избегайте конфигурации приложения

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

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

defmodule DashSplitter do
  def split(string) when is_binary(string) do
    String.split(string, "-", parts: 2)
  end
end

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

def split(string) when is_binary(string) do
  parts = Application.get_env(:dash_splitter, :parts, 2)
  String.split(string, "-", parts: parts)
end

Теперь пользователи могут настроить вашу библиотеку в файле config/config.exs следующим образом:

config :dash_splitter, :parts, 3

После конфигурации вашей библиотеки её поведение изменится для всех пользователей. Если библиотека ожидала разделения строки на 2 части, так как конфигурация глобальная, теперь она будет разделять её на 3 части.

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

def split(string, opts \\ []) when is_binary(string) and is_list(opts) do
  parts = Keyword.get(opts, :parts, 2)
  String.split(string, "-", parts: parts)
end

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

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

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

Избегайте use когда достаточно import

Библиотека не должна предоставлять use MyLib функциональность, если всё, что делает use MyLib — это import/alias сам модуль. Например, это антипаттерн:

defmodule MyLib do
  defmacro __using__(_) do
    quote do
      import MyLib
    end
  end

  def some_fun(arg1, arg2) do
    ...
  end
end

Причина, по которой следует избегать определения макроса __using__ выше, заключается в том, что когда разработчик пишет:

defmodule MyApp do
  use MyLib
end

он позволяет use MyLib выполнять любой код в модуле MyApp. Для человека, читающего код, невозможно оценить влияние, которое use MyLib оказывает на модуль, не изучая реализацию __using__.

Следующий код намного яснее:

defmodule MyApp do
  import MyLib
end

Этот код указывает, что мы импортируем только функции из MyLib, поэтому можем вызывать some_fun(arg1, arg2) напрямую без префикса MyLib.. Более того, import MyLib указывает, что у нас есть возможность вообще не импортировать модуль, так как мы можем просто вызвать функцию как MyLib.some_fun(arg1, arg2).

Если у модуля, на котором вы хотите вызвать функцию, длинное имя, например, SomeLibrary.Namespace.MyLib, и вы считаете его громоздким, вы можете воспользоваться специальной формой alias/2 и по-прежнему ссылаться на модуль как MyLib.

Хотя в большинстве случаев использование модуля необходимо, use следует пропускать, если всё, что оно делает, это import или alias модуль. Короче говоря, alias проще и понятнее, чем import, а import проще и понятнее, чем use.

Избегайте макросов

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

Цитируем официальное руководство по макросам:

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

Elixir уже предоставляет механизмы для написания повседневного кода простым и удобочитаемым способом с использованием его структур данных и функций. Макросы следует использовать только в крайнем случае. Помните, что **явное предпочтительнее неявного**. **Ясный код предпочтительнее лаконичного кода**.

Когда вам абсолютно необходимо использовать макрос, убедитесь, что макрос не является единственным способом взаимодействия пользователя с вашей библиотекой, и минимизируйте количество кода, генерируемого макросом. Например, модуль Logger предоставляет debug/2, info/2 и другие макросы, которые могут извлекать информацию об окружении, но механизм низкого уровня для ведения журнала всё ещё доступен с помощью Logger.bare_log/3.

Избегайте использования процессов для организации кода

Разработчик никогда не должен использовать процесс для организации кода. Процесс должен использоваться для моделирования свойств выполнения, таких как:

  • Изменяемое состояние и доступ к общим ресурсам (таким как ets, файлы и т. д.)
  • Конкурентность и распределение
  • Логика инициализации, завершения и перезапуска (как в супервайзерах)
  • Системные сообщения, такие как таймерные сообщения и события мониторинга

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

def add(a, b) do
  GenServer.call(__MODULE__, {:add, a, b})
end

def handle_call({:add, a, b}, _from, state) do
  {:reply, a + b, state}
end

def handle_call({:subtract, a, b}, _from, state) do
  {:reply, a - b, state}
end

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

def add(a, b) do
  a + b
end

def subtract(a, b) do
  a - b
end

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

Избегайте запуска неконтролируемых процессов

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

Если в вашем приложении нет дерева супервизора, его можно добавить, изменив def application внутри mix.exs так, чтобы включить ключ :mod с именем обратного вызова приложения:

def application do
  [
    extra_applications: [:logger],
    mod: {MyApp.Application, []}
  ]
end

а затем определите файл my_app/application.ex со следующим шаблоном:

defmodule MyApp.Application do
    # See https://hexdocs.pm/elixir/Application.html
    # for more information on OTP Applications
    @moduledoc false

    use Application

    def start(_type, _args) do
      # List all child processes to be supervised
      children = [
        # Starts a worker by calling: MyApp.Worker.start_link(arg)
        # {MyApp.Worker, arg},
      ]

      # See https://hexdocs.pm/elixir/Supervisor.html
      # for other strategies and supported options
      opts = [strategy: :one_for_one, name: MyApp.Supervisor]
      Supervisor.start_link(children, opts)
    end
  end
end

Это тот же шаблон, который генерируется mix new --sup.

Каждый процесс, запущенный с приложением, должен быть указан как дочерний элемент в Supervisor выше. Мы называем их «статическими процессами», потому что они известны заранее. Для обработки динамических процессов, таких как процессы, запускаемые во время запросов и других пользовательских вводов, посмотрите на модуль DynamicSupervisor.

Один из немногих случаев, когда допустимо запускать процесс вне дерева супервизора, – это с помощью Task.async/1 и Task.await/2. В отличие от Task.start_link/1, механизм async/await даёт вам полный контроль над жизненным циклом запущенного процесса – именно поэтому вы всегда должны вызывать Task.await/2 после запуска задачи с помощью Task.async/1. Однако, если ваше приложение запускает несколько асинхронных процессов, рекомендуется использовать Task.Supervisor для лучшей наглядности при инструментировании и мониторинге системы.

© 2012 Plataformatec
Licensed under the Apache License, Version 2.0.
https://hexdocs.pm/elixir/1.7.4/library-guidelines.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API