Generics в Golang: пример рефакторинга

Generics в Golang: пример рефакторинга
Дженерики — это способ написания кода, который не зависит от конкретных применяемых типов. Функции и типы теперь могут быть написаны для любого набора типов.
С дженериками в язык добавляются три важные функциональные возможности:
  • Типы как параметры для функций и типов.
  • Определение интерфейсных типов как наборов типов, в том числе типов без методов.
  • Выведение типа, когда во многих случаях типы аргументов при вызове функции опускаются.
Это очень полезный инструмент, который может быть использован в рефакторинге кода.
Я не буду в данной статье останавливаться на теоретических аспектах генериков, а покажу один из примеров рефакторинга, который я применил с использованием generics. В качестве примера - мой проект EasyList.
В данном проекте я использовал интересный аспект в generics, который, по моему мнению, может придать истинную ценность, когда у нас есть целый набор методов с одинаковым поведением.
type Folder struct {
ID int64 `jsonapi:"primary,folders"`
Name string `jsonapi:"attr,name"`
Icon string `jsonapi:"attr,icon"`
Version int32 `json:"-"`
Order int32 `jsonapi:"attr,order"`
UserId sql.NullInt64 `json:"-"`
CreatedAt time.Time `jsonapi:"attr,created_at,iso8601"`
UpdatedAt time.Time `jsonapi:"attr,updated_at,iso8601"`
Lists Lists `jsonapi:"relation,lists,omitempty"`
}
 
type Item struct {
ID int64 `jsonapi:"primary,items"`
UserId int64 `json:"-"`
ListId int64 `jsonapi:"attr,list_id"`
Name string `jsonapi:"attr,name"`
Description string `jsonapi:"attr,description"`
Quantity int32 `jsonapi:"attr,quantity"`
QuantityType string `jsonapi:"attr,quantity_type"`
Price float32 `jsonapi:"attr,price"`
IsStarred bool `jsonapi:"attr,is_starred"`
IsDone bool `jsonapi:"attr,is_done"`
File string `jsonapi:"attr,file"`
Order int32 `jsonapi:"attr,order"`
Version int32 `json:"-"`
CreatedAt time.Time `jsonapi:"attr,created_at,iso8601" json:"created_at" time_format:"sql_datetime"`
UpdatedAt time.Time `jsonapi:"attr,updated_at,iso8601" json:"updated_at" time_format:"sql_datetime"`
List *List `jsonapi:"relation,list,omitempty"`
}
Здесь приведены два конкретных типа, которые представляют собой данные, необходимые для вставки в базу. Я напиcал отдельную функцию для каждого типа, которая осуществляет операцию вставки в БД.
 
func (i ItemModel) Insert(item *Item) error {
var query = "INSERT INTO items (user_id, list_id, name, description, quantity, quantity_type, price, is_starred, file, version, `order`, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, NOW(), NOW())"

lastOrder, err := i.GetLastItemOrderForUser(item.UserId, item.ListId)
if err != nil {
return err
}

var args = []any{item.UserId, item.ListId, item.Name, item.Description, item.Quantity, item.QuantityType, item.Price, item.IsStarred, item.File, lastOrder}
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
result, err := i.DB.ExecContext(ctx, query, args...)
if err != nil {
return err
}
id, err := result.LastInsertId()
if err != nil {
return err
}
item.ID = id
item.Order = int32(lastOrder)
return nil
}
 
func (f FolderModel) Insert(folder *Folder) error {
var query = "INSERT INTO folders (user_id, name, icon, version, `order`, created_at, updated_at) VALUES (?, ?, ?, ?, ?, NOW(), NOW())"
 
lastOrder, err := f.GetLastFolderOrderForUser(folder.UserId.Int64)
if err != nil {
return err
}
 
var args = []any{folder.UserId, folder.Name, folder.Icon, folder.Version, lastOrder}
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
result, err := f.DB.ExecContext(ctx, query, args...)
if err != nil {
return err
}
id, err := result.LastInsertId()
if err != nil {
return err
}
folder.ID = id
folder.Order = int32(lastOrder)
return nil
}
 
 
Если мы посмотрим на указанный пример внимательнее, мы увидим лишь небольшую разницу между двумя имплементациями. Все они делают одинаковую операцию - вставляют данные в базу, получают id записи. Если назначение id могло быть переведено в generics, я мог бы вынести повторяющийся блок кода в отдельную функцию.
 
Далее мы могли бы создать следующий тип:
 
type entities interface {
Folder | Item
}
 
Но ввиду ограничений в языке, мы не можем назначить в свойства новые данные, поэтому объявим другой интерфейс:
type Entities interface {
SetId(id int64)
}
 
И далее сделаем имплементацию в обоих структурах:
 
func (f *Folder) SetId(id int64) {
f.ID = id
}
 
Теперь мы можем написать новую функцию, которая будет сохранять данные в базу:
 
func Insert[T Entities](db *sql.DB, entity T, query string, args ...any) (T, error) {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
result, err := db.ExecContext(ctx, query, args...)
if err != nil {
return entity, err
}
id, err := result.LastInsertId()
if err != nil {
return entity, err
}
entity.SetId(id)

return entity, nil
}
 
Теперь наш метод сохранения папки стал намного легче:
 
func (f FolderModel) Insert(folder *Folder) error {
var query = "INSERT INTO folders (user_id, name, icon, version, `order`, created_at, updated_at) VALUES (?, ?, ?, ?, ?, NOW(), NOW())"

lastOrder, err := f.GetLastFolderOrderForUser(folder.UserId.Int64)
if err != nil {
return err
}

folder, err = Insert[*Folder](f.DB, folder, query, folder.UserId, folder.Name, folder.Icon, folder.Version, lastOrder)
if err != nil {
return err
}
folder.Order = int32(lastOrder)
return nil
}
 
Решение было бы намного проще и чище, если бы мы не использовали сеттеры, а назначали id непосредственно следующим образом:
entity.ID = id
 
Но go не позволяет пока нам этого сделать, поэтому используем обходной путь.
 
Таким образом, генерики дают языку программирования определённую лёгкость, позволяют упростить рефакторинг и писать функции с поведением в зависимости от используемых параметров.

Популярное

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

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

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

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

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

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

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

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

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

Telegram
@sergeyem
Telephone
+4915211100235