Pull to refresh

Comments 45

А тулинг от такого как быстро суёт голову в песок?

С тулингом тут нет проблем. Вообще паттерн functional options старая штука,в go в коде разных проектов встречается.

А в чем же здесь проблема для тулинга? server := NewServer(WithPort(9090), WithLogs(true)) здесь WithLogs это ведь просто функция, тулинг прекрасно справляется. Или вы не про language server и всякие линтеры?

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

Ну это не проблема, функциональные опции в go называются одинаково, просто начинаете писать With и сразу все возможные варианты можете видеть.

А если мы пытаемся сконфигурировать несколько сущностей?

Покажите, пожалуйста, так непонятно

Представим, что нам одновременно нужно, условно, объект HTTP клиента, и HTTP сервер. А ещё GRPC сервер. Чтобы они работали вместе. И их надо друг за дружкой сконфигурировать. Нейминг с With не решает проблему выбора подходящих конфигураций.

Т.е. имеете в виду, что сделали структуру раз и передали в разные, условно, билдеры? Да, окей, такую проблему не решает никак

Проблема в том, чтобы тулинг позволил угадать, что ещё туда передать можно.

Скрытый текст

Подобный подход с опциями используется в GRPC и Goland хорошо справляется(всё из списка на скрине это валидные опции кроме tabnine, это AI), особенно если использовать комбинацию Shift+Ctrl+Space , чтобы автодополнение учитывало тип.

Строитель нарушает SRP.

Так же, по остальным принципам, внутреннее состояние из вне можно определять только при создании объекта, т.е. через конструктор.

И в чём же заключается это нарушение?

Отвечает за создание и конфигурацию объекта.

Если бизнес-логика создания объектов изменяется, вам придётся менять сам строитель, что увеличивает количество причин для изменения этого класса.

Это одна задача, а не две, потому что объект нельзя создать несконфигурированным.

И какую ещё причину изменения билдера вы видите, помимо изменения, э-э-э, бизнес-логики создания объекта?

Билдер может формировать промежуточное внутреннее представление, из которого потом целиком собирается объект. Совсем не обязательно чтобы задачей билдера было изменение собираемого объекта.

Но в таком случае было бы неплохо иметь интерфейс билдера во fluent стиле.

Вот только как потом понять какие параметры в конструктор передать можно?

И почему -бы просто не создать структуру serverOptions и передать ее в конструктор?
Вам ее так и так создавать надо, но в «традиционном варианте» есть описание структуры с дефолтными полями и комментариями.

Надо посмотреть какие параметры есть - прочитали описание.

Попытались добавить параметр которого нет - IDE/ компилятор вас тормознёт.

В итоге все- равно придётся параметры из конфиг файла читать - для этого же специально flags завезли.

Чтобы быстро и просто структуры из конфигуратор инициализировать.

flags - это для параметров командной строки.
В контейнерной теме принято через переменные окружения параметры задавать и там как ни крути получится `var := cmp.Or(os.Getenv(EnvName), EnvDefault)`

Но статья ни разу не про это.

А вот структуру передать в конструктор - добавляет нюансов с тем, что в этой структуре считать не инициализированными значениями (т.е. где применять дефолты). Самое замечательное это bool - там дефолт и одно из двух значений..... остается только *bool использовать, что бы значением nul указывать на то, что нужно проставить дефолт. Но, если так со всеми полями поступить то, код превратится в изрядную портянку однотипных проверок на nul.

добавляет нюансов с тем, что в этой структуре считать не инициализированными значениями (т.е. где применять дефолты)

Можно сделать для структуры опций конструктор, который будет устанавливать дефолтные значения, а прикладной код будет менять те, которые ему интересны:

// Библиотека

type serverOptions struct {
    Port       int
    Timeout    time.Duration
    EnableLogs bool
}

func NewServerOptions() *serverOptions {
    return &serverOptions{
        Port:       8080,
        Timeout:    60,
        EnableLogs: false,
    }
}

func NewServer(opts *serverOptions) *server {
    // ...
}

// Прикладной код

serverOptions := NewServerOptions()
serverOptions.Port = 9090
serverOptions.EnableLogs = true
server := NewServer(serverOptions)

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

// Библиотека

type ServerOptions struct {
    Port       int
    Timeout    time.Duration
    EnableLogs bool
}

type serverConfig func(*ServerOptions)

func NewServer(configure serverConfig) *server {
    options := &ServerOptions{
        Port:       8080,
        Timeout:    60,
        EnableLogs: false,
    }
    configure(options)

    return createServer(options)
}

func createServer(options *ServerOptions) *server {
    // ...
}

// Прикладной код

server := NewServer(func (options *ServerOptions) {
    options.Port = 9090
    options.EnableLogs = true
})

А в чем тут выигрыш по сравнению с функциональными опциями? По моему: те же яйца, вид сбоку.

Не надо писать по функции для каждого параметра

Как в такой конструкции можно гарантировать, что значение переменных опций не измениться после инициализации инстанса?

Клонировать структуру опций внутри NewServer. Или передавать в NewServer не указатель на структуру, а саму структуру.

Такой подход тоже имеет право на существование. Используйте наиболее подходящий.

А можно ещё проще

type Port int

func NewHTTPServer(options ...any) *HTTPServer {
  server := new(HTTPServer)

  for _, option := range options {
    switch typed := option.(type) {
      case int:
        server.port = typed
    }
  }
}

Лапши меньше, переносов контекстов меньше, функциональность 1 в 1.

Только возникает некоторое количество "НО"...
Если у меня больше одного параметра типа int?

Эм, и как понять какие именно опции он принимает?

Очевидно, почитать документацию... Ой

В приличном месте такое использование any никогда ревью не пройдёт. Посмотрите как надо: https://cs.opensource.google/go/go/+/refs/tags/go1.23.1:src/encoding/json/stream.go;l=289
И да, в вашем примере лапши будет гораздо больше, ведь вам же надо перебирать все возможные типы для всех возможных опций. В Go используя any вы теряете информацию о типе, но не получаете ничего. Если тип переменной известен - никогда не передавайте её как any, это просто не имеет смысла.

Спасибо за статью, сам натыкался на эти опции в библиотеках. Первое - начинаешь лазить по исходникам и искать как нужную опцию передать. Доки иногда не помогают т.к. в коде скудно с комментариями (док_стрингами точнее). Связь между опциями - тоже досталяет.... В результате мне как пользователю 2-й и 3-й минус перевешивают все плюсы. Уже пару раз они меня подталкивали поискать альтернативу библиотеке где используется этот паттерн.

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

пройдя путём знатного пердолинга

Или комбинируя вызовы родных функций конфигурации

Это в случае, если они есть. А если очень нужно задать значение приватного поля, но нет соответствующего сеттера - пердолинга не избежать.

Статья была как изменить одно значение в бооольшом конфиге и нам предложили передавать в конструктор миллион вызовов функций. Если параметров сотня то желаю автору удачи

Вот еще тривиальные способы:

server.Port(777).Timeout(30)

Server.set("port", 777)

server.Set("port",777).Set('"timeout",30).Set()

SetPort(server, port,777)

И даже так: cfg := server.cfg()

cfg.Port = 777

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

Тут нет "принципиально лучше". Люди разные) - Я предпочитаю читаемость и сеттеры

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

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

Можно, но слишком много движений и лишнего кода. С таким же успехом можно ничего не делать, кода будет меньше, код будет проще поддерживать.

А разве из отдельного пакета вы получите доступ к приватным свойствам сервера, ради модификации которых всё и затевалось?

Можно сделать их публичными. Затевалось всё не ради скрытых свойств сервера, а ради упрощения работы с большим количеством аргументов.

Можно положить код сервера в тот же пакет.

Неплохо, но я все равно предпочитаю fluent interface

Sign up to leave a comment.

Articles