Golang - применяем паттерн Builder в разработке CRM.

Golang - применяем паттерн Builder в разработке CRM.

Когда мы проектируем API модулей, может возникнуть один вопрос: как будет выстраиваться логика работы с опциональными параметрами нашей структуры? Эффективное решение этой проблемы может существенно повысить удобство нашего API. В этой статье я приведу конкретный пример из моей практики в разработке CRM и как мне помог в этом вопросе паттерн Builder.

Рассмотрим один из модулей в CRM. У нас есть Счета, один из ключевых функционалов нашей системы, в котором присутствует определённая бизнес-логика. Модуль принимает различные поля - сумму, ответственного пользователя, массив товаров и прочее.

 

type Invoice struct {

 ID         string

 Subject string

 AssignedUser int

 CreateTime time.Time

 UpdatedTime time.Time

 LineItems  []item.LineItem

 Total      float64

}

 

func NewInvoice(id string, subject string, user int, lineItems []item.LineItem) (*Invoice, error) {

    // ...

}

 

Разработчики начали использовать нашу функцию NewInvoice и вроде все счастливы. Но наступил момент, когда клиенты попросили добавить новые параметры в структуру модуля - описание, размер скидки, ID контрагента. Причём последнее поле должно быть обязательным. Однако, мы понимаем, что добавление новых параметров в функцию сломает обратную совместимость, изменениям подвергнутся другие модули. Т.е. изменение одного параметра в функции приведёт к вынужденной переработке других частей системы.

Одновременно с этим, нам поступила задача, что поле ID должно быть обязательным, только если Total не является пустым.

А у поля AssignedUser должна быть своя логика:

  • Если пользователь не заполнен, используем пользователя по умолчанию.
  • Если передано отрицательное значение, выбрасываем ошибку валидации.
  • Если значение равно 0, используем случайно выбранного пользователя.
  • Во всех остальных случаях заполняем пользователя, который нам передал клиент.

Как мы можем сделать дружественный API для разработчиков? Давайте посмотрим в сторону паттерна Builder.

Паттерн Builder был описан в книге Банды Четырёх и он предоставлял гибкое решение в создании объектов. Процесс создания счёта изолирован от структуры самой по себе. Он требует отдельную структуру - InvoiceBuilder, который получает методы для конфигурирования и создания Счёта.

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

 

type Invoice struct {

 ID         *string

 Subject string

 AssignedUser *int

 CreateTime time.Time

 UpdatedTime time.Time

 LineItems  []item.LineItem

 Total      float64

}

 

type InvoiceBuilder struct {

 Invoice

}

 

func NewInvoiceBuilder() *InvoiceBuilder {

 return &InvoiceBuilder{

  Invoice: Invoice{

   CreateTime: time.Now(),

   UpdatedTime : time.Now(),

  },

 }

}

 

func (b *InvoiceBuilder) WithID(id string) *InvoiceBuilder {

 b.ID = &id

 return b

}

 

func (b *InvoiceBuilder) WithAssignedUser(id int) *InvoiceBuilder {

 b.ID = &id

 return b

}

 

func (b *InvoiceBuilder) WithLineItems(items []item.LineItem) *InvoiceBuilder {

 b.LineItems = &items

 b.Total = float64(len(items)) * 10.0 // Assume each item costs 10.0

 return b

}

 

func (b *InvoiceBuilder) Build() (*Invoice, error) {

 if b.ID == nil && b.Total < 1 {

  return nil, fmt.Errorf("ID can't be empty when amount is 0")

 }

 if len(b.LineItems) == 0 {

  return nil, fmt.Errorf("at least one line item should be there")

 }

 if b.AssignedUser == nil {

  b.AssignedUser = GetDefaultUser()

 } else {

  if *b.AssignedUser == 0 {

  b.AssignedUser = RandomUser()

  } else if *b.AssignedUser < 0 {

  return &Invoice{}, ValidationError("Assigned user is not correct")

  }

 }

 return &b.Invoice, nil

}

 

Структура InvoiceBuilder содержит в себе конфигурацию Счётов. Мы напиcали метод WithAssignedUser, для настройки связанного пользователя в счёте. Обычно подобная конфигурация методов возвращает нам сам Builder, чтобы мы могли использовать цепочку методов (например, builder.Foo("foo").Bar("bar")). Ключевым обычно является сам метод Build, который содержит в себе логику инициализации связанного пользователя и возвращает созданный счёт.

Не существует единственной возможной реализации паттерна Builder.

Например, некоторые могут предпочесть подход, при котором логика определения конечного значения пользователя находится в методе WithAssignedUser, а не в методе Build. Задача данной статьи - представить обзор паттерна Builder, а не рассматривать все возможные вариации.

Использовать этот паттерн мы можем следующим образом:

func main() {

 builder := NewInvoiceBuilder()

 invoice, err := builder.WithID("1").WithLineItems([]item.LineItem{...}).Build()

 

 if err != nil {

  fmt.Println("Failed to build invoice:", err)

  return

 }

 fmt.Println(invoice)

}

 

Первое, что мы должны сделать - это создать InvoiceBuilder и использовать его в дальнейшем для назначения опциональных полей, таких как ID, товары, пользователя. Затем, он вызывает метод Build, который производит валидацию и проверку на ошибки. Если всё хорошо, мы возвращаем готовую структуру счёта.

Этот подход делает управление полями структуры намного удобнее. Нам не обязательно передавать ссылку на пользователя или идентификатор, эти методы принимают строку или integer. Нам также не обязательно передавать сумму, Builder рассчитает её автоматом. При этом мы также можем продолжать использовать старый метод NewInvoice, если другие разработчики захотят использовать конфигурацию по умолчанию.

Однако в этом подходе есть и определённые недостатки, в частности, возникает вопрос обработки ошибок. В других языках программирования, где выбрасывается исключение, методы Builder, такие как WithLineItems могут выбросить исключение, если значение неверное. Если мы хотим по прежнему выстраивать цепочку методов при вызове Builder, функция не может вернуть ошибку. Таким образом, нам нужно переносить вопросы валидации в метод Build. Если клиент может передавать несколько параметров, но мы хотим обрабатывать только поле ID, это делает вопрос обработки ошибок сложнее.

 

В статье мы рассмотрели пример применения паттерна Builder для структуры Invoice в CRM системе. Паттерн позволяет удобно работать с опциональными параметрами и создавать объекты с различными конфигурациями, делая API более понятным и удобочитаемым для разработчиков.

Преимущества использования паттерна Builder:

  • Упрощение конфигурации объектов: Разделение процесса создания объектов от их структуры делает код более чистым и понятным, особенно когда объекты имеют множество опциональных параметров.
  • Обеспечение обратной совместимости: Добавление новых параметров в структуру объекта не ломает обратную совместимость, так как новые параметры могут быть добавлены через новые методы в Builder, не затрагивая существующие вызовы.
  • Читаемый код: Цепочка методов Builder делает код API более читаемым, позволяя легко следить за тем, какие параметры были установлены.
  • Централизованная обработка ошибок: Метод Build позволяет проводить валидацию параметров и обрабатывать ошибки в одном месте, что упрощает управление ошибками.

Недостатки:

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

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

Популярное

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

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

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

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

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

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

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

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

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

Telegram
@sergeyem
Telephone
+4915211100235