Зависимости и зонтичные проекты

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

Наше приложение kv закончено, и значит пришло время сделать сервер, который будет обрабатывать запросы, которые мы объявили в первой главе:

CREATE shopping
OK

PUT shopping milk 1
OK

PUT shopping eggs 3
OK

GET shopping milk
1
OK

DELETE shopping eggs
OK

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

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

Внешние зависимости

Внешними называют зависимости, которые не относятся к вашей бизнес-логике. Например, если вам нужен HTTP API для распределённого приложения KV, вы можете использовать проект Plug как внешнюю зависимость.

Установка внешних зависимостей предельна проста. Как правило, мы используем пакетный менеджер Хекс, перечисляя зависимости в функции deps в файле mix.exs:

def deps do
  [{:plug, "~> 1.0"}]
end

Эта зависимость ссылается на последнюю версию Plug в серии версий 1.x.x, которая известна Хексу. Для указания последней версии используется стрелка ~> перед номером минимальной версии. Больше информации о версиях можно получить в документации к модулю Version.

Обычно стабильные релизы загружены в Хекс. Если вы хотите добавить внешнюю зависимость, которая ещё находится в разработке, Микс позволяет указать Гит-зависимость:

def deps do
  [{:plug, git: "git://github.com/elixir-lang/plug.git"}]
end

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

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

$ mix help
mix deps              # Lists dependencies and their status
mix deps.clean        # Deletes the given dependencies' files
mix deps.compile      # Compiles dependencies
mix deps.get          # Gets all out of date dependencies
mix deps.tree         # Prints the dependency tree
mix deps.unlock       # Unlocks the given dependencies
mix deps.update       # Updates the given dependencies

Чаще всего используются функции mix deps.get и mix deps.update. Однажды загруженные зависимости будут автоматически скомпилированы. Вы можете узнать больше про deps, если вызовете команду mix help deps, а также из документации к модулю Mix.Tasks.Deps.

Внутренние зависимости

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

Если у вас есть внутренняя зависимость, Микс поддерживает два метода работы с ними: Гит-репозитории и зонтичные проекты.

Например, если вы храните проект kv в Гит-репозитории, вам нужно указать его в списке зависимостей, чтобы начать его использовать:

def deps do
  [{:kv, git: "https://github.com/YOUR_ACCOUNT/kv.git"}]
end

Если же репозиторий приватный, вам понадобится приватный URL git@github.com:YOUR_ACCOUNT/kv.git. В этом случае Микс сможет получать доступ к репозиторию, пока у вас есть права доступа.

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

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

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

Давайте создадим новый Микс-проект. Назовём его оригинально – kv_umbrella, и этот проект будет включать в себя уже созданное приложение kv и новое, kv_server. Структура директорий должна выглядеть следующим образом:

+ kv_umbrella
  + apps
    + kv
    + kv_server

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

Что ж, начнём!

Зонтичные проекты

Давайте начнём новый проект, запустив команду mix new. Назовём этот проект kv_umbrella, а также добавим опцию --umbrella при создании. Не создавайте этот новый проект внутри существующего проекта kv!

$ mix new kv_umbrella --umbrella
* creating .gitignore
* creating README.md
* creating mix.exs
* creating apps
* creating config
* creating config/config.exs

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

defmodule KvUmbrella.Mixfile do
  use Mix.Project

  def project do
    [
      apps_path: "apps",
      start_permanent: Mix.env == :prod,
      deps: deps
    ]
  end

  defp deps do
    []
  end
end

Отличие от предыдущего проекта в его определении, а именно в строке apps_path: "apps". Это значит, что проект будет вести себя, как зонтичный. Такие проекты не содержат ни исходников, ни тестов, но они могут иметь свои зависимости. Каждое дочернее приложение должно быть определено внутри директории apps.

Давайте перейдём в директорию apps и начнём работать над приложением kv_server. В этот раз передадим флаг --sup, который укажет Миксу сгенерировать дерево супервизора автоматически, вместо того, чтобы создавать его руками, как мы делали в предыдущих главах:

$ cd kv_umbrella/apps
$ mix new kv_server --module KVServer --sup

Сгенерированные файлы идентичны сгенерированным для kv, но с некоторыми отличиями. Откройте mix.exs:

defmodule KVServer.Mixfile do
  use Mix.Project

  def project do
    [
      app: :kv_server,
      version: "0.1.0",
      build_path: "../../_build",
      config_path: "../../config/config.exs",
      deps_path: "../../deps",
      lockfile: "../../mix.lock",
      elixir: "~> 1.6-dev",
      start_permanent: Mix.env == :prod,
      deps: deps()
    ]
  end

  # Выполните команду "mix help compile.app", чтобы узнать о приложении больше.
  def application do
    [
      extra_applications: [:logger],
      mod: {KVServer.Application, []}
    ]
  end

  # Выполните команду "mix help deps", чтобы узнать о зависимостях.
  defp deps do
    [
      # {:dep_from_hexpm, "~> 0.3.0"},
      # {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"},
      # {:sibling_app_in_umbrella, in_umbrella: true},
    ]
  end
