Функциональные опции в Go: реализация шаблона опций в Golang

Kate

Administrator
Команда форума

Функциональные опции​

Оригинал статьи автора Soham Kamani здесь.
В этом посте рассказывается о том, какие функциональные опции есть в Go и как мы можем использовать шаблон "опции" для их реализации. Функциональные опции имеют форму дополнительных аргументов функции, которые расширяют или изменяют ее поведение. Вот пример, в котором используются функциональные параметры для создания новой структуры House:
h := NewHouse(
WithConcrete(),
WithoutFireplace(),
)

Здесь NewHouse - это метод-конструктор. WithConcrete и WithFireplace - это параметры, передаваемые конструктору для изменения возвращаемого значения.
Вскоре мы увидим, почему WithConcrete и WithFireplace называются «функциональными» опциями и чем они полезны по сравнению с обычными аргументами функций.

Определение конструктора​

Во-первых, давайте определим структуру, для которой мы создадим опции:
type House struct {
Material string
HasFireplace bool
Floors int
}

// `NewHouse` это метод-конструктор для `*House`
func NewHouse() *House {
const (
defaultFloors = 2
defaultHasFireplace = true
defaultMaterial = "wood"
)

h := &House{
Material: defaultMaterial,
HasFireplace: defaultHasFireplace,
Floors: defaultFloors,
}

return h
}

Дом может быть построен из определенного материала, может иметь определенное количество этажей и, при желании, может содержать камин. Конструктор NewHouse возвращает нам указатель на структуру House по умолчанию с некоторыми значениями по умолчанию для всех его атрибутов. Обычно нам нужно сначала построить дом, а затем изменить его значения, если нам нужен другой вариант. С помощью функциональных опций мы можем предоставить список модификаций самого конструктора.

Определение функциональных опций​

Давайте определим тип функции, который принимает указатель на House:
type HouseOption func(*House)

Это сигнатура наших функциональных опций. Давайте определим некоторые функциональные параметры, которые изменяют экземпляр *House:
func WithConcrete() HouseOption {
return func(h *House) {
h.Material = "concrete"
}
}

func WithoutFireplace() HouseOption {
return func(h *House) {
h.HasFireplace = false
}
}

Каждая из вышеперечисленных функций является «конструктором параметров» и возвращает другую функцию, которая принимает *House в качестве аргумента и ничего не возвращает. Мы видим, что возвращенные функции изменяют предоставленный экземпляр *House. Мы даже можем добавить аргументы в конструкторы параметров, чтобы изменить возвращаемые параметры:
func WithFloors(floors int) HouseOption {
return func(h *House) {
h.Floors = floors
}
}

Это вернет опцию, которая изменяет количество этажей в доме в соответствии с аргументом, заданным конструктору опции WithFloors.

Добавление функциональных опций в наш конструктор​

Теперь мы можем включить функциональные опции в наш конструктор:
// NewHouse теперь принимает слайс опций в качестве аргументов
func NewHouse(opts ...HouseOption) *House {
const (
defaultFloors = 2
defaultHasFireplace = true
defaultMaterial = "wood"
)

h := &House{
Material: defaultMaterial,
HasFireplace: defaultHasFireplace,
Floors: defaultFloors,
}

// Применяем в цикле каждую опцию
for _, opt := range opts {
// Call the option giving the instantiated
// *House as the argument
opt(h)
}

// вернуть измененный экземпляр House
return h
}

Конструктор теперь принимает список любого количества аргументов функциональных опций, каждый элемент которого затем применяет к экземпляру *House перед его возвратом. Возвращаясь к первому примеру, теперь мы можем понять, что делают эти опции:
h := NewHouse(
WithConcrete(),
WithoutFireplace(),
WithFloors(3),
)

Вы можете сами попробовать пример кода здесь!!!

Преимущества использования паттерна "функциональные опции"​

Теперь, когда мы увидели, как реализовать шаблон опций, давайте посмотрим, почему мы хотели бы использовать функциональные опции.
Наявность
Вместо того, чтобы изменять *House следующим образом:
h := NewHouse()
h.Material = "concrete"

мы можем явно указать строительный материал в самом конструкторе:
h := NewHouse(WithConcrete())

Это помогает нам четко указать строковое обозначение материала. Предыдущий пример позволяет пользователю делать опечатки и раскрывает внутреннюю часть экземпляра *House.

Расширяемость​

В случае, если мы действительно хотим предоставить пользователю расширяемость, мы всегда можем предоставить аргументы нашему конструктору опций. Например, поскольку у нас может быть любое количество этажей в нашем доме, возможность добавления этажей в дом может быть создана путем предоставления аргумента для количества этажей:
h := NewHouse(WithFloors(4))

Порядок аргументов​

При использовании функциональных опций порядок их не имеет значения. Это дает нам большую гибкость по сравнению с обычными аргументами функции (которые должны быть в правильном порядке). Кроме того, мы можем предоставить любое количество вариантов. При использовании функций с обычными аргументами мы должны предоставить все аргументы:
/*
Как выглядел бы `NewHouse`, если бы мы использовали обычные аргументы
функции. Нам всегда нужно было бы предоставлять все три аргумента,
неважно какие.
*/
h := NewHouse("concrete", 5, true)

Итак, теперь, когда вы узнали о функциональных опциях, можете ли вы придумать, как можно улучшить уже существующий код? Видите ли вы какие-либо другие варианты использования или предостережения в использовании, которые я упустил? Дайте мне знать в комментариях! Вот еще несколько шаблонов проектирования Golang, которые я рассмотрел:
Статья от Dave Cheney про функциональные опции: https://dave.cheney.net/2014/10/17/functional-options-for-friendly-apis

 
Сверху