[Проектирование и рефакторинг, Go] О репозиториях замолвите слово
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
В последнее время на хабре, и не только, можно наблюдать интерес GO сообщества к луковой/чистой архитектуре, энтерпрайз паттернам и прочему DDD. Читая статьи на данную тему и разбирая примеры кода, постоянно замечаю один момент — когда дело доходит до хранения сущностей предметной области — начинается изобретение своих велосипедов, которые зачастую еле едут. Код вроде бы состоит из набора паттернов: сущности, репозитории, value object’ы и так далее, но кажется, что они для того там “чтобы были”, а не для решения поставленных задач.
В данной статье я бы хотел не только показать, что, по моему мнению, не так с типичными DDD-примерами на GO, но также продемонстрировать собственную ORM для реализации персистентности доменных сущностей.
Дисклеймер.
Прежде чем приступить к теме статьи, есть несколько моментов, которые необходимо осветить:
- Данная статья о том, как писать приложения с богатой бизнес логикой. Сервисы на GO зачастую такими не являются, не нужно применять к ним DDD’шные подходы.
- Исходя из того, что я не являюсь ярым фанатом ORM, считаю, что зачастую использование этой технологии попросту излишне. Кроме того, необходимо брать ее лишь в том случае, когда вы отдаете себе отчет в ее целесообразном использовании в проекте, иначе вы попросту используете инструмент для галочки, “для того, чтоб был”.
- Оппонировать я буду подходам из этой статьи и (раз, два) примерам проектов.
- Я буду иллюстрировать свои мысли на примере типичного приложения — wish list.
А теперь — можно начинать.
Энтерпрайз паттерны в GO и что с ними не так.
Речь здесь пойдет о таких паттернах как: репозиторий, сущность, агрегат и способах их приготовления. Для начала, давайте разберемся, что же это за паттерны такие. Я не буду придумывать определения в стиле “от себя”, а буду использовать слова признанных мастеров: Ерика Эванса и Мартина Фаулера.
Сущность.
Начнем с сущности. По Эвансу:
Entity: Objects that have a distinct identity that runs through time and different representations. You also hear these called "reference objects".
Ну тут вроде бы ничего сложного, сущности в GO можно реализовать, используя структуры. Вот типичный пример:
type Wish struct {
id sql.NullInt64
content string
createAt time.Time
}
Агрегат.
А вот про этот шаблон как то незаслуженно забывают, особенно в контексте GO. А забывают, между прочим, абсолютно зря. Чуть позже мы разберем почему агрегаты намеренно не используются в различных примерах DDD проектов на GO. Итак, определение по Эвансу:
Cluster the entities and value objects into aggregates and define boundaries around each. Choose one entity to be the root of each aggregate and control all access to the objects inside the boundary through the root
Рассмотрим пример aggregate root:
type User struct {
id sql.NullInt64
name string
email Email
wishes []*Wish
friends []*User
}
Тут у нас агрегат “пользователь”, который включает в себя сущность User, а также набор желаний этого пользователя и набор друзей.
Ну пока все ок, скажете вы, разве есть какие-то проблемы с реализацией? Я считаю — есть, перейдем к репозиториям.
Репозиторий.
A Repository mediates between the domain and data mapping layers, acting like an in-memory domain object collection. Client objects construct query specifications declaratively and submit them to Repository for satisfaction. Objects can be added to and removed from the Repository, as they can from a simple collection of objects, and the mapping code encapsulated by the Repository will carry out the appropriate operations behind the scenes. Conceptually, a Repository encapsulates the set of objects persisted in a data store and the operations performed over them, providing a more object-oriented view of the persistence layer. Repository also supports the objective of achieving a clean separation and one-way dependency between the domain and data mapping layers.
Определение емкое, поэтому выделю основные моменты:
- Репозиторий абстрагирует конкретное хранилище — ну обычно на этом в GO проектах все и заканчивается. Да, конечно, это важно, например, при написании юнит тестов, но это далеко не вся суть репозиториев.
- Репозитории создаются только для aggregate root. Это исходит из определения агрегата, потому как все, что мы делаем в доменном слое, должно быть сделано через корень агрегата.
- Репозиторий предоставляет интерфейс схожий с интерфейсом коллекции.
Давайте рассмотрим типичный для GO пример репозитория и как он используется:
type UserRepository interface {
Save(*User)
Update(*User)
FindById(*User, error)
}
user1 := &User{}
userRepo.Save(user1) // Save
user2, _ := userRepo.FindById(1) // FindById
user2.Name = “new user”
userRepo.Update(user2 ) // Update
Вопросы, которые сразу же возникают для таких репозиториев:
- должен ли FindById загружать коллекции друзей и желаний (что ведет к расходам на дополнительные запросы)? Что, если для решения конкретной бизнес задачи эти коллекции мне не нужны?
- Должен ли Update каждый раз проверять список друзей и желаний — не изменилось ли там что-то? Как мне отслеживать эти изменения?
- Как быть с транзакционностью? В одном кейсе я хочу сделать Save одного пользователя, а в другом кейсе я хочу, чтобы в транзакции было два Save’a. Очевидно, в таком случае управление транзакцией должно быть вне метода Save. Как в данном случае избежать протечки инфраструктурной логики в домен?
Обычно в примерах GO кода такие вопросы принято “обходить” всеми возможными способами:
- делаем репозитории и для агрегатов и для сущностей, ведь чем меньше (и проще) структура — тем проще ее сохранить
- сознательно избегаем кейсов, где наша реализация начинает протекать в бизнес логику, так же поступаем с кейсами, реализация которых будет выглядеть крайне громоздкой
- берем базу данных в которую можно положить агрегат целиком (например mongo) и делаем вид, что других хранилищ не бывает и транзакции нам не нужны
А как насчет схожести интерфейса репозитория к интерфейсу GO-коллекций? Ниже представлен пример работы с коллекцией пользователей реализованной через slice:
var users []*User
user1 := &User{}
users = append(users, user1) // Save
user2 = users[1] // FindById
user2.Name = “new user” // Update
Как видите, эквивалент методу Update для слайса users просто не требуется, потому, что изменения внесенные в агрегат User применяются сразу же.
Обобщим проблемы, которые не дают DDD-like GO коду быть достаточно выразительным, тестируемым и вообще классным:
- Типичные GO-репозитории создаются для всего подряд, агрегат, сущность может value object — who cares? Причина — нет ORM или других инструментов позволяющая “грамотно” работать сразу с графом объектов.
- Типичные GO-репозитории не стараются походить на коллекции. В результате страдает выразительность и тестируемость кода. Знание о базе данных может протечь в бизнес логику. Причина — вновь упираемся в отсутствие подходящей ORM. Можно, опять же, все делать руками, но как показывает практика — это слишком неудобно.
D3 ORM. Зачем оно мне?
Хм, похоже что написать свою ORM не самая плохая идея, что я и сделал. Рассмотрим как же она помогает решить описанные выше проблемы. Для начала, как выглядит сущность Wish и агрегат User:
//d3:entity
//d3_table:lw_wish
type Wish struct {
id sql.NullInt64 `d3:"pk:auto"`
content string
createAt time.Time
}
//d3:entity
//d3_table:lw_user
type User struct {
id sql.NullInt64 `d3:"pk:auto"`
name string `d3:"column:name"`
email Email `d3:"column:email"`
wishes *entity.Collection `d3:"one_to_many:<target_entity:Wish,join_on:user_id,delete:cascade>"`
friends *entity.Collection `d3:"many_to_many:<target_entity:User,join_on:u1_id,reference_on:u2_id,join_table:lw_friend>"`
}
Как видите изменений не много, но они есть. Во первых — появились аннотации, с помощью которых описывается мета-информация (имя таблицы в БД, маппинг полей структуры на поля в БД, индексы). Во вторых — вместо обычных для GO коллекций — slice’ов D3 ORM накладывает требования на использование своих коллекций. Данное требование исходит из желания иметь фичу lazy/eager loading. Можно сказать, что, если не брать в расчет кастомные коллекции, то описание бизнес сущностей делается полностью нативными средствами.
Ну что ж, а теперь перейдем непосредственно к тому, как выглядят работа с репозиториями в D3ORM:
userRepo, _:= d3orm.MakeRepository(&domain.User{})
userRepo.Persists(ctx, user1) // Save
user2, _ := userRepo.FindOne(ctx, userRepo.Select().AndWhere("id", "=", 1)) // FindById
user2.Name = “new user” // Update
Итого получаем решение которое, на сколько это возможно, повторяет интерфейс встроенных в GO коллекций. С одной маленькой ремаркой: после того, как мы выполнили все манипуляции, необходимо синхронизировать изменения с базой данных:
orm.Session(ctx).Flush()
Если вы работали с такими инструментами как: hybernate или doctrine то, для вас это не будет неожиданностью. Так же для вас не должно быть неожиданностью то, что вся работа выполняется в рамках логических транзакций — сессий. Для удобства работы с сессиями в D3 ORM есть ряд функций, которые позволяют положить и вынуть их из контекста.
Разберем еще некоторые примеры кода для демонстрации тех или иных фич:
- lazy loading, в данном примере запрос на извлечение из БД желаний пользователя будет создан и выполнен в момент непосредственного обращения к коллекции (в последней строке)
u, _ := userRepo.FindOne(ctx, userRepo.Select().AndWhere("id", "=", 1)) // будет сгенерирован запрос только для таблицы lw_user
wishes := u.wishes.ToSlice() // cгенерируется запрос для таблицы lw_wish
- transactions — D3 ORM использует концепцию UnitOfWork или другими словами транзакции на уровне приложения. Все изменения накапливаются пока не будет вызван Flush(). Кроме того транзакцией можно управлять вручную, объединяя несколько Flush’ей в одну транзакцию
userRepo.Persists(ctx, user1)
userRepo.Persists(ctx, user2)
orm.Session(ctx).Flush() // стандартное поведение - при вызове Flush создается физическая транзакция, в рамках которой выполняется два insert’a
session := orm.Session(ctx)
session.BeginTx() // переводим в ручной режим управления транзакцией
userRepo.Persists(ctx, user1)
userRepo.Persists(ctx, user2)
session.Flush() // в ручном режиме тут не будет сгенерировано запросов к базе
userRepo.Persists(ctx, user3)
session.Flush()
session.CommitTx() // на этой строчке будет сгенерирована транзакция в рамках которой выполняется три insert’a
- при вызове Persists сохраняются все объекты от корневого (то есть граф объектов). При этом запросы в базу данных на вставку/обновление генерируются только для тех, которые действительно изменились
Подробно о том, как работать с ORM, есть документация, а также демо проект. Краткий список фич:
- кодогенерация вместо рефлексии
- автогенерация схемы базы данных на основе сущностей
- «один к одному», «один ко многим» и «многие ко многим» связи между сущностями
- lazy/eager загрузка связей
- query builder
- загрузка связей в одном запросе к базе (используется join)
- кэш сущностей
- каскадное удаление и обновление связанных сущностей
- application-level transactions (UnitOfWork)
- DB transactions
- поддерживается UUID
А зачем оно вам?
Резюмируя, чем вам может быть полезна D3 ORM:
- у вас много бизнес логики и вы хотите: чтобы ваш код был как можно ближе к языку доменной области и чтобы все инварианты и вообще весь доменный слой был покрыт юнит тестами
В противном случае не могу советовать использовать D3 ORM.
А еще бы хотел описать случаи, где, по моему мнению, использовать любую ORM плохая идея:
- если вам действительно важна производительность и вы боритесь за каждую аллокацию
- если ваше приложение выполняет в основном READ операции. Ну право дело, для этого у нас есть отличный инструмент — SQL, зачем нам что-то другое?
- если ваше приложение тонкий клиент к базе данных. Зачем вводить ненужные абстракции?
Заключение.
Надеюсь данной статьей мне удалось хотя бы немного поставить под сомнение типичный GO-style написания бизнес логики. Кроме того, я постарался показать и альтернативу этому подходу. В любом случае решать, как писать код, вам, ну что ж, удачи в этом нелегком деле!
===========
Источник:
habr.com
===========
Похожие новости:
- В Google News прекращён приём сайтов, использующих внешние ссылки в статьях
- [Разработка веб-сайтов, Python, Django] Что происходит, когда вы выполняете manage.py test? (перевод)
- [Программирование, Совершенный код, C++, Проектирование и рефакторинг] Наследование реализации в С++. Реальная история (перевод)
- [Анализ и проектирование систем, Совершенный код, Проектирование и рефакторинг, API] Чего ждать при работе с API: 5 (не)обычных проблем при интеграции приложений
- [Разработка игр, Godot] Модули и расширения для Godot 3, ссылки и краткий обзор существующих
- [Разработка игр, Прототипирование, Дизайн игр, Godot] Микрокосм, демоверсия
- [Контент-маркетинг, Управление продуктом, Управление медиа, Брендинг, Социальные сети и сообщества] Как работает управление репутацией в интернете
- [Программирование, Проектирование и рефакторинг, Разработка игр] Как мы пришли к реактивному связыванию в Unity3D
- [Разработка под Linux, Разработка на Raspberry Pi, Гаджеты, Компьютерное железо] Ubuntu 20.10 «Groovy Gorilla» отдельно фокусируется на Raspberry Pi
- [Анализ и проектирование систем, Программирование, Проектирование и рефакторинг, Управление персоналом, Управление разработкой] Человечная декомпозиция работы
Теги для поиска: #_proektirovanie_i_refaktoring (Проектирование и рефакторинг), #_go, #_go, #_orm, #_ddd, #_proektirovanie_i_refaktoring (
Проектирование и рефакторинг
), #_go
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 20:35
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
В последнее время на хабре, и не только, можно наблюдать интерес GO сообщества к луковой/чистой архитектуре, энтерпрайз паттернам и прочему DDD. Читая статьи на данную тему и разбирая примеры кода, постоянно замечаю один момент — когда дело доходит до хранения сущностей предметной области — начинается изобретение своих велосипедов, которые зачастую еле едут. Код вроде бы состоит из набора паттернов: сущности, репозитории, value object’ы и так далее, но кажется, что они для того там “чтобы были”, а не для решения поставленных задач. В данной статье я бы хотел не только показать, что, по моему мнению, не так с типичными DDD-примерами на GO, но также продемонстрировать собственную ORM для реализации персистентности доменных сущностей. Дисклеймер. Прежде чем приступить к теме статьи, есть несколько моментов, которые необходимо осветить:
А теперь — можно начинать. Энтерпрайз паттерны в GO и что с ними не так. Речь здесь пойдет о таких паттернах как: репозиторий, сущность, агрегат и способах их приготовления. Для начала, давайте разберемся, что же это за паттерны такие. Я не буду придумывать определения в стиле “от себя”, а буду использовать слова признанных мастеров: Ерика Эванса и Мартина Фаулера. Сущность. Начнем с сущности. По Эвансу: Entity: Objects that have a distinct identity that runs through time and different representations. You also hear these called "reference objects".
type Wish struct {
id sql.NullInt64 content string createAt time.Time } Агрегат. А вот про этот шаблон как то незаслуженно забывают, особенно в контексте GO. А забывают, между прочим, абсолютно зря. Чуть позже мы разберем почему агрегаты намеренно не используются в различных примерах DDD проектов на GO. Итак, определение по Эвансу: Cluster the entities and value objects into aggregates and define boundaries around each. Choose one entity to be the root of each aggregate and control all access to the objects inside the boundary through the root
type User struct {
id sql.NullInt64 name string email Email wishes []*Wish friends []*User } Тут у нас агрегат “пользователь”, который включает в себя сущность User, а также набор желаний этого пользователя и набор друзей. Ну пока все ок, скажете вы, разве есть какие-то проблемы с реализацией? Я считаю — есть, перейдем к репозиториям. Репозиторий. A Repository mediates between the domain and data mapping layers, acting like an in-memory domain object collection. Client objects construct query specifications declaratively and submit them to Repository for satisfaction. Objects can be added to and removed from the Repository, as they can from a simple collection of objects, and the mapping code encapsulated by the Repository will carry out the appropriate operations behind the scenes. Conceptually, a Repository encapsulates the set of objects persisted in a data store and the operations performed over them, providing a more object-oriented view of the persistence layer. Repository also supports the objective of achieving a clean separation and one-way dependency between the domain and data mapping layers.
Давайте рассмотрим типичный для GO пример репозитория и как он используется: type UserRepository interface {
Save(*User) Update(*User) FindById(*User, error) } user1 := &User{} userRepo.Save(user1) // Save user2, _ := userRepo.FindById(1) // FindById user2.Name = “new user” userRepo.Update(user2 ) // Update Вопросы, которые сразу же возникают для таких репозиториев:
Обычно в примерах GO кода такие вопросы принято “обходить” всеми возможными способами:
А как насчет схожести интерфейса репозитория к интерфейсу GO-коллекций? Ниже представлен пример работы с коллекцией пользователей реализованной через slice: var users []*User
user1 := &User{} users = append(users, user1) // Save user2 = users[1] // FindById user2.Name = “new user” // Update Как видите, эквивалент методу Update для слайса users просто не требуется, потому, что изменения внесенные в агрегат User применяются сразу же. Обобщим проблемы, которые не дают DDD-like GO коду быть достаточно выразительным, тестируемым и вообще классным:
D3 ORM. Зачем оно мне? Хм, похоже что написать свою ORM не самая плохая идея, что я и сделал. Рассмотрим как же она помогает решить описанные выше проблемы. Для начала, как выглядит сущность Wish и агрегат User: //d3:entity
//d3_table:lw_wish type Wish struct { id sql.NullInt64 `d3:"pk:auto"` content string createAt time.Time } //d3:entity //d3_table:lw_user type User struct { id sql.NullInt64 `d3:"pk:auto"` name string `d3:"column:name"` email Email `d3:"column:email"` wishes *entity.Collection `d3:"one_to_many:<target_entity:Wish,join_on:user_id,delete:cascade>"` friends *entity.Collection `d3:"many_to_many:<target_entity:User,join_on:u1_id,reference_on:u2_id,join_table:lw_friend>"` } Как видите изменений не много, но они есть. Во первых — появились аннотации, с помощью которых описывается мета-информация (имя таблицы в БД, маппинг полей структуры на поля в БД, индексы). Во вторых — вместо обычных для GO коллекций — slice’ов D3 ORM накладывает требования на использование своих коллекций. Данное требование исходит из желания иметь фичу lazy/eager loading. Можно сказать, что, если не брать в расчет кастомные коллекции, то описание бизнес сущностей делается полностью нативными средствами. Ну что ж, а теперь перейдем непосредственно к тому, как выглядят работа с репозиториями в D3ORM: userRepo, _:= d3orm.MakeRepository(&domain.User{})
userRepo.Persists(ctx, user1) // Save user2, _ := userRepo.FindOne(ctx, userRepo.Select().AndWhere("id", "=", 1)) // FindById user2.Name = “new user” // Update Итого получаем решение которое, на сколько это возможно, повторяет интерфейс встроенных в GO коллекций. С одной маленькой ремаркой: после того, как мы выполнили все манипуляции, необходимо синхронизировать изменения с базой данных: orm.Session(ctx).Flush()
Если вы работали с такими инструментами как: hybernate или doctrine то, для вас это не будет неожиданностью. Так же для вас не должно быть неожиданностью то, что вся работа выполняется в рамках логических транзакций — сессий. Для удобства работы с сессиями в D3 ORM есть ряд функций, которые позволяют положить и вынуть их из контекста. Разберем еще некоторые примеры кода для демонстрации тех или иных фич:
u, _ := userRepo.FindOne(ctx, userRepo.Select().AndWhere("id", "=", 1)) // будет сгенерирован запрос только для таблицы lw_user
wishes := u.wishes.ToSlice() // cгенерируется запрос для таблицы lw_wish
userRepo.Persists(ctx, user1)
userRepo.Persists(ctx, user2) orm.Session(ctx).Flush() // стандартное поведение - при вызове Flush создается физическая транзакция, в рамках которой выполняется два insert’a session := orm.Session(ctx) session.BeginTx() // переводим в ручной режим управления транзакцией userRepo.Persists(ctx, user1) userRepo.Persists(ctx, user2) session.Flush() // в ручном режиме тут не будет сгенерировано запросов к базе userRepo.Persists(ctx, user3) session.Flush() session.CommitTx() // на этой строчке будет сгенерирована транзакция в рамках которой выполняется три insert’a
Подробно о том, как работать с ORM, есть документация, а также демо проект. Краткий список фич:
А зачем оно вам? Резюмируя, чем вам может быть полезна D3 ORM:
В противном случае не могу советовать использовать D3 ORM. А еще бы хотел описать случаи, где, по моему мнению, использовать любую ORM плохая идея:
Заключение. Надеюсь данной статьей мне удалось хотя бы немного поставить под сомнение типичный GO-style написания бизнес логики. Кроме того, я постарался показать и альтернативу этому подходу. В любом случае решать, как писать код, вам, ну что ж, удачи в этом нелегком деле! =========== Источник: habr.com =========== Похожие новости:
Проектирование и рефакторинг ), #_go |
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 20:35
Часовой пояс: UTC + 5