Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
145 changes: 145 additions & 0 deletions posts/elixir_configuration.md
Original file line number Diff line number Diff line change
@@ -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 |
| ----------| ------------- | ------------- |
| Прозрачность применения конфига | ❌ | ✅ |
| Повторное использование конфига в разных модулях | ✅ | ❌ |
| Побуждение к структуризации модулей | ❌ | ✅ |
| Отсутствие лишних абстракций | ✅ | ❌ |