Golang - functional options pattern

Golang - functional options pattern

В предыдущей статье мы обсуждали паттерн Builder. Там мы заметили такой недостаток, что мы не можем производить валидацию при назначении параметров. В результате мы лишаемся возможности выстраивать цепочку методов. Давайте рассмотрим другой более интересный паттерн - Functional options. Существуют различные имплементации с небольшими отличиями, но основная идея заключается в следующем:

  • Создаётся неэкспортируемая структура, содержашая конфигурацию: options.
  • Каждая опция - это функция, которая возвращает один и тот же тип: type Option func(options *options) error. Например, функция WithEmail, которая принимает строку адреса электронной почты в качестве аргумента и возвращает тип Option, который представляет из себя функцию с инструкцией о том, как обновлять структуру.

Ниже я приведу конкретный пример из CRM системы, которая позволяет создать новый контакт:

 

package main

 

import (

 "errors"

 "fmt"

 "net/mail"

 "strings"

)

 

type Contact struct {

 ID       string

 Name     string

 Phone    string

 Email    string

 Company  *CompanyInfo

 Address  string

 Notes    string

}

 

type CompanyInfo struct {

 Name     string

 Position string

}

 

type ContactOptions struct {

 Phone    string

 Email    string

 Company  *CompanyInfo

 Address  string

 Notes    string

}

 

type ContactOption func(*ContactOptions) error

 

func WithPhone(phone string) ContactOption {

 return func(options *ContactOptions) error {

  if len(phone) < 10 {

   return errors.New("Phone number must be at least 10 digits")

  }

  options.Phone = phone

  return nil

 }

}

 

func WithEmail(email string) ContactOption {

 return func(options *ContactOptions) error {

  _, err := mail.ParseAddress(email)

  if err != nil {

   return errors.New("Invalid email address")

  }

  options.Email = email

  return nil

 }

}

 

func WithCompany(name string, position string) ContactOption {

 return func(options *ContactOptions) error {

  if strings.TrimSpace(name) == "" || strings.TrimSpace(position) == "" {

   return errors.New("Both company name and position must be provided")

  }

  options.Company = &CompanyInfo{

   Name:     name,

   Position: position,

  }

  return nil

 }

}

 

func WithAddress(address string) ContactOption {

 return func(options *ContactOptions) error {

  options.Address = address

  return nil

 }

}

 

func WithNotes(notes string) ContactOption {

 return func(options *ContactOptions) error {

  options.Notes = notes

  return nil

 }

}

 

func CreateContact(id string, name string, opts ...ContactOption) (*Contact, error) {

 options := ContactOptions{}

 

 for _, opt := range opts {

  err := opt(&options)

  if err != nil {

   return nil, err

  }

 }

 

 contact := &Contact{

  ID:       id,

  Name:     name,

  Phone:    options.Phone,

  Email:    options.Email,

  Company:  options.Company,

  Address:  options.Address,

  Notes:    options.Notes,

 }

 

 return contact, nil

}

 

func main() {

 contact, err := CreateContact("1", "John Doe", WithPhone("1234567890"), WithEmail("john.doe@gmail.com"), WithCompany("ABC Corp", "Manager"), WithAddress("123 Main St"), WithNotes("Important client"))

 if err != nil {

  fmt.Println("Failed to create contact:", err)

  return

 }

 fmt.Println(contact)

}

 

Здесь мы видим множество методов, наподобие Builder, которые наполняют нашу структуру необходимыми данными. Например, WithNotes, WithAddress, WithCompany - все они возвращают замыкание. Т.е. анонимную функцию, которая связывает переменные вне своей области видимости. В нашем случае это заметки, компания, адрес. Функция соответствует типу Option и содержит в себе логику валидации данных. Для каждого поля в этом случае нам нужно написать свою функцию, которая по стандартам начинается с префикса With. И эта функция должна содержать в себе ту же самую логику: валидация данных при необходимости и обновление структуры.

Давайте посмотрим на функцию CreateContact. Мы передаём опции как переменные аргументы. Следовательно, для изменения структуры, мы должны произвести итерацию над этими опциями.

Всё начинается с того, что мы создаём пустую структуру

options := ContactOptions{}

Затем, мы производим итерацию по каждому аргументу Option, выполняем её как функцию, эта функция, в свою очередь, производит изменение структуры.

 

 for _, opt := range opts {

  err := opt(&options)

  if err != nil {

   return nil, err

  }

 }

 

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

 

contact := &Contact{

  ID:       id,

  Name:     name,

  Phone:    options.Phone,

  Email:    options.Email,

  Company:  options.Company,

  Address:  options.Address,

  Notes:    options.Notes,

 }

 

 Потому что CreateContact принимает опции в качестве аргумента, разработчики могут теперь очень удобно использовать наше API, передавая различные опциональные параметры, при этом идентафикатор и имя у нас будут обязательны:

contact, err := CreateContact("1", "John Doe", WithPhone("1234567890"), WithEmail("john.doe@gmail.com"), WithCompany("ABC Corp", "Manager"), WithAddress("123 Main St"), WithNotes("Important client"))

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

Этот паттерн является functional options. Он обеспечивает удобный и дружественный к API способ работы с опциями. Хотя паттерн builder может быть приемлемым вариантом, он имеет некоторые незначительные недостатки, которые делают паттерн functional options идиоматическим способом решения этой проблемы в Go. Отметим также, что этот паттерн используется в различных библиотеках Go, таких как gRPC.

Популярное

Самые популярные посты

Как быть максимально продуктивным на удалённой работе?
Business

Как быть максимально продуктивным на удалённой работе?

Я запустил собственный бизнес и намеренно сделал всё возможное, чтобы работать из любой точки мира. Иногда я сижу с своём кабинете с большим 27-дюймовым монитором в своей квартире в г. Чебоксары. Иногда я нахожусь в офисе или в каком-нибудь кафе в другом городе.

Привет! Меня зовут Сергей Емельянов и я трудоголик
Business PHP

Привет! Меня зовут Сергей Емельянов и я трудоголик

Я программист. В душе я предприниматель. Я начал зарабатывать деньги с 11 лет, в суровые 90-е годы, сдавая стеклотару в местный магазин и обменивая её на сладости. Я зарабатывал столько, что хватало на разные вкусняшки.

Акция! Профессиональный разработчик CRM за 2000 руб. в час

Выделю время под ваш проект. Знания технологий Vtiger CRM, SuiteCRM, Laravel, Vue.js, Golang, React.js. Предлагаю варианты сотрудничества, которые помогут вам воспользоваться преимуществами внешнего опыта, оптимизировать затраты и снизить риски. Полная прозрачность всех этапов работы и учёт временных затрат. Оплачивайте только рабочие часы разработки после приемки задачи. Экономьте на платежах по его содержанию разработчика в штате. Возможно заключение договора по ИП. С чего начать, чтобы нанять профессионального разработчика на full-time? Просто заполните форму!

Telegram
@sergeyem
Telephone
+4915211100235