Комментарии 30
Менеджер транзакций это про микросервисы? Скажите, го не имеет смысла использовать без микросервисов? Они глубоко в паттернах гошки
Менеджер транзакций это про микросервисы?
Менеджер транзакций — это абстракция, которая позволяет работать с ACID-транзакциями вне репозитория. В данной статье рассматривается как антипаттерн, поскольку может привести к проблемам, о которых идёт речь в тексте статьи.
Скажите, го не имеет смысла использовать без микросервисов? Они глубоко в паттернах гошки
В статье не рассматривается выбор между микросервисной и монолитной архитектурами, так как это более высокоуровневый вопрос. Основное внимание уделяется построению архитектуры самого сервиса на уровне кодовой базы: слои, компоненты и их взаимодействие.
Если говорить вне контекста статьи, то Go можно использовать как для монолитов, так и для микросервисов. Выбор архитектурного стиля зависит не от языка программирования, а от требований к разделению зон ответственности.
Не покидает ощущение попытки сделать из того, что вполне себе может оставаться простым - сложным, "по книжкам, как деды завещали". Возможно, ощущение ошибочное, но если перед всеми другими призмами смотреть на код через призму KIS(s) - он заиграет совсем другими красками (но это не точно)
Не покидает ощущение попытки сделать из того, что вполне себе может оставаться простым - сложным, "по книжкам, как деды завещали"
Старался не использовать заезженные иллюстрации со слоями, но и выдумывать свою терминологию не решился. Заранее подозревал, что после прочтения статьи может показаться, будто у меня на столе лежит жёлтая книжечка Мартина вместо Библии, но это не так :)
Возможно, ощущение ошибочное, но если перед всеми другими призмами смотреть на код через призму KIS(s) - он заиграет совсем другими красками (но это не точно)
По моему мнению, описанный подход и рекомендации идеологически соонаправлены с принципом KISS, так как конечная цель — снижение сложности. В частности, это достигается за счёт изоляции бизнес-логики, отказа от перегруженных service в пользу простых usecase и уменьшения связанности системы.
Согласен, что выстраивание архитектуры — это не бесплатный процесс, но на личном опыте убедился: в продуктовых командах, работающих с бизнесом, рано или поздно встаёт вопрос о поддерживаемости и масштабировании системы.
У меня не было опыта работы в инфраструктурных командах, но предполагаю, что для "строгих" инженерных задач все эти слои и абстракции действительно могут оказаться избыточными.
И всё же описанный в статье подход однозначно избыточно книжный/формальный. На практике можно и нужно срезать некоторые углы, которые хоть формально и нарушают книжные рекомендации, но при этом не создают проблем в реальных проектах и при этом заметно упрощают работу. Например:
Слои usecase и domain в большинстве проектов нет смысла разделять. Тестирование логики usecase на моках domain (и вообще отдельно от логики domain) не даёт никаких плюшек, это просто излишнее усложнение.
Учитывая автоматическую генерацию моков зачастую удобнее сделать один "жирный" интерфейс с десятком-двумя методов чем делать два десятка интерфейсов по одному методу. Это даёт возможность сделать один интерфейс для всего слоя usecase+domain и ещё один для всего слоя repository. Такие интерфейсы сильно повышают наглядность при чтении и анализе кода - сразу виден полный список всех операций как с БД так и с бизнес-логикой.
Транзакции действительно должны быть полностью внутри конкретного метода репозитория, но при этом нет острой необходимости обязательно сохранять связь "один метод репо - одна модель". Т.е. в примере из статьи не обязательно создавать модель-агрегат каждый раз когда нужно передать несколько моделей в один метод репо из-за транзакций - можно в этот метод репо передать нужные модели разными параметрами.
Что до передачи функции бизнес-логики параметром в метод репозитория - это должно быть не базовой рекомендацией, а редчайшим исключением. Потому что эта функция бизнес-логики вполне может начать делать вызовы в другие сервисы и даже обратно в репозиторий, что, очевидно, создаст кучу проблем. И даже если сейчас она ничего такого не делает, то в неё такой функционал вполне может быть добавлен в будущем. И чтобы это предотвратить такие функции нужно как-то явно отмечать, что они не являются обычными методами бизнес-логики и в них нельзя делать то, что в других методах бизнес-логики делать можно.
Вместо использования такого подхода лучше использовать специализированные методы репо. Например, если нужно в транзакции увеличить баланс юзера, то вместо того, чтобы метод репо "начал транзакцию, достал юзера из БД в модель, вызвал метод бизнес-логики модифицирующий модель юзера, сохранил модель юзера в БД, завершил транзакцию" написать метод репо `UpdateBalance(userID, amount)`, который сделает один SQL-запрос "UPDATE" в БД который изменит одно поле в БД. Да, в некоторых ситуациях логика внесения изменений зависит от текущих данных, что приводит к формальному "протеканию" бизнес-логики в такие методы репо. Иногда это можно "компенсировать" указав все нюансы бизнес-логики в названии и/или аргументах метода репо, чтобы сохранить полный контроль над этой частью бизнес-логики снаружи. Например, если изменять баланс можно только для активных юзеров, и эта проверка статуса юзера "протекла" в метод репо, то его достаточно переименовать в `UpdateActiveUserBalance(userID, amount)` либо добавить параметр `UpdateBalance(userID, isActive, amount)`.
Благодарю за конструктивный и развернутый комментарий.
И всё же описанный в статье подход однозначно избыточно книжный/формальный. На практике можно и нужно срезать некоторые углы, которые хоть формально и нарушают книжные рекомендации, но при этом не создают проблем в реальных проектах и при этом заметно упрощают работу.
Согласен, что слепое следование любым подходам и принципам — плохая идея. Я постарался отметить это в заключении статьи. Что касается книжности и формальности, то формат и объем статьи ограничены, поэтому не всегда удаётся подробно разобрать все детали и возможные компромиссы. Часть практических аспектов я осознанно вынес в раздел «рекомендации», чтобы дать читателям больше гибкости в применении.
Слои usecase и domain в большинстве проектов нет смысла разделять.
Отчасти согласен, возможна ситуация, когда доменная сущность может быть перенесена в пакет конкретного usecase — как в виде сущности, так и через перенос логики в сам usecase. Однако, если потребуется переиспользовать логику в разных usecase, она вынужденно вернётся в слой domain. С моей точки зрения, заранее закладывать разделение domain/usecase — это выгодный подход, хотя понимаю, что кому-то он может казаться формальным.
Учитывая автоматическую генерацию моков зачастую удобнее сделать один "жирный" интерфейс с десятком-двумя методов чем делать два десятка интерфейсов по одному методу.
В этом вопросе мы с вами расходимся, но я не претендую на бескомпромиссность подхода. В данном контексте я придерживаюсь принципа разделения интерфейсов, чтобы делать их более лёгкими. «Жирный» интерфейс может привести к проблемам при его реализации, что, в свою очередь, нарушает принцип единственной ответственности. Другое дело, если хочется описать интерфейс UserGetter с 2–3 методами для получения пользователя — это вполне валидный кейс, который я считаю приемлемым.
Также не вижу проблемы в множестве мелких интерфейсов с точки зрения читабельности. Открывая конкретный usecase, сразу видно, от каких интерфейсов он зависит. К тому же их названия дают понять, о чём идёт речь.
Транзакции действительно должны быть полностью внутри конкретного метода репозитория, но при этом нет острой необходимости обязательно сохранять связь "один метод репо - одна модель"
В этом тезисе я с вами согласен. В некоторых случаях острая необходимость в агрегате может отсутствовать, и тогда его можно заменить передачей нескольких параметров — это уже вопрос вкуса при проектировании интерфейса репозитория. Однако, с точки зрения бизнес-логики агрегат — не просто контейнер для сущностей. Он подразумевает наличие поведения, затрагивающего дочерние сущности. Например, отмена заказа — это действие, которое влияет не только на сам заказ, но и на товары внутри него через единый метод агрегата. По моему мнению, в этом случае разбиение лишь усложнит интерфейс и излишне раскроет детали реализации.
Что до передачи функции бизнес-логики параметром в метод репозитория - это должно быть не базовой рекомендацией, а редчайшим исключением. Потому что эта функция бизнес-логики вполне может начать делать вызовы в другие сервисы и даже обратно в репозиторий, что, очевидно, создаст кучу проблем.
Согласен, контроль над выполняемыми операциями внутри функции в любом случае необходим, без этого не обойтись.
Вместо использования такого подхода лучше использовать специализированные методы репо.
Согласен, если есть возможность построить взаимодействие с репозиторием через специализированное и простое API без передачи функций, следует идти этим путём — этому стоило бы уделить больше внимания в статье.
Хочу отметить, что в статье я пытался противопоставлять агрегаты (агрегаты + функции) как альтернативу менеджеру транзакций, а не default подход. В статье были рассмотрены два крайних случая: когда нужно просто выполнить операцию атомарно и когда важно знать текущее состояние ресурса, выполнять дополнительные проверки, обрабатывать бизнес-ошибки, а также обеспечивать транзакционность — то есть в тех ситуациях, когда возникает острая необходимость введения менеджера транзакций.
Однако, если потребуется переиспользовать логику в разных usecase, она вынужденно вернётся в слой domain.
Если под "разными usecase" подразумеваются разные методы в слое usecase одного микросервиса - то нет. А если разные usecase разных микросервисов у которых общая часть вынесена в единый для них всех слой domain (т.е. по сути в отдельную библиотеку общую для разных микросервисов) - то да. Но в последнем случае это уже не совсем настоящий (книжный) слой domain, это просто общая либа с бизнес-логикой которая обязана быть общей для всего проекта. Я к тому, что в эту либу попадает всякое разное важное для бизнеса, но это не обязательно только модели домена и не обязательно все модели относящиеся к домену.
«Жирный» интерфейс может привести к проблемам при его реализации, что, в свою очередь, нарушает принцип единственной ответственности.
Я много проектов в таком стиле написал, ни разу никаких проблем такие жирные интерфейсы в пакетах бизнес-логики и репо не создали. Можете уточнить, о каких потенциальных проблемах речь?
Что до SRP, то нет, он не нарушается. Дядя Боб под SRP имел в виду единый источник требований к конкретному коду - а-ля чтобы функциональные требования от разных "заказчиков" (напр. отдела бухгалтерии и отдела юристов) не реализовывались в одном классе/функции.
Открывая конкретный usecase, сразу видно, от каких интерфейсов он зависит.
Ну, обычно видя названия методов в жирных интерфейсах бизнес-логики и репо обычно легко сообразить, какие методы репо используются из каких методов бизнес-логики (там нередко 1-к-1)… но острой потребности точно знать какие методы репо вызывает конкретный метод бизнес-логики у меня обычно не возникало. Мне, как архитекту, намного полезнее увидеть общую картину всех методов рядом, что позволяет легко оценить полноту/избыточность решения.
Однако, с точки зрения бизнес-логики агрегат — не просто контейнер для сущностей. Он подразумевает наличие поведения, затрагивающего дочерние сущности.
Там где агрегат требуется для правил бизнес-логики - там пусть будет агрегат. Но в контексте интерфейса репо и транзакций - не каждая транзакция подразумевает наличие агрегата на уровне бизнес-логики (по крайней мере пока мы не используем DDD, но у Вас в статье речь о DDD не идёт, да и не нужен DDD большинству проектов).
Если под "разными usecase" подразумеваются разные методы в слое usecase одного микросервиса - то нет.
Да, я имел в виду различные usecase в рамках одного сервиса. Например, в пакете domain могут быть описаны правила возврата товаров, и несколько usecase могут обращаться к этой бизнес-логике, чтобы избежать ее дублирования у себя. Кроме того, в слое domain я практически всегда размещаю статусы сущностей, которые регламентируют их жизненный цикл и другие глобальные для бизнес-логики типы.
Я много проектов в таком стиле написал, ни разу никаких проблем такие жирные интерфейсы в пакетах бизнес-логики и репо не создали. Можете уточнить, о каких потенциальных проблемах речь?
На своей практике я встречал service с вот такими интерфейсами, которые я называю «жирными»:
type IBasket interface {
AddProductToBasket(...) error
GetProductsFromBasket(...) ([]domain.Product, error)
DeleteProductsFromBasket(...) error
// и еще портяночка методов
}
type IOrder interface {
GetOrder(...) (domain.Order, error)
CreateOrder(...) error
CancelOrder(...) error
RefundOrder(...) error
// и еще портяночка методов
}
type IUser interface {
GetUser(...) (domain.User, error)
UpdateUser(...) error
// и еще портяночка методов
}
type OrderService struct {
basket IBasket
order IOrder
user IUser
}
Со временем функциональность расширяется, service «разрастаются», а вместе с ними и интерфейсы, т.е. растёт сложность системы.
В рамках рефакторинга я старался дробить такие сервисы с «жирными» интерфейсами на изолированные usecase, каждый из которых отвечал за конкретное действие. То есть один большой сервис разбивался на N-ое количество usecase. Руководствуясь принципом разделения интерфейсов для описания зависимостей, в большинстве случаев удавалось естественным образом следовать правилу «один интерфейс — один метод». Подчеркну, что это не обязательная история, но такой подход мне однозначно импонирует.
Ну, на первый взгляд данный пример напоминает скорее обычный монолит. И для монолитов такое разбухание и усложнение со временем абсолютно естественный процесс, который, разумеется, создаёт сложности. Но эта проблема возникает вовсе не из-за "жирных" интерфейсов, а из-за природы монолита.
А вот когда User, Order и Basket - это три разных микросервиса (или полноценно изолированных модуля в монолите), то данные "жирные" интерфейсы в этих микросервисах/модулях смотрятся очень хорошо и наглядно. А объединяющего их и бесконечно разбухающего OrderService обычно вообще нет - его функционал "размазывается" частично по микросервисной архитектуре (для работы Order обычно из всего domain.User достаточно поля ID, где-то можно обойтись eventual consistency и отправкой событий, etc.), частично по отдельным микросервисам, иногда даже частично по клиенту, а в исключительных случаях ещё и в сагу.
И для монолитов такое разбухание и усложнение со временем абсолютно естественный процесс, который, разумеется, создаёт сложности. Но эта проблема возникает вовсе не из-за "жирных" интерфейсов, а из-за природы монолита.
Согласен, но разрабатывать вполне пристойные монолиты, не превращая их в запущенную заводь — это выгодная стратегия с самого начала.
Думаю, что это вопрос популяризации в команде необходимых подходов и поиска компромиссов. При понятной доменной области и адекватных сроках не составляет труда сразу выделить пакеты для user, order и basket, описывать в каждом из них изолированные usecase, которые используют понятные и «легкие» интерфейсы.
Такой монолит будет by design модульным и его будет приятно распиливать на сервисы. А если не доживём до распила, то хотя бы позывов писать статьи не будет :)
предлагается на каждую комбинацию репозиториев заводить агрегат и хардкодить его сохранение?
неужели в го нет ни одного прямого способа реализовать cross cutting concerns
предлагается на каждую комбинацию репозиториев заводить агрегат и хардкодить его сохранение?
В рассматриваемом фрагменте статьи предлагается отказаться от введения абстракции менеджера транзакций, чтобы избежать «утечки» ACID-транзакции на уровень usecase через интерфейс репозитория. Вместо этого рекомендуется использовать агрегаты и проектировать соответствующие интерфейсы, что, в свою очередь, побуждает разработчика отходить от подхода «один репозиторий — одна таблица».
Надеюсь, я ответил на ваш вопрос. Если нет, пожалуйста, уточните вопрос :)
в статье пример агрегата order+products - его имплементацию надо захадкодить
потом появится агрегат order+client - его имплементацию тоже надо захадкодить
потом появится еще пять агрегатов, потом еще десять - вы обречены копипастить и копипастить
и тестировать весь зоопарк придется по отдельности
Все верно, с точки зрения usecase интерфейс для работы с агрегатом «order + products» и интерфейс для работы с агрегатом «order + client» — это разные интерфейсы.
Вопрос переиспользования кодовой базы репозитория, который реализует эти интерфейсы, решается через приватные методы репозитория.
На мой взгляд, с переиспользованием приватных методов репозитория, содержащих как raw SQL, так и ORM-модели, нужно быть аккуратным. Это особенно важно в контексте обновления данных, о чём я упоминаю в статье.
так, все же протечка абстракции - это когда вы пользуетесь специфической возможностью реализации интерфейса, минуя абстракцию.
Если же у вас есть бизнес-требование - совершать атомарно несколько операций с хранилищем, то вы вводите абстракцию, скажем, OneShotExecutor и реализуете ее поверх, скажем, бд-транзакции. Ура, у нас нет протечки.
так, все же протечка абстракции - это когда вы пользуетесь специфической возможностью реализации интерфейса, минуя абстракцию.
Выглядит так, что наши рассуждения не противоречат друг другу.
Поясню:
Если репозиторий является абстракцией, предоставляющей интерфейс для работы с хранилищем и скрывающей его конкретную природу (будь то РСУБД, NoSQL или текстовый файл), то раскрытие в этом интерфейсе информации о «транзакциях» как раз означает использование специфической возможности конкретной реализации. Например, реляционные БД поддерживают ACID-транзакции, а текстовые файлы — нет. В этом контексте это и является «утечкой абстракции».
Если же у вас есть бизнес-требование - совершать атомарно несколько операций с хранилищем, то вы вводите абстракцию, скажем, OneShotExecutor и реализуете ее поверх, скажем, бд-транзакции. Ура, у нас нет протечки.
Здесь я тоже не вижу противоречий. Возможно, дело в терминологии: агрегат и интерфейс взаимодействия с ним выполняют ту же роль, что и OneShotExecutor в вашей терминологии — отвечают за атомарность операций и предотвращают «утечку абстракции».
раскрытие в этом интерфейсе информации о «транзакциях» как раз означает использование специфической возможности конкретной реализации. Например, реляционные БД поддерживают ACID-транзакции, а текстовые файлы — нет. В этом контексте это и является «утечкой абстракции».
но если вы не тянете транзакцию из реализации, а вводите новую абстракцию, реализуемую хоть на бд-транзакции, хоть на самописном механизме, то это не утечка. Я специально абстракцию назвал OneShotExecutor, а не TransactionManager, чтобы увести от ненужных коннотаций.
То есть суммарно я хочу сказать, что использование транзакции вне репозитория - это протечка, только если вы не озаботились это бизнес-требование (а это бизнес-требование) выразить в виде абстракции.
А дальше реализуйте хоть на транзакциях, хоть на функциях доступа к файлам.
PS я не противопоставляю новую абстракцию агрегатам, лишь разбираю конкретный случай. Агрегаты, очевидно, будут тут удобнее, но это будет не всегда.
Я полностью согласен с вашим тезисом о том, что на уровне usecase возможна абстракция, описывающая именно «бизнес-транзакцию». Это не является «утечкой». Важный нюанс — определение необходимых гарантий и свойств этих «бизнес-транзакций». Предполагаю, что реализация паттерна saga примерно так и выглядит.
Подсвечу, что в статье я говорю о случаях, когда разработчики осознанно или неосознанно «сверлят дырочку», используя TransactionManager, через который «утекает» именно ACID-транзакция. Это создает жесткую связь кодовой базы и конкретной технологии, а также приводит к некорректному использованию свойств ACID-транзакций со всеми вытекающими последствиями.
Можно. В go завезли генерики, далее берем их делаем декораторы и все будет. Из минусов код получается как бы не очень идиоматичный для go и читается прям хуже. В целом я склоняюсь к выводу что если такое случается, вместо обмазывания кода генериками и декораторами в рукопашную, проще сделать генератор который будет это делать это по DSL.
Избегайте экспортируемых интерфейсов в угоду слабой связанности между пакетами, даже если это приводит к их дублированию.
Но в коде примерах видно что все экспортируемое и даже интерфейсы по месту использования. Пример:
type OrderCreator interface {
CreateOrder(ctx context.Context, order domain.CreateOrder) error
}.
Риск что кто то к себе затянет эту зависимость в пакет и будут 2 сильносвязанных пакета.
Откажитесь от подхода "один репозиторий — одна таблица", так как задача репозитория — инкапсулировать всю логику работы бизнес-сущностей с хранилищем
Паттерн «Репозиторий» — это когда работаешь с сущностью как с коллекцией, то есть фактически это когда 1 таблица = 1 сущность, хотя можно и по-другому, но по факту в реальном мире в основном так используют, так как удобно и это подталкивает использование всяких ORM. Если как вы описали, то это по факту не репозиторий, а хранилище — storage. Абстрактный слой хранения данных, и неважно, в БД в виде таблиц или в файлах и так далее.
Следствием этого практически не нужен будет менеджер транзакций, так как метод соответствующий будет скрыт в слое хранения и не выйдет в слой логики.
Про Domain:
// Getters
func (c *CancelOrder) ID() int64 { return c.id }
Несколько раз пробовал на го сделать классический ДДД по Эвансу, и постоянно получалось какая-то каша из функций с этими геттерами и сеттерами, пока остановился на варианте поля экспортируемые (как ДТО), но есть и методы API у них, как у вас в примере.
// API
func (c *CancelOrder) Cancel() error {
if c.status.IsCanceled() {
return ErrOrderAlreadyCanceled
}
c.status = OrderCanceled
c.canceledAt = time.Now()
return nil
}
Все же го не для ДДД это четко мое мнение и все оттуда 1 к 1 копировать не надо.
В принципе, в статье верное понимание, на мой взгляд, с учетом использования в проектах, но есть некоторые, на мой взгляд, спорные моменты.
Несколько раз пробовал на го сделать классический ДДД по Эвансу, и постоянно получалось какая-то каша из функций с этими геттерами и сеттерами
Вообще именно в "классическом DDD" именно классических ;-) геттеров и сеттеров быть не должно. Все эти методы должны быть бизнес-операциями. Внутри они могут быть похожи на геттеры/сеттеры, но называться должны не GetField/SetField а в соответствии с выполняемыми бизнес-задачами. А для предоставления доступа к полям внешнему миру (API/БД) достаточно одного-двух методов преобразующих модель в DTO. Понятно, что в некоторых случаях добавить геттер а-ля ID()
может быть проще и удобнее альтернатив, но именно прям классической толпы геттеров/сеттеров на большинство полей модели в DDD быть не должно.
Благодарю за конструктивный и развернутый комментарий.
Но в коде примерах видно что все экспортируемое и даже интерфейсы по месту использования.
Действительно, интерфейс публичный, но речь скорее о том, что не стоит экспортировать чужие интерфейсы из соседних usecase. Возможно, формулировка была неудачной, но именно это я и имел в виду.
Если как вы описали, то это по факту не репозиторий, а хранилище — storage. Абстрактный слой хранения данных, и неважно, в БД в виде таблиц или в файлах и так далее.
Хм, спасибо за замечание. Возможно, проблема в расхождении терминологии. В статье я старался раскрыть термин «репозиторий» и требования, которые к нему предъявляются.
Несколько раз пробовал на го сделать классический ДДД по Эвансу, и постоянно получалось какая-то каша из функций с этими геттерами и сеттерами, пока остановился на варианте поля экспортируемые (как ДТО), но есть и методы API у них, как у вас в примере.
В геттерах проблем не вижу — объявляю только необходимые, а не на все поля, но не припомню случая, когда возникала необходимость в сеттерах, т.к. обычно есть функция:
type Model struct {
filedA string
fieldB string
}
func NewModel(fieldA, fieldB) (Model, error) {...}
Если же сущность требует слишком много данных, я использую зеркальную DTO-структуру:
type ModelDTO struct {
FiledA string
FieldB string
// портянка других полей и вложенных типов
}
type Model struct {
filedA string
fieldB string
// портянка других полей и вложенных типов
}
func NewModel(dto ModelDTO) (Model, error) {...}
В итоге поля модели остаются приватными, а дальнейшая работа происходит исключительно через её API.
Смущает ваш подход с транзакциями, в комментариях по нему больше всего вопросов возникло. На мой взгляд вы привели пример скрытой передачи транзакции, а затем сказали что в целом этот подход антипатерн потому что выполняется не явно, через контекс, и что может привести к утечкам.
То что передавать в контексте транзакцию это не лучшая идея, я с вами полностью согласен, и вы правы что транзакции не присущи текстовому файлику, там на придется создавать журнал изменений и делать резервные копии и возвращать файл к предыдущему состоянию через перезапись.
И это может сделать менеджер транзакций. Меня смущает, что ваше решение добавляет лишней сложности, ну нет транзакций у файлика, назовите менеджер ACID операций. Но вам в любом случае нужна управляющая конструкция которая следит и принимает решение об комитете или откате(перезаписи).
Ваше предложение хорошо работает в монолите, но если у нас распределеный модности (несколько репозиториев) и мне нужно вызвать 6 из них, мне придется создать транзакцию в первом и передавать ее через сервис дальше, а кто отвечать будет за коммит?
Это должен делать сервис, явно создавать транзакцию и вызывать методы репозитория, он источник правды, а репозиторий не должен думать об откатах, а только исполнять. Вы хотите что бы сервис не знал про способ отката операций, напишите менеджер отката операций, но по-моему это излишне, потребность сменить бд с постгреса на текстовый файл стремится к 0, а сложностей коллегам вы добавите изрядно
если у нас распределеный модности
Это что вообще?
В целом, любых вариантов распределённых транзакций стоит избегать настолько, насколько это вообще возможно в принципе. В классическом монолите с этим проще, но уже в модульном монолите каждый модуль это аналог микросервиса, и как и у микросервисов у этих модулей должен быть собственный кусок БД (или полностью своя БД), и транзакции не должны пересекать границы этого куска БД (и границы модуля в коде). Если транзакция должна пересекать границы микросервиса/модуля монолита, то в большинстве случаев это индикатор ошибки проектирования границ ответственности (bounded context) этого микросервиса/модуля и нужно перепроектировать эти границы а не вводить транзакции пересекающие эти границы. Если перепроектировать в каком-то случае невозможно при всём желании, то да, приходится начинать использовать либо распределённые транзакции либо саги - но эта тема совершенно за рамками данной статьи/слоёной архитектуры. А попытка использовать распределённые транзакции/саги для затыкания ошибок проектирования создаст на порядок больше проблем, чем решит - потому что реализовывать и поддерживать корректную логику отката таких транзакций дико сложно и люди эту задачу просто не вытягивают (примерно как они не вытягивают задачу ручного управления памятью).
Благодарю за конструктивный и развернутый комментарий.
На мой взгляд вы привели пример скрытой передачи транзакции, а затем сказали что в целом этот подход антипатерн потому что выполняется не явно, через контекс, и что может привести к утечкам.
Уточню, чтобы избежать недопонимания. Я не хотел в статье прямо называть это антипаттерном, чтобы не вызывать ненужной эмоциональной реакции у читателей, так как понимаю, что менеджер транзакций — это скорее стандартный подход, которым пользуются разработчики. В статье я хотел подчеркнуть возможные последствия его использования, с которыми сталкивался на личном опыте, а также описать альтернативу и объяснить, почему считаю ее более корректной. А вот в комментариях, да, я сделал такое утверждение.
И это может сделать менеджер транзакций. Меня смущает, что ваше решение добавляет лишней сложности, ну нет транзакций у файлика, назовите менеджер ACID операций. Но вам в любом случае нужна управляющая конструкция которая следит и принимает решение об комитете или откате(перезаписи).
Я согласен с тезисом о том, что на уровне usecase возможна абстракция, описывающая именно «бизнес-транзакцию», и, соответственно, сущность менеджера для них. Важный нюанс — определение необходимых гарантий и свойств этих «бизнес-транзакций», а также их реализация.
Я не согласен с тем, что на уровень usecase корректно пробрасывать ACID-транзакцию и менеджер для ACID-транзакций — об этом я и пишу в статье. Это создает жесткую связь между конкретной технологией и кодовой базой, усложняет управление транзакциями и дает возможность использовать ACID-транзакции некорректно, то есть не так, как изначально предполагали разработчики СУБД.
Ваше предложение хорошо работает в монолите, но если у нас распределеный модности (несколько репозиториев) и мне нужно вызвать 6 из них, мне придется создать транзакцию в первом и передавать ее через сервис дальше, а кто отвечать будет за коммит?
В этом контексте речь идет как раз о «бизнес-транзакциях», т.е. о сагах, которые упомянул @powerman.
С моей точки зрения, если у вас есть необходимость в сагах, то менеджер саг будет находиться слоем выше и, скорее всего, оперировать usecase как этапами выполнения саги. В этом случае ACID-транзакция также остается деталью реализации репозитория и не проникает на уровень usecase.
Переусложнено
1) main.go должен быть не в папке cmd
а в папке cmd/application_name
2) "internals" должен называться "internal",
IDE следит чтобы pkg не лазил в internal,
название "internals" - это способ обмануть IDE и стандарты.
3) у меня нет объекта "Repository",
у моделей есть методы Read(), Save() и др.,
программмисты даже не знают (не видят) что где-то есть репозиторий :-)
IDE следит чтобы pkg не лазил в internal
За этим следит компилятор Go, а не IDE.
у моделей есть методы Read(), Save() и др.,
Это скорее проблема, нежели повод для радости.
"internals" должен называться "internal"
Спасибо, что заметили опечатку. Действительно, там должна быть папка internal без "s". Поправлю.
Вот тоже на эту тему. Только в видео-формате: https://vkvideo.ru/video-229915115_456239019
Архитектура продуктового Go-сервиса