DEV Community

Petrovichev Sergey
Petrovichev Sergey

Posted on

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

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

Оригинал статьи автора Soham Kamani здесь.

В этом посте рассказывается о том, какие функциональные опции есть в Go и как мы можем использовать шаблон "опции" для их реализации. Функциональные опции имеют форму дополнительных аргументов функции, которые расширяют или изменяют ее поведение. Вот пример, в котором используются функциональные параметры для создания новой структуры House:

h := NewHouse(
  WithConcrete(),
  WithoutFireplace(),
)
Enter fullscreen mode Exit fullscreen mode

Здесь 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
}
Enter fullscreen mode Exit fullscreen mode

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

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

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

type HouseOption func(*House)
Enter fullscreen mode Exit fullscreen mode

Это сигнатура наших функциональных опций. Давайте определим некоторые функциональные параметры, которые изменяют экземпляр *House:

func WithConcrete() HouseOption {
    return func(h *House) {
        h.Material = "concrete"
    }
}

func WithoutFireplace() HouseOption {
    return func(h *House) {
        h.HasFireplace = false
    }
}
Enter fullscreen mode Exit fullscreen mode

Каждая из вышеперечисленных функций является «конструктором параметров» и возвращает другую функцию, которая принимает *House в качестве аргумента и ничего не возвращает. Мы видим, что возвращенные функции изменяют предоставленный экземпляр *House. Мы даже можем добавить аргументы в конструкторы параметров, чтобы изменить возвращаемые параметры:

func WithFloors(floors int) HouseOption {
    return func(h *House) {
        h.Floors = floors
    }
}
Enter fullscreen mode Exit fullscreen mode

Это вернет опцию, которая изменяет количество этажей в доме в соответствии с аргументом, заданным конструктору опции 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
}
Enter fullscreen mode Exit fullscreen mode

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

h := NewHouse(
  WithConcrete(),
  WithoutFireplace(),
  WithFloors(3),
)
Enter fullscreen mode Exit fullscreen mode

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

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

Теперь, когда мы увидели, как реализовать шаблон опций, давайте посмотрим, почему мы хотели бы использовать функциональные опции.

Наявность

Вместо того, чтобы изменять *House следующим образом:

h := NewHouse()
h.Material = "concrete"
Enter fullscreen mode Exit fullscreen mode

мы можем явно указать строительный материал в самом конструкторе:

h := NewHouse(WithConcrete())
Enter fullscreen mode Exit fullscreen mode

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

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

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

h := NewHouse(WithFloors(4))
Enter fullscreen mode Exit fullscreen mode

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

При использовании функциональных опций порядок их не имеет значения. Это дает нам большую гибкость по сравнению с обычными аргументами функции (которые должны быть в правильном порядке). Кроме того, мы можем предоставить любое количество вариантов. При использовании функций с обычными аргументами мы должны предоставить все аргументы:

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

Итак, теперь, когда вы узнали о функциональных опциях, можете ли вы придумать, как можно улучшить уже существующий код? Видите ли вы какие-либо другие варианты использования или предостережения в использовании, которые я упустил? Дай мне знать в комментариях! Вот еще несколько шаблонов проектирования Golang, которые я рассмотрел:

Discussion (0)