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, Wordpress. Предлагаю варианты сотрудничества, которые помогут вам воспользоваться преимуществами внешнего опыта, оптимизировать затраты и снизить риски. Полная прозрачность всех этапов работы и учёт временных затрат. Оплачивайте только рабочие часы разработки после приемки задачи. Экономьте на платежах по его содержанию разработчика в штате. Возможно заключение договора по ИП. С чего начать, чтобы нанять профессионального разработчика на full-time? Просто заполните форму!

Telegram
@sergeyem
Telephone
+4915211100235
Email