Search
Write a publication
Pull to refresh

Comments 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". Поправлю.

Sign up to leave a comment.

Articles