end

Первое, что можно заметить, Микс автоматически определил структуру зонтичного проекта, т. к. мы сгенерировали этот проект внутри kv_umbrella/apps, и добавил следующие строки:

build_path: "../../_build",
config_path: "../../config/config.exs",
deps_path: "../../deps",
lockfile: "../../mix.lock",

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

Второе изменение в функции application внутри файла mix.exs:

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

Т. к. мы передали флаг --sup, Микс автоматически добавил mod: {KVServer.Application, []}, определяющий, что KVServer.Application – модуль колбэка приложения. Он запустит дерево супервизора нашего приложения.

А теперь откроем файл lib/kv_server/application.ex:

defmodule KVServer.Application do
  # Больше информации об OTP-приложениях
  # по ссылке https://hexdocs.pm/elixir/Application.html
  @moduledoc false

  use Application

  def start(_type, _args) do
    # Список всех дочерних процессов супервайзера
    children = [
      # Запускаем воркер вызовом функции `KVServer.Worker.start_link(arg)`
      # `{KVServer.Worker, arg}`
    ]

    # Другие стратегии и поддерживаемые опции
    # смотрите по ссылке https://hexdocs.pm/elixir/Supervisor.html
    opts = [strategy: :one_for_one, name: KVServer.Supervisor]
    Supervisor.start_link(children, opts)
  end
end

Обратите внимание, что тут определен колбэк приложения start/2, и вместо определения супервизора с именем KVServer.Supervisor, который использует модуль Supervisor, супервизор удобно определён в списке атрибутов! Вы можете прочитать больше о таких супервизорах в документации модуля Supervisor.

Мы уже можем попробовать наше первое приложение внутри проекта. Можно запустить тесты в директории apps/kv_server, но это будет не так круто, как перейти в корневую директорию зонтичного проекта и запустить mix test:

$ mix test

И это сработает!

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

Зависимости внутри зонтичного проекта

Микс предоставляет лёгкий механизм для указания зависимостей одного приложения-потомка от другого. Откройте файл apps/kv_server/mix.exs и измените функцию deps/0 на следующую:

defp deps do
  [{:kv, in_umbrella: true}]
end

Строка выше делает :kv доступным как зависимость внутри приложения :kv_server, и автоматически запускает приложение :kv перед запуском сервера.

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

+ kv_umbrella
  + apps
    + kv
    + kv_server

А теперь нам нужно изменить файл apps/kv/mix.exs, чтобы он содержал строки, специфичные для зонтичного приложения, которые мы видели в файле apps/kv_server/mix.exs. Откройте файл apps/kv/mix.exs и добавьте в функцию project следующее:

build_path: "../../_build",
config_path: "../../config/config.exs",
deps_path: "../../deps",
lockfile: "../../mix.lock",

Теперь вы можете запускать тесты для обоих проектов из корневой директории проекта командой mix test. Супер!

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

Заключение

В этой главе мы узнали больше о зависимостях Микса и о зонтичных проектах. Тогда как мы можем запустить приложение kv без сервера, сам сервер kv_server прямо зависит от kv. Разделив их на изолированные приложения, мы получаем больший контроль над их разработкой и тестированием.

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

Зонтичные приложения можно использовать как промежуточный этап при выделении некоторой функциональности из вашего кода в отдельное приложение. Например, представьте веб-приложение, которое отправляет «пуш-уведомления» своим пользователям. Вся «система пуш-уведомлений» может быть разработана как зонтичное приложение, с собственным супервизором и API. Если вы когда-нибудь столкнётесь с ситуацией, когда другой проект тоже должен отправлять пуш-уведомления, отделение этой логики будет предельно простым, а зависеть будет только от изменений API. Неважно, сделаете вы это на вторую неделю или третий год разработки. Выделенная однажды, система пуш-уведомлений может быть перемещена в приватный репозиторий или публичный пакет на сайте Hex.pm.

Разработчики также могут использовать зонтичные приложения для разделения объёмной бизнес-логики на части. Главное в этой ситуации – убедиться, что эти части не зависят друг от друга в двух направлениях (это называется циклической зависимостью). Если такая ситуация происходит, эти приложения не изолированы друг от друга, как вы думали изначально, и у вас есть проблемы в архитектуре и проектировании, которые нужно решить. Резюмируя, зонтичные приложения не являются магическим улучшением структуры вашего кода. Они могут, однако, помочь соблюдению границ, когда код хорошо спроектирован.

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

Имея наш работающий зонтичный проект, самое время перейти к написанию самого—сервера.

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