Обзор реализаций конструкторов объектов в Go
Functional Options
Источники:
1. https://dave.cheney.net/2014/10/17/functional-options-for-friendly-apis
2. https://rednafi.com/go/configure_options/
Начнём с наиболее распространённого в Go подхода — Functional Options.
type User struct { // Обязательный параметр Email string // Опциональные параметры Name string Public bool } type UserOption func(*User) error func WithName(name string) UserOption { return func(u *User) error { u.Name = name return nil } } func WithPublicFlag(u *User) error { u.Public = true return nil } func NewUser(email string, options ...UserOption) (*User, error) { // Инициализируем объект с обязательными полями. // Здесь же можно задать дефолтные значения для опциональных полей. u := &User{ Email: email, } for _, option := range options { if err := option(u); err != nil { return nil, err } } return u, nil } func main() { user, _ := NewUser("ivan@example.com") fmt.Printf("%+v\n", user) // &{Email:ivan@example.com Name: Public:false} user, _ = NewUser("ivan@example.com", WithName("Ivan Ivanich"), WithPublicFlag) fmt.Printf("%+v\n", user) // &{Email:ivan@example.com Name:Ivan Ivanich Public:true} }
- IDE не показывает доступные опции конструктора, нужно явно просматривать структуру UserOption
- На практике, With-опций вызываются с префиксом названия пакета, что может выглядеть чуждо
Configurable Object
Источники:
1. https://rednafi.com/go/dysfunctional_options_pattern/
Реализация с отдельной структурой для хранения опциональных параметров:
type User struct { // Обязательный параметр Email string UserOptions } // Опциональные параметры type UserOptions struct { Name string Public bool } func (userOption UserOptions) WithName(name string) UserOptions { userOption.Name = name return userOption } func (userOption UserOptions) WithPublic() UserOptions { userOption.Public = true return userOption } func NewUser(email string, options UserOptions) *User { // Описываем объект с обязательными полями. // Здесь же можно задать дефолтные значения для опциональных полей. u := &User{ Email: email, UserOptions: options, } return u } func main() { user := NewUser("ivan@example.com", UserOptions{}) fmt.Printf("%+v\n", user) // &{Email:ivan@example.com UserOptions:{Name: Public:false}} user = NewUser("ivan@example.com", UserOptions{}.WithName("Ivan Ivanich").WithPublic()) fmt.Printf("%+v\n", user) // &{Email:ivan@example.com UserOptions:{Name:Ivan Ivanich Public:true}} }
Выявляется менее наглядная работа с ошибками, валидацию в With-опции встроить не получится, только описать на уровне NewUser().
Также сигнатура конструктора обязывает передавать options, даже в неиспользуемом случае. Здесь можно упомянуть альтернативу — Builder pattern, реализующую цепочку методов, мутирующих основную структуру:
type User struct { // Обязательный параметр Email string // Опциональные параметры Name string Public bool } func (u *User) WithName(name string) *User { u.Name = name return u } func (u *User) WithPublicFlag() *User { u.Public = true return u } func NewUser(email string) *User { return &User{ Email: email, } } func main() { user := NewUser("ivan@example.com") fmt.Printf("%+v\n", user) // &{Email:ivan@example.com Name: Public:false} user = NewUser("ivan@example.com").WithName("Ivan Ivanich").WithPublicFlag() fmt.Printf("%+v\n", user) // &{Email:ivan@example.com Name:Ivan Ivanich Public:true} }
Однако в ней полностью исключается возврат ошибок. И допускается модификация приватных полей, после создания объекта.
Также важным недостатком, объединяющим оба примера, является смешивание методов конструктора и основной структуры в автодополнении IDE.
Структура Options
Источники:
1. https://asankov.dev/post/different-ways-to-initialize-go-structs
Вместо дополнительного кода ради Configurable Object, напрашивается использование простой структуры:
type User struct { // Обязательный параметр Email string UserOptions } // Опциональные параметры type UserOptions struct { Name string Public bool } func NewUser(email string, options UserOptions) *User { u := &User{ Email: email, UserOptions: options, } return u } func main() { user := NewUser("ivan@example.com", UserOptions{}) fmt.Printf("%+v\n", user) // &{Email:ivan@example.com UserOptions:{Name: Public:false}} user = NewUser("ivan@example.com", UserOptions{ Name: "Ivan Ivanich", Public: true, }) fmt.Printf("%+v\n", user) // &{Email:ivan@example.com UserOptions:{Name:Ivan Ivanich Public:true}} }
Если мы имеем дело с приватными полями, поможет прокси-структура:
type User struct { email, name string public bool } // Опциональные параметры type UserOptions struct { Name string Public bool } func NewUser(email string, options UserOptions) *User { u := &User{ email: email, } // Такой метод сравнения работает если структура состоит только из comparable-полей // - https://stackoverflow.com/questions/28447297/how-to-check-for-an-empty-struct if options == (UserOptions{}) { return u } if options.Name != "" { u.name = options.Name } if options.Public != false { u.public = options.Public } return u } func main() { user := NewUser("ivan@example.com", UserOptions{}) fmt.Printf("%+v\n", user) // &{Email:ivan@example.com UserOptions:{Name: Public:false}} user = NewUser("ivan@example.com", UserOptions{ Name: "Ivan Ivanich", Public: true, }) fmt.Printf("%+v\n", user) // &{Email:ivan@example.com UserOptions:{Name:Ivan Ivanich Public:true}} }
Из недостатков, сохраняется обязательность аргумента options в конструкторе.