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

Оригинал статьи автора 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

Комментарии (4)


  1. JekaMas
    30.08.2021 13:05
    +2

    Странно, что нет ссылки на оригинал 2014 года

    https://dave.cheney.net/2014/10/17/functional-options-for-friendly-apis


    1. Ad_augusta_per_angusta Автор
      30.08.2021 13:10

      может быть потому, что это все таки другая статья


      1. JekaMas
        30.08.2021 13:11
        +2

        Другая, но изначальная относительно предложения этого паттерна в го.


        1. Ad_augusta_per_angusta Автор
          30.08.2021 13:15
          +1

          добавил в конец ссылку, спасибо за уточнение