Menu
Coddy logo textTech

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

Часть раздела Объектно-ориентированное программирование путешествия по 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.

challenge icon

Задание

Легко

Давайте создадим настраиваемое подключение к базе данных, используя паттерн Функциональные опции (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", maxConnections 10, timeout 30. Примените все предоставленные опции после установки значений по умолчанию.

    Добавьте метод 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")
}
quiz iconПроверьте себя

В этом уроке есть небольшой тест. Начните урок, чтобы ответить на вопросы и сохранить прогресс.

Все уроки раздела Объектно-ориентированное программирование