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

Итак, как нам настроить архитектуру приложения (Эликсира)? Какой из них мы должны выбрать, так как у нас их так много, как:

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

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

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

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

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

Независимо от того, что вы слышите от какого-то лидера мысли или авторитета. Кроме того, это всегда дело вкуса.

Теперь, когда это очищается, давайте прыгать!

Лук — основа любого хорошего супа

Для лучшего понимания и чтобы сразу не заблудиться, мы для начала проигнорируем основную детальную схему нашей архитектуры, нарисованную Герберто (показана вверху статьи).

Вместо этого мы нарисуем более концептуальную диаграмму, мини-карту, которую будем держать в уме до конца статьи. Он основан на Onion Architecture и выглядит следующим образом:

На схеме есть две важные вещи:

  • Направление зависимостей — к центру. Это означает, что внешнее знает о внутреннем, а не наоборот. Например, домен в центре не вызывает инфраструктуру (например, БД). Внутреннее просто определяет контракт (интерфейс или поведение или порт), а внешнее реализует его (адаптер), что на самом деле является Порты и адаптер (шестиугольная) Архитектура. Тогда контракт является частью внутри, потому что только внутри знает, что ему нужно! Пожалуйста, посмотрите видео Шестиугольная архитектура, чтобы узнать больше.

В Elixir, используя behaviours и применяя «программу к поведению», мы можем с помощью конфигурации определить, какой адаптер (реализация поведения) мы собираемся использовать. . Как мы увидим позже. Так:

«Программа поведения» — это концепция, которую следует помнить, и она играет ключевую роль.

  • Луковая архитектура не использует классическую многоуровневость приложения, т.е. один слой поверх другого (так называемая лазанья). Вместо этого мы используем контексты, где контекст представляет бизнес-процесс. И каждый использует кусочек определенного слоя лука. Другими словами, мы не размещаем все контроллеры в одном каталоге и т. д.

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

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

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

Ядро приложения

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

Эти четыре группы:

  1. Модели с бизнес-правилами.
  2. Повторяемая бизнес-логика.
  3. Варианты использования (которые работают с двумя вышеупомянутыми группами, организуя их).
  4. Соглашения о деловом общении с внешним миром.

Отныне все будет крутиться вокруг этих четырех групп. Начнем с того, что следуем рекомендациям DDD и определяем наш пример домена.

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

Домен управляет дизайном!

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

Знание домена

Мой домен — это статистика объявлений о недвижимости. В одном предложении я могу описать это так:

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

Разделим домен на два поддомена — ограниченные контексты: Идентификация и Статистика. Конечно, реальность никогда не бывает такой простой, как наш пример. Обычно у нас есть кросс-контекстные зависимости, которых здесь нет. Вы можете задаться вопросом, как домен Statistics узнает, какой пользователь вошел в систему, поскольку пользователь является частью контекста Identity. В моем упрощенном случае пользователь сохраняется в Elixir Plug Connection после того, как контексты Identity аутентифицируют его.

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

Давайте теперь нарисуем диаграмму того, что мы знаем о наших субдоменах:

Как мы видим, слева у нас есть команды (с ассоциативными данными), которые запускают рабочие процессы. На выходе у нас есть события, на которые могут подписываться другие контексты и т.д. и которые из-за упрощения не отрисовываются. В моем примере результаты рабочих процессов просто синхронно возвращаются во «внешний мир».

Доменный уровень (доменные модели + доменные службы)

На секунду давайте еще раз взглянем на нашу мини-карту. Мы все еще находимся в ядре приложения. Мы только что определили домен. Давайте теперь перейдем к середине ядра приложения!

Там мы можем найти уровень домена, который состоит из модели домена и сервисов домена. Это дом для группы 1 и группы 2, упомянутых выше. Точнее, в терминологии DDD это будут модели предметной области в виде сущностей, агрегатов, объектов-значений и доменных служб: логика предметной области, который может повторно использоваться между рабочими процессами и не относится к самим моделям предметной области.

Это может запутать, поэтому давайте начнем рисовать основную схему с того, что мы только что узнали:

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

Три правила слоя предметной области (Эликсира)

Помимо того, что я написал выше о направлении зависимостей — Домен Слой ни от чего не зависит (т.е. никаких обращений к «снаружи»), есть три правила, которые я заметил во время моего исследования и которым я следую в обязательном порядке:

  1. Только модели могут менять модели. В Эликсире, определяя типы моделей как Непрозрачные, мы скрываем их внутренние свойства от внешнего мира. Мы выставляем только тип и функции, вот и все. Даже если снаружи по-прежнему находится Ядро приложения, слои за пределами Модели предметной области не могут получить доступ к свойствам или изменить их. В противном случае диализатор выдаст ошибку. Это большая победа, потому что мы можем быть уверены, что только модели (и под этим я подразумеваю внутренние бизнес-правила) могут изменить модели!
  2. Модели не используются вне ядра приложения. Другими словами, модели не утекают даже внутри одного и того же контекста. Вот почему я реализую функцию to_map/1 для каждой модели, которая преобразует структуры в карты. Таким образом, не создается никакой зависимости! Если вы хотите узнать об этом больше, рекомендую посмотреть видео Код алхимика: больше ценности с меньшим количеством магии.
  3. Используйте умные конструкторы для создания моделей. Из-за непрозрачности мы можем создавать модели только с помощью функции-конструктора, которая берет входную карту и строит модель. Что это значит? Всякий раз, когда у нас есть модель в Application Core, мы можем быть уверены, что она была построена с помощью конструктора. Чтобы продвинуть это использование еще дальше — почему бы не проанализировать (проверить) входную карту? Таким образом, мы можем выполнять различные бизнес-проверки входных данных еще до создания модели. Мы должны сделать это даже для простых значений, таких как количества, например, это должно быть положительное число от 1 до 1000.

Если вы все еще спрашиваете себя, почему, почему, почему? Ответ прост, главное преследование здесь:

Иметь ограниченный контекст, который всегда содержит данные, которым можно доверять.

Давайте взглянем на содержимое одного из Credentials моей модели, соблюдая правила, установленные выше:

defstruct [:username, :password]
@opaque t :: %__MODULE__{
  username: String.t(),
  password: String.t(),
}
@spec new(map()) :: {:ok, t()} | {:error, atom()}
def new(params) do
  Data.Constructor.struct([
    {:username, BuiltIn.string()},
    {:password, BuiltIn.string()}],
    __MODULE__,
    params)
end
@spec to_map(t()) :: map()
def to_map(%__MODULE__{} = token_expiration) do
  token_expiration
  |> Map.from_struct()
end
@spec username(t()) :: String.t()
def username(%__MODULE__{username: u}), do: u
@spec password(t()) :: String.t()
def password(%__MODULE__{password: p}), do: p

Вверху я определяю модель предметной области struct, а затем тип как непрозрачный. После этого я реализую умный конструктор, выполняя простой синтаксический анализ, т.е. требуя, чтобы имя пользователя и пароль были непустыми строками. Для умного конструирования и парсинга я использую эликсирную библиотеку под названием data.

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

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

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

Теперь, когда я рассмотрел Domain Layer, давайте сделаем еще один шаг. Помните, что мы все еще находимся в Application Core, и у нас все еще есть две группы (группа 3 и группа 4), которые нужно охватить. Продолжим во второй части серии. Пожалуйста, продолжайте!