Menu
Coddy logo textTech

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

Часть раздела Объектно-ориентированное программирование путешествия по GO на Coddy. Урок 79 из 107.

При создании структур с множеством необязательных полей конфигурации функции-конструкторы могут стать громоздкими. Паттерн функциональных опций элегантно решает эту проблему, используя функции для настройки объектов.

Рассмотрим структуру сервера с несколькими необязательными настройками. Вместо конструктора с множеством параметров мы определяем тип option и функции, которые возвращают options:

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
    }
}

Конструктор принимает вариативный список options и применяет каждый из них:

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))

Этот шаблон особенно эффективен, потому что вызывающий код указывает только то, что ему нужно, defaults ясно определены в одном месте, а добавление новых options не нарушает существующий код. Он широко используется в библиотеках Go, таких как grpc и zap.

challenge icon

Задание

Легко

Давайте создадим настраиваемое подключение к database с использованием шаблона функциональных опций! Этот шаблон особенно удобен, когда у вас много необязательных настроек с разумными значениями по умолчанию — именно то, что нужно подключению к database.

Вы организуете код в двух файлах:

  • database.go: Определите тип подключения к database и function опций.

    Создайте структуру DBConnection со следующими неэкспортируемыми полями:

    • host (string)
    • port (int)
    • username (string)
    • password (string)
    • maxConnections (int)
    • timeout (int) — в секундах

    Определите тип Option как function, которая изменяет *DBConnection.

    Создайте следующие function опций, возвращающие Option:

    • WithPort(port int)
    • WithCredentials(username, password string)
    • WithMaxConnections(max int)
    • WithTimeout(seconds int)

    Создайте конструктор NewDBConnection(host string, opts ...Option), который возвращает *DBConnection. Установите следующие значения по умолчанию: port 5432, username "admin", password "secret", maxConnections 10, timeout 30. Примените все переданные опции после установки значений по умолчанию.

    Добавьте метод ConnectionString(), который возвращает информацию о подключении в следующем формате:

    [username]:[password]@[host]:[port] (max:[maxConnections], timeout:[timeout]s)
  • main.go: Создайте подключения к database с различными конфигурациями.

    Сначала прочитайте host, затем количество применяемых опций. Для каждой опции прочитайте её тип и значение или значения:

    • port, затем номер port
    • credentials, затем username и password (две строки)
    • maxconn, затем количество max connections
    • timeout, затем timeout в секундах

    Создайте DBConnection с указанными опциями и выведите его строку подключения.

Будут предоставлены следующие входные данные:

  • Строка 1: имя host
  • Строка 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)

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

Попробуйте сами

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Проверьте себя

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

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

Потренируйтесь самостоятельно: Онлайн-компилятор Go