diff --git a/README.md b/README.md index 231243b..b5ca97d 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,7 @@ ## Elixir * [Elixir и паттернматчинг](/posts/elixir_patternmatching.md) +* [Пишем конфиг в Elixir приложениях](/posts/elixir_configuration.md) * [spawn процессов (Антипаттерн)](/posts/elixir_spawn_trouble.md) * [GenServer и его тестирование](/posts/elixir_genserver.md) * [Макросы и метапрограммирование (ч.1)](/posts/elixir_macroses_p1.md) diff --git a/posts/elixir_configuration.md b/posts/elixir_configuration.md new file mode 100644 index 0000000..8fe5054 --- /dev/null +++ b/posts/elixir_configuration.md @@ -0,0 +1,145 @@ +# Пишем конфиг в Elixir приложениях + +В этой статье мы разберем, какой подход к конфигурированию приложения предпочтительнее выбирать, стоит ли придерживаться каких-либо стандартов в этом деле или нет. + +## Как обычно делают (Способ 1) + +В большинстве случаев конфиг пишется чисто интуитивно: начинается со слова config, затем дописывается какой-либо осмысленный атом, а после структура со значениями. + +Пример: +```elixir +config :my_app, :slack, + url: "https://hooks.slack.com", + webhook: System.get_env("SLACK_WEBHOOK"), + timeout: 15, + emoji: ":ghost:" +``` + +Дальше где-то в **модуле с кодом** это все вытаскивается таким образом: + +```elixir +Application.get_env(:my_app, :slack)[:webhook] +``` + +## Плюсы и минусы такого подхода + +Один из главных минусов - отсутствует привязка к модулю, в итоге нет точного понимания, какой модуль использует данный конфиг. + +Второй минус вытекает из первого - мы не знаем, **сколько** модулей использует данный конфиг. Возможно он используется несколькими модулями, это может быть плюсом, но только в том случае, если модули используют параметры одинаково. + +Проблемы начнутся, например, когда этим модулям нужен параметр `url`, но одним он нужен со схемой `(http, https)`, а другим без. + +---- + +## Как можно делать (Способ 2) + +Думаю, уже по предыдущим абзацам стало ясно к чему я веду. Вместо атома можно использовать название модуля. + +Пример: +```elixir +config :my_app, MyApp.Integrations.SlackNotification, + url: "https://hooks.slack.com", + webhook: System.get_env("SLACK_WEBHOOK"), + timeout: 15, + emoji: ":ghost:" +``` + +Теперь мы видим, что конфиг ссылается на модуль `MyApp.Integrations.SlackNotification`, можем сделать вывод, что он используется в уведомлениях и знаем, в каком конкретно модуле. + +Если придерживаться такого правила при написании конфига, то в дальнейшем можно облегчить себе работу с ним, получать его более удобным способом. + +Для этого на уровне приложения нужно объявить **следующий макрос**: +```elixir +defmodule MyApp do + defmacro get_module_config(key) do + quote do + Application.get_env(:my_app, __MODULE__)[unquote(key)] + end + end +end +``` + +Теперь в модуле `MyApp.Integrations.SlackNotification` вместо `Application.get_env(:my_app, :slack)[:webhook]` можно написать: + +```elixir +defmodule MyApp.Integrations.SlackNotification do + require MyApp + ... + + def send(text) do + url = MyApp.get_module_config(:url) + timeout = MyApp.get_module_config(:timeout) + ... + end +end +``` + +Получается, мы добавили немного абстракции, которая позволяет стандартизировать работу с конфигом и, как следствие, упростить его получение. + + +## Плюсы и минусы такого подхода + +У данного подхода есть пара минусов: мы добавили лишнюю абстракцию и лишили себя возможности использовать один и тот же конфиг в нескольких модулях. + +Но, на самом деле, из этих минусов вытекают плюсы: +- Теперь мы знаем, в каком модуле конфиг используется +- Если конфиг пытается перетечь в несколько модулей, то, возможно, стоит пересмотреть их архитектуру и выделить **общее** в отдельный модуль, который будет хранить основной конфиг. + +Например, у нас появилась необходимость отправлять уведомления в другой чат с соответствующим emoji. Хорошим тоном будет использовать для каждого из них отдельный webhook. +При реализации конфига **первым способом** с большой вероятностью он превратился бы в такой: + +```elixir +config :my_app, :slack, + url: "https://hooks.slack.com", + timeout: 15, + webhook_1: System.get_env("SLACK_WEBHOOK_1"), + emoji_1: ":ghost:" + webhook_2: System.get_env("SLACK_WEBHOOK_2"), + emoji_2: ":beer:" +``` + +**Второй способ** добавил немного строгости и, в **худшем случае**, конфиг будет следующий: + +```elixir +config :my_app, MyApp.Integrations.SlackNotification1, + url: "https://hooks.slack.com", + webhook: System.get_env("SLACK_WEBHOOK_1"), + timeout: 15, + emoji: ":ghost:" + +config :my_app, MyApp.Integrations.SlackNotification2, + url: "https://hooks.slack.com", + webhook: System.get_env("SLACK_WEBHOOK_2"), + timeout: 15, + emoji: ":beer:" +``` + +**В лучшем**, программист разобьет модуль интеграции на несколько составляющих, чтобы избежать дублирования конфига: + +```elixir +config :my_app, MyApp.Integrations.SlackNotificationBase, + timeout: 15, + url: "https://hooks.slack.com" + +config :my_app, MyApp.Integrations.SlackNotification1, + webhook: System.get_env("SLACK_WEBHOOK_1"), + emoji: ":ghost:" + +config :my_app, MyApp.Integrations.SlackNotification2, + webhook: System.get_env("SLACK_WEBHOOK_2"), + emoji: ":beer:" +``` +---- + +# Выводы + +Для подведения итогов я составил таблицу с указанием того, какие возможности дает тот или иной подход. + +Я однозначно выбираю и использую в своих проектах второй способ, так как он добавляет прозрачность и стандартизирует подход. + +| - | Способ 1 | Способ 2 | +| ----------| ------------- | ------------- | +| Прозрачность применения конфига | ❌ | ✅ | +| Повторное использование конфига в разных модулях | ✅ | ❌ | +| Побуждение к структуризации модулей | ❌ | ✅ | +| Отсутствие лишних абстракций | ✅ | ❌ |