В этой статье мы разберем, какой подход к конфигурированию приложения предпочтительнее выбирать, стоит ли придерживаться каких-либо стандартов в этом деле или нет.
В большинстве случаев конфиг пишется чисто интуитивно: начинается со слова config, затем дописывается какой-либо осмысленный атом, а после структура со значениями.
Пример:
config :my_app, :slack,
url: "https://hooks.slack.com",
webhook: System.get_env("SLACK_WEBHOOK"),
timeout: 15,
emoji: ":ghost:"
Дальше где-то в модуле с кодом это все вытаскивается таким образом:
Application.get_env(:my_app, :slack)[:webhook]
Один из главных минусов - отсутствует привязка к модулю, в итоге нет точного понимания, какой модуль использует данный конфиг.
Второй минус вытекает из первого - мы не знаем, сколько модулей использует данный конфиг. Возможно он используется несколькими модулями, это может быть плюсом, но только в том случае, если модули используют параметры одинаково.
Проблемы начнутся, например, когда этим модулям нужен параметр url
, но одним он нужен со схемой (http, https)
, а другим без.
Думаю, уже по предыдущим абзацам стало ясно к чему я веду. Вместо атома можно использовать название модуля.
Пример:
config :my_app, MyApp.Integrations.SlackNotification,
url: "https://hooks.slack.com",
webhook: System.get_env("SLACK_WEBHOOK"),
timeout: 15,
emoji: ":ghost:"
Теперь мы видим, что конфиг ссылается на модуль MyApp.Integrations.SlackNotification
, можем сделать вывод, что он используется в уведомлениях и знаем, в каком конкретно модуле.
Если придерживаться такого правила при написании конфига, то в дальнейшем можно облегчить себе работу с ним, получать его более удобным способом.
Для этого на уровне приложения нужно объявить следующий макрос:
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]
можно написать:
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. При реализации конфига первым способом с большой вероятностью он превратился бы в такой:
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:"
Второй способ добавил немного строгости и, в худшем случае, конфиг будет следующий:
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:"
В лучшем, программист разобьет модуль интеграции на несколько составляющих, чтобы избежать дублирования конфига:
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 |
---|---|---|
Прозрачность применения конфига | ❌ | ✅ |
Повторное использование конфига в разных модулях | ✅ | ❌ |
Побуждение к структуризации модулей | ❌ | ✅ |
Отсутствие лишних абстракций | ✅ | ❌ |