Всем привет!
Эта статья является выжимкой моего опыта. Возможно для кого-то это очевидные вещи, тем не менее, считаю, что она может мыть полезна начинающим и не только.
Для работы с базами данных в го есть несколько подходов от стандартного "ручного", до удобных, таких как sqlx, GORM, sqlc... Список можно продолжать дальше.
При разработке очередного ПО я познакомился с sqlc (https://sqlc.dev/) и его подход мне понравился: на основе sql запросов создается полноценная обертка над бд - чем не песня, но как ее грамотно использовать в соответствии с принципами SOLID и Го подходом?
Смотрим через SOLID
S — Single Responsibility (Принцип единственной ответственности)
Идея: модуль должен делать что-то одно.
Пусть в usecase у нас будет
db.Querier, однако Usecase должен отвечать за логику, поэтому в этом случае на него будет возложена миссия осуществления маппинга типов БД в бизнес-логику, а это уже не проходит по SRP, но на практике часто встречаетсяЕсли у нас есть отдельный слой "репозиторий", миссия по приведению типов будет выполнять он, тогда в usecase будет исключительно бизнес-логика. В этом варианте SRP соблюдается строже
Вывод: с точки зрения SRP ручной слой выглядит предпочтительнее, но в Go нередко жертвуют формальным соблюдением принципа ради простоты.
O — Open‑Closed (Принцип открытости‑закрытости)
Идея: программные сущности должны быть открыты для расширения, но закрыты для модификации.
db.Querier. Интерфейс генерируется sqlc и содержит все методы. При добавлении нового запроса вы не меняете существующий интерфейс (он перегенерируется), но расширяете набор вызовов в use case. Однако сам интерфейс жёстко привязан к набору запросов в БД: подменить его на другую реализацию без перегенерации сложно.Ручной репозиторий. Вы сами определяете только те методы, которые реально нужны домену. Интерфейс можно расширять новыми методами, не меняя существующие. Реализация репозитория может быть дополнена (например, кешированием) без изменения контракта.
Что имеем в итоге: дополнительный слой репозиторий обеспечивает большую гибкость, потому, что вы контролируете контракт, а не подстраиваетесь под сгенерированный набор методов
L — Liskov Substitution (Принцип подстановки Барбары Лисков)
Идея: объекты в программе должны быть заменяемы экземплярами их подтипов без потери корректности работы программы. То есть у нас должна быть возможность без боли заменить один модуль другим, реализующим такой же контракт.
Здесь оба подхода идентичны, поддерживают LSP, поскольку являются интерфейсами
I — Interface Segregation (Принцип разделения интерфейсов)
Идея: объекты не должны зависеть от методов, которые они не используют. Дпугими словами, не нужно давать зависимому объекту больше, чем действителельно нужно
db.Querier. sqlc генерирует один большой интерфейс, включающий все методы всех запросов проекта. Если ваш use case использует толькоGetExperimentByID, он всё равно зависит от интерфейса с методамиCreateAtmosphereProfile,CreatePreparedMetaи т. д. Это явное нарушение ISP.Ручной репозиторий. Вы проектируете интерфейс специфично для use case — только те методы, которые нужны этому бизнес‑сценарию. Это идеальное соблюдение ISP.
Что в итоге: по критерию ISP ручной репозиторий явно выигрывает, но в Го эта проблема решается довольно просто через определение локальных интерфейсов с нужным набором методов на стороне потребителя (правило интерфейсы появляются там, где используются)
D — Dependency Inversion (Принцип инверсии зависимостей)
Идея: зависимости должны строиться относительно абстракций, а не деталей реализации. Другими словами, если объект A зависит от B, то эта зависимость должна осуществляться через интерфейс, а не сам тип, Так мы сможем сильнее развязать модули.
Здесь снова оба подхода соответствуют DIP. Но в случае дополнительного слоя usecase не будет знать о типах SQL.
Вывод
Принцип |
| Дополнительный слой репозитория |
SRP | Use case совмещает бизнес‑логику и конвертацию типов | Конвертация выносится в отдельный слой |
OCP | Интерфейс жестко привязан к запросам БД | Контракт легко расширяется при необходимости |
LSP | подстановка работает | подстановка работает |
ISP | Здесь "жиный" интерфейс, но можно решить через определение локальных интерфейсоы | точечные итерфейсы |
DIP | Зависит от абстракции | то же, но при этом модель не знает ничего о внешних деталях (типах SQL) |
Го подход
Как я уже упоминал, в Го не принято плодить абстракции (разрабатывать. интерфейсы)
«Accept interfaces, return structs» - идиома Го, как раз и говорит об этом. Нужно использовать интерфейсы (например в конструкторах типов)
Меньше кода — лучше. здесь тоже использование sqlc оправдно, автоматически генерирует обвязку БД
С этой точки зрения использование в usecase
db.Querierне только оправдано, но и является идиоматическим подходом (сокращает проект на много строк кода)
Однако что делать с преобразованием типов в usecase? Если проект небольшой, или нет строгих правил относительно подходов (не DDD), можно использовать функции - конвертеры, которые преобразуют результат выполнения запросов в доменные типы и вызывать их сразу после получения данных в usecase.
И напоследок, когда все-таки дополнительный слой репозиторий оправдвн - это если мы пишем библиотеку, где модель не должна ничего знать о БД.
В своей работе я сначала принялся городить дополнительный слой репозитория поверх db.Querier, однако в скором времени пришлось от этого отказаться, поскольку этот интерфейс ничего кроме приведения тиипов не делал, но при добавлении нового запроса в БД мне приходилось дополнительно проксировать вызов нового метода.
