Spec-Zone.ru › Elixir 1.9

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

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

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

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

$ mix new my_library

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

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

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

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

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

Публикация

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

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

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

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

Следует избегать использования среды приложения (см. 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

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

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

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

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

Библиотека не должна предоставлять функциональность 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.

Хотя есть ситуации, когда 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.9.4/library-guidelines.html

Spec-Zone.ru

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