Паттерн Functional Options
Этот подход также порой может называться Option Pattern, OptFunc, Config Fucation и т.п., зависит от фантазии ваших коллег 🙃
Да, как мы знаем, опциональные аргументы Go не поддерживает, но можно делать так:
func SendNotification(userID string, msg string, opts ...OptionFunc)
Вместо добавления множества параметров, мы передаем один или несколько опциональных параметров (опций), в виде variadic-аргументов — opts.
Ниже мы ещё разберёмся как это работает, но давайте сначала посмотрим как этим пользоваться. Теперь вызовы будут выглядеть таким образом:
// С отправкой СМС, но без Email и без указания приоритета
SendNotification(userID, myMessage, WithSMS())
// С отправкой Email и с указанным приоритетом6
SendNotification(userID, myMessage, WithEmail(), WithPriority(HighPriority))
// И без всего этого:
SendNotification(userID, myMessage)
Что нам это даёт:
- Не надо рефакторить уже имеющиеся вызовы, они останутся без изменений
- Вызовы функций менее громоздкие, когда весь набор опций требуется очень редко, либо никогда
- Этот подход популярный и узнаваемый, а значит ваш опытный коллега сразу поймёт суть, лишь окинув ваш код быстрым взглядом.
- Моё субъективное мнение — это удобно, красиво и интуитивно понятно. Но тут вы можете со мной не согласиться, это нормально
- Параметры становятся будто бы именованными, сравните:
(true, false, "high", "important", ...)
и
(WithSMS(), WithEmail(), WithPriority("high"), WithTitle("important"), ...)
В первом случае вы можете лишь догадываться, какой параметр что означает, во втором сразу видите что есть что
Но есть и минусы — нужно написать чуть больше кода, и когда таких With() становится много, выглядит это даже более громоздко, чем раньше. То есть, если в большинстве вызовов вам приходится передавать много подобных опций, то, скорее всего, это не ваш вариант.
Если вам это не подходит, есть и другие варианты, например:
- Передавать вместо набора параметров одну структуру (такой подход используется в aws-sdk-go)
- Использовать отдельные функции для разных типов вызовов и др. О них я могу рассказать отдельно, но они более простые и очевидные.
Реализация
Сначала описываем тип OptionFunc и несколько функций, которые задают определенные параметры:
type OptionFunc func(*Notification)
func WithEmail() OptionFunc {
return func(n *Notification) {
n.SendEmail = true
}
}
func WithSMS() OptionFunc {
return func(n *Notification) {
n.SendSMS = true
}
}
func WithPriority(priority string) OptionFunc {
return func(n *Notification) {
n.Priority = priority
}
}
Теперь учим нашу функцию SendNotification() применять всё это:
func SendNotification(userID string, msg string, opts ...OptionFunc) {
// Создаем уведомление с базовыми параметрами
notification := &Notification{
UserID: userID,
Message: message,
}
// Применяем опции
for _, opt := range opts {
opt(notification)
}
// Отправляем уведомление
send(notification)
}
Здесь можете посмотреть рабочий пример и поиграться с ним. И также я скину исходный код этого примера в комментарии к этому посту, чтобы не потерялось.
Я встречал этот паттерн во всех компаниях, в которых писал код на Go, и мне он очень хорошо зашел. И даже если он вам не нравится, сталкиваться вы с ним будете часто, поэтому понимать что это и зачем точно стоит.
В комментариях предлагаю обсудить — нравится ли вам такое решение, и почему.
Сразу предупреждаю — агрессивные, неконструктивные, надменные и прочие комментарии я буду удалять, а в некоторых случаях банить авторов. Давайте вести профессиональную и конструктивную дискуссию, без оскорблений и эмоций.
#guide #pattern
Этот подход также порой может называться Option Pattern, OptFunc, Config Fucation и т.п., зависит от фантазии ваших коллег 🙃
Да, как мы знаем, опциональные аргументы Go не поддерживает, но можно делать так:
func SendNotification(userID string, msg string, opts ...OptionFunc)
Вместо добавления множества параметров, мы передаем один или несколько опциональных параметров (опций), в виде variadic-аргументов — opts.
Ниже мы ещё разберёмся как это работает, но давайте сначала посмотрим как этим пользоваться. Теперь вызовы будут выглядеть таким образом:
// С отправкой СМС, но без Email и без указания приоритета
SendNotification(userID, myMessage, WithSMS())
// С отправкой Email и с указанным приоритетом6
SendNotification(userID, myMessage, WithEmail(), WithPriority(HighPriority))
// И без всего этого:
SendNotification(userID, myMessage)
Что нам это даёт:
- Не надо рефакторить уже имеющиеся вызовы, они останутся без изменений
- Вызовы функций менее громоздкие, когда весь набор опций требуется очень редко, либо никогда
- Этот подход популярный и узнаваемый, а значит ваш опытный коллега сразу поймёт суть, лишь окинув ваш код быстрым взглядом.
- Моё субъективное мнение — это удобно, красиво и интуитивно понятно. Но тут вы можете со мной не согласиться, это нормально
- Параметры становятся будто бы именованными, сравните:
(true, false, "high", "important", ...)
и
(WithSMS(), WithEmail(), WithPriority("high"), WithTitle("important"), ...)
В первом случае вы можете лишь догадываться, какой параметр что означает, во втором сразу видите что есть что
Но есть и минусы — нужно написать чуть больше кода, и когда таких With() становится много, выглядит это даже более громоздко, чем раньше. То есть, если в большинстве вызовов вам приходится передавать много подобных опций, то, скорее всего, это не ваш вариант.
Если вам это не подходит, есть и другие варианты, например:
- Передавать вместо набора параметров одну структуру (такой подход используется в aws-sdk-go)
- Использовать отдельные функции для разных типов вызовов и др. О них я могу рассказать отдельно, но они более простые и очевидные.
Реализация
Сначала описываем тип OptionFunc и несколько функций, которые задают определенные параметры:
type OptionFunc func(*Notification)
func WithEmail() OptionFunc {
return func(n *Notification) {
n.SendEmail = true
}
}
func WithSMS() OptionFunc {
return func(n *Notification) {
n.SendSMS = true
}
}
func WithPriority(priority string) OptionFunc {
return func(n *Notification) {
n.Priority = priority
}
}
Теперь учим нашу функцию SendNotification() применять всё это:
func SendNotification(userID string, msg string, opts ...OptionFunc) {
// Создаем уведомление с базовыми параметрами
notification := &Notification{
UserID: userID,
Message: message,
}
// Применяем опции
for _, opt := range opts {
opt(notification)
}
// Отправляем уведомление
send(notification)
}
Здесь можете посмотреть рабочий пример и поиграться с ним. И также я скину исходный код этого примера в комментарии к этому посту, чтобы не потерялось.
Я встречал этот паттерн во всех компаниях, в которых писал код на Go, и мне он очень хорошо зашел. И даже если он вам не нравится, сталкиваться вы с ним будете часто, поэтому понимать что это и зачем точно стоит.
В комментариях предлагаю обсудить — нравится ли вам такое решение, и почему.
Сразу предупреждаю — агрессивные, неконструктивные, надменные и прочие комментарии я буду удалять, а в некоторых случаях банить авторов. Давайте вести профессиональную и конструктивную дискуссию, без оскорблений и эмоций.
#guide #pattern