Паттерн функциональных опций
Часть раздела Объектно-ориентированное программирование путешествия по GO на Coddy — урок 79 из 107.
При создании структур с множеством необязательных полей конфигурации функции-конструкторы могут стать громоздкими. Паттерн «Функциональные параметры» (Functional Options Pattern) элегантно решает эту проблему, используя функции для настройки объектов.
Рассмотрим структуру сервера с несколькими необязательными настройками. Вместо конструктора с множеством параметров мы определим тип опции и функции, которые возвращают опции:
type Server struct {
host string
port int
timeout int
}
type Option func(*Server)
func WithPort(port int) Option {
return func(s *Server) {
s.port = port
}
}
func WithTimeout(timeout int) Option {
return func(s *Server) {
s.timeout = timeout
}
}Конструктор принимает вариативный список опций и применяет каждую из них:
func NewServer(host string, opts ...Option) *Server {
s := &Server{
host: host,
port: 8080, // по умолчанию
timeout: 30, // по умолчанию
}
for _, opt := range opts {
opt(s)
}
return s
}Теперь создание серверов становится читаемым и гибким:
// Использовать значения по умолчанию
s1 := NewServer("localhost")
// Настроить определенные опции
s2 := NewServer("localhost", WithPort(3000), WithTimeout(60))Этот паттерн хорош тем, что вызывающая сторона указывает только то, что ей нужно, значения по умолчанию четко определены в одном месте, а добавление новых опций не ломает существующий код. Он широко используется в библиотеках Go, таких как grpc и zap.
Задание
ЛегкоДавайте создадим настраиваемое подключение к базе данных, используя паттерн Функциональные опции (Functional Options Pattern)! Этот паттерн отлично подходит, когда у вас много необязательных настроек с разумными значениями по умолчанию — именно то, что нужно для подключения к базе данных.
Вы организуете свой код в двух файлах:
database.go: Определите тип подключения к базе данных и функции опций.Создайте структуру
DBConnectionсо следующими неэкспортируемыми полями:host(string)port(int)username(string)password(string)maxConnections(int)timeout(int) - в секундах
Определите тип
Optionкак функцию, которая изменяет*DBConnection.Создайте следующие функции опций, которые возвращают
Option:WithPort(port int)WithCredentials(username, password string)WithMaxConnections(max int)WithTimeout(seconds int)
Создайте конструктор
NewDBConnection(host string, opts ...Option), который возвращает*DBConnection. Установите следующие значения по умолчанию: порт5432, имя пользователя"admin", пароль"secret", maxConnections10, timeout30. Примените все предоставленные опции после установки значений по умолчанию.Добавьте метод
ConnectionString(), который возвращает информацию о подключении в следующем формате:[username]:[password]@[host]:[port] (max:[maxConnections], timeout:[timeout]s)main.go: Создавайте подключения к базе данных с различными конфигурациями.Прочитайте хост, затем прочитайте количество применяемых опций. Для каждой опции прочитайте тип опции и её значение(я):
port, за которым следует номер портаcredentials, за которыми следуют имя пользователя и пароль (две строки)maxconn, за которым следует максимальное количество соединенийtimeout, за которым следует время ожидания в секундах
Создайте
DBConnectionс указанными опциями и выведите его строку подключения.
Будут предоставлены следующие входные данные:
- Строка 1: Имя хоста
- Строка 2: Количество опций
- Последующие строки: Тип опции и значение(я)
Например, для входных данных:
localhost
0Ваш вывод должен быть:
admin:secret@localhost:5432 (max:10, timeout:30s)И для входных данных:
db.example.com
2
port
3306
credentials
root
mypasswordВаш вывод должен быть:
root:mypassword@db.example.com:3306 (max:10, timeout:30s)И для входных данных:
production.server
4
port
5433
credentials
dbuser
securepass123
maxconn
50
timeout
60Ваш вывод должен быть:
dbuser:securepass123@production.server:5433 (max:50, timeout:60s)Обратите внимание, как вызывающая сторона указывает только те опции, которые ей нужны — для неуказанных настроек используются разумные значения по умолчанию. В этом и заключается элегантность паттерна Функциональные опции!
Шпаргалка
Паттерн Functional Options (Функциональные параметры) предоставляет элегантный способ настройки структур с множеством необязательных полей, используя функции вместо длинных списков параметров конструктора.
Определите тип Option как функцию, которая изменяет указатель на вашу структуру:
type Server struct {
host string
port int
timeout int
}
type Option func(*Server)Создайте функции опций, которые возвращают замыкания Option:
func WithPort(port int) Option {
return func(s *Server) {
s.port = port
}
}
func WithTimeout(timeout int) Option {
return func(s *Server) {
s.timeout = timeout
}
}Конструктор принимает вариативный список опций, устанавливает значения по умолчанию, а затем применяет каждую опцию:
func NewServer(host string, opts ...Option) *Server {
s := &Server{
host: host,
port: 8080, // по умолчанию
timeout: 30, // по умолчанию
}
for _, opt := range opts {
opt(s)
}
return s
}Использование получается чистым и гибким — вызывающая сторона указывает только то, что ей нужно:
// Использование значений по умолчанию
s1 := NewServer("localhost")
// Настройка конкретных опций
s2 := NewServer("localhost", WithPort(3000), WithTimeout(60))Преимущества: значения по умолчанию централизованы, вызывающая сторона указывает только необходимое, а добавление новых опций не ломает существующий код.
Попробуйте сами
package main
import (
"bufio"
"fmt"
"os"
"strconv"
"strings"
)
func main() {
reader := bufio.NewReader(os.Stdin)
// Чтение хоста
host, _ := reader.ReadString('\n')
host = strings.TrimSpace(host)
// Чтение количества опций
numOptionsStr, _ := reader.ReadString('\n')
numOptions, _ := strconv.Atoi(strings.TrimSpace(numOptionsStr))
// Сбор опций
var opts []Option
for i := 0; i < numOptions; i++ {
optionType, _ := reader.ReadString('\n')
optionType = strings.TrimSpace(optionType)
switch optionType {
case "port":
portStr, _ := reader.ReadString('\n')
port, _ := strconv.Atoi(strings.TrimSpace(portStr))
// TODO: Добавьте опцию WithPort в opts
_ = port // Удалите эту строку при реализации
case "credentials":
username, _ := reader.ReadString('\n')
username = strings.TrimSpace(username)
password, _ := reader.ReadString('\n')
password = strings.TrimSpace(password)
// TODO: Добавьте опцию WithCredentials в opts
_, _ = username, password // Удалите эту строку при реализации
case "maxconn":
maxStr, _ := reader.ReadString('\n')
maxConn, _ := strconv.Atoi(strings.TrimSpace(maxStr))
// TODO: Добавьте опцию WithMaxConnections в opts
_ = maxConn // Удалите эту строку при реализации
case "timeout":
timeoutStr, _ := reader.ReadString('\n')
timeout, _ := strconv.Atoi(strings.TrimSpace(timeoutStr))
// TODO: Добавьте опцию WithTimeout в opts
_ = timeout // Удалите эту строку при реализации
}
}
// TODO: Создайте DBConnection, используя NewDBConnection с host и opts
// TODO: Выведите строку подключения, используя метод ConnectionString()
_ = opts // Удалите эту строку при реализации
fmt.Println("TODO: Print connection string here")
}
В этом уроке есть небольшой тест. Начните урок, чтобы ответить на вопросы и сохранить прогресс.
Все уроки раздела Объектно-ориентированное программирование
1Основы ООП в Go
Внешние файлыРабочее пространство и модули GoПакеты и импортыЭкспортируемые и неэкспортируемые именаВведение в ООП в GoСтруктуры как классыОпределение методов структурПолучатели-указатели и получатели-значенияИнициализация структурФункции-конструкторыИтоги — Простой калькулятор4Интерфейсы
Введение в интерфейсыНеявная реализацияИнтерфейс как контрактПустой интерфейс (any)Утверждение типаПереключатель типовКомпозиция интерфейсовИнтерфейсы Stringer и ErrorПовторение: Калькулятор фигур7Инкапсуляция
Экспортируемые и неэкспортируемые поляИнкапсуляция на уровне пакетовГеттеры и сеттерыСокрытие информации в GoИтоги — Записи о студентах10Обобщения (Generics) (Go 1.18+)
Введение в GenericsПараметры типовОграничения типовОбобщенные структурыОбходной путь для обобщенных методовИтоги — Обобщенная коллекция13Паттерны проектирования. Часть 1
Введение в паттерны проектированияПаттерн SingletonПаттерн FactoryПаттерн Abstract FactoryПаттерн ObserverПаттерн Strategy2Глубокое погружение в типы и структуры
Базовые и составные типыОпределение пользовательских типовТеги структурАнонимные структурыВложенные структурыНулевые значения и значения по умолчаниюПовторение — Контактная книга5Композиция вместо наследования
Почему в Go нет наследованияОсновы встраивания структурПродвижение методовВстраивание нескольких структурВстраивание против агрегацииЗатенение встроенных методовИтоги — Иерархия сотрудников8Обработка ошибок и ООП
Интерфейс errorПользовательские типы ошибокОбертывание ошибок (fmt.Errorf)Sentinel-ошибкиerrors.Is() и errors.As()Panic, Defer и RecoverИтоги — Парсер файлов11Стандартная библиотека и ООП
io.Reader и io.Writersort.InterfaceИнтерфейс fmt.Stringerencoding/json со структурамиИнтерфейс http.HandlerПовторение: модели REST API14Паттерны проектирования. Часть 2
Паттерн КомандаПаттерн АдаптерПаттерн ДекораторПаттерн Шаблонный методПаттерн СостояниеПаттерн КомпоновщикMiddleware как Декоратор3Указатели и память
Основы указателей в GoУказатели на структурыПередача по значению и по ссылкеФункция new()Сборка мусора в GoПовторение: Конструктор связного списка6Полиморфизм в Go
Полиморфизм через интерфейсыУтиная типизация в GoПравила реализации интерфейсовПолиморфные коллекцииВнедрение зависимостейИтоги — Обработчик платежей9Конкурентность и ООП
Основы горутинКаналы и взаимодействиеБуферизованные и небуферизованные каналыОператор selectsync.Mutex и sync.RWMutexsync.WaitGroupПроектирование потокобезопасных структурПовторение — Worker Pool12Продвинутые концепции ООП
Паттерн функциональных опцийПаттерн Builder в GoЦепочки вызовов методовПсевдонимы и определения типовОсновы рефлексииОбзор генерации кода