Spec-Zone.ru › Elixir 1.10

Руководство по библиотекам

В данном документе изложены общие рекомендации, антипаттерны и правила для тех, кто создает и публикует библиотеки 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 , но значение этих чисел зависит от вас. Большинство проектов выбирают Семантическое версионирование.

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

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

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

  • Написать документацию. Сообщество 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, который может быть запущен с несколькими параметрами, вы хотите проверить эти параметры при запуске сервера и полагаться только на структурированные данные на протяжении всего жизненного цикла процесса. Аналогично, если база данных или сокет предоставляют вам карту строк, после получения данных вы должны проверить их и, возможно, преобразовать в структуру или карту атомов.

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

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

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

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

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

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

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

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

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

@http_client Application.fetch_env!(:my_app, :http_client)

def request(path) do
  @http_client.request(path)
end

вы должны сделать так:

def request(path) do
  http_client().request(path)
end

defp http_client() do
  Application.fetch_env!(:my_app, :http_client)
end

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

Если по какой-либо причине вам необходимо прочитать среду приложения во время компиляции, используйте Application.compile_env/2. Подробнее об этом см. раздел «Среда компиляции» в документации по приложениям.

Избегайте 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 указывает, что у нас есть возможность вообще не делать import MyLib , так как мы можем просто вызывать функцию как MyLib.some_fun(arg1, arg2).

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

END_OF_DOCUMENT_MARKER

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

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

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

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

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

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

Когда вам абсолютно необходимо использовать макрос, убедитесь, что макрос не является единственным способом взаимодействия пользователя с вашей библиотекой, и старайтесь свести к минимуму количество кода, генерируемого макросом. Например, модуль Logger предоставляет Logger.debug/2, Logger.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
    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

Это тот же шаблон, который генерируется 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.10.4/library-guidelines.html

Spec-Zone.ru

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