Функциональные  опции в Go: реализация шаблона опций в Golang
Tags: go, patterns
Published: 2021-08-30 09:51:10

# Функциональные опции
> Оригинал статьи автора Soham Kamani [здесь](https://www.sohamkamani.com/golang/options-pattern/).

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

```go
h := NewHouse(
  WithConcrete(),
  WithoutFireplace(),
)
```
Здесь NewHouse - это метод-конструктор. WithConcrete и WithFireplace - это параметры, передаваемые конструктору для изменения возвращаемого значения.

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

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

Во-первых, давайте определим структуру, для которой мы создадим опции:
```go
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:

```go
type HouseOption func(*House)
```

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

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

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

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

```go
func WithFloors(floors int) HouseOption {
	return func(h *House) {
		h.Floors = floors
	}
}
```

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

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

Теперь мы можем включить функциональные опции в наш конструктор:

```go
// 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 перед его возвратом. Возвращаясь к первому примеру, теперь мы можем понять, что делают эти опции:

```go
h := NewHouse(
  WithConcrete(),
  WithoutFireplace(),
  WithFloors(3),
)
```

Вы можете сами попробовать пример кода [здесь](https://play.golang.org/p/wfgJV9UWtB-)!!!

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

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

#### Наявность

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

```go
h := NewHouse()
h.Material = "concrete"
```

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

```go
h := NewHouse(WithConcrete())
```

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

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

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

```go
h := NewHouse(WithFloors(4))
```

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

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

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

Итак, теперь, когда вы узнали о функциональных опциях, можете ли вы придумать, как можно улучшить уже существующий код? Видите ли вы какие-либо другие варианты использования или предостережения в использовании, которые я упустил? Дай мне знать в комментариях! Вот еще несколько шаблонов проектирования Golang, которые [я](https://www.sohamkamani.com/)  рассмотрел:

-   [Паттерн команда](https://www.sohamkamani.com/golang/command-pattern/)
-   [Паттерн фабрика](https://www.sohamkamani.com/golang/2018-06-20-golang-factory-patterns/)
-   [Перечисления](https://www.sohamkamani.com/golang/enums/)