Обзор реализаций конструкторов объектов в 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 в конструкторе.