Search
Write a publication
Pull to refresh
1
0
Мансур Мансуров @pugnack

User

Send message

И для монолитов такое разбухание и усложнение со временем абсолютно естественный процесс, который, разумеется, создаёт сложности. Но эта проблема возникает вовсе не из-за "жирных" интерфейсов, а из-за природы монолита.

Согласен, но разрабатывать вполне пристойные монолиты, не превращая их в запущенную заводь — это выгодная стратегия с самого начала.

Думаю, что это вопрос популяризации в команде необходимых подходов и поиска компромиссов. При понятной доменной области и адекватных сроках не составляет труда сразу выделить пакеты для user, order и basket, описывать в каждом из них изолированные usecase, которые используют понятные и «легкие» интерфейсы.

Такой монолит будет by design модульным и его будет приятно распиливать на сервисы. А если не доживём до распила, то хотя бы позывов писать статьи не будет :)

Благодарю за конструктивный и развернутый комментарий.

На мой взгляд вы привели пример скрытой передачи транзакции, а затем сказали что в целом этот подход антипатерн потому что выполняется не явно, через контекс, и что может привести к утечкам.

Уточню, чтобы избежать недопонимания. Я не хотел в статье прямо называть это антипаттерном, чтобы не вызывать ненужной эмоциональной реакции у читателей, так как понимаю, что менеджер транзакций — это скорее стандартный подход, которым пользуются разработчики. В статье я хотел подчеркнуть возможные последствия его использования, с которыми сталкивался на личном опыте, а также описать альтернативу и объяснить, почему считаю ее более корректной. А вот в комментариях, да, я сделал такое утверждение.

И это может сделать менеджер транзакций. Меня смущает, что ваше решение добавляет лишней сложности, ну нет транзакций у файлика, назовите менеджер ACID операций. Но вам в любом случае нужна управляющая конструкция которая следит и принимает решение об комитете или откате(перезаписи).

Я согласен с тезисом о том, что на уровне usecase возможна абстракция, описывающая именно «бизнес-транзакцию», и, соответственно, сущность менеджера для них. Важный нюанс — определение необходимых гарантий и свойств этих «бизнес-транзакций», а также их реализация.

Я не согласен с тем, что на уровень usecase корректно пробрасывать ACID-транзакцию и менеджер для ACID-транзакций — об этом я и пишу в статье. Это создает жесткую связь между конкретной технологией и кодовой базой, усложняет управление транзакциями и дает возможность использовать ACID-транзакции некорректно, то есть не так, как изначально предполагали разработчики СУБД.

Ваше предложение хорошо работает в монолите, но если у нас распределеный модности (несколько репозиториев) и мне нужно вызвать 6 из них, мне придется создать транзакцию в первом и передавать ее через сервис дальше, а кто отвечать будет за коммит?

В этом контексте речь идет как раз о «бизнес-транзакциях», т.е. о сагах, которые упомянул @powerman.

С моей точки зрения, если у вас есть необходимость в сагах, то менеджер саг будет находиться слоем выше и, скорее всего, оперировать usecase как этапами выполнения саги. В этом случае ACID-транзакция также остается деталью реализации репозитория и не проникает на уровень usecase.

Благодарю за конструктивный и развернутый комментарий.

Но в коде примерах видно что все экспортируемое и даже интерфейсы по месту использования.

Действительно, интерфейс публичный, но речь скорее о том, что не стоит экспортировать чужие интерфейсы из соседних 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.

"internals" должен называться "internal"

Спасибо, что заметили опечатку. Действительно, там должна быть папка internal без "s". Поправлю.

Если под "разными 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. Руководствуясь принципом разделения интерфейсов для описания зависимостей, в большинстве случаев удавалось естественным образом следовать правилу «один интерфейс — один метод». Подчеркну, что это не обязательная история, но такой подход мне однозначно импонирует.

Благодарю за конструктивный и развернутый комментарий.

И всё же описанный в статье подход однозначно избыточно книжный/формальный. На практике можно и нужно срезать некоторые углы, которые хоть формально и нарушают книжные рекомендации, но при этом не создают проблем в реальных проектах и при этом заметно упрощают работу.

Согласен, что слепое следование любым подходам и принципам — плохая идея. Я постарался отметить это в заключении статьи. Что касается книжности и формальности, то формат и объем статьи ограничены, поэтому не всегда удаётся подробно разобрать все детали и возможные компромиссы. Часть практических аспектов я осознанно вынес в раздел «рекомендации», чтобы дать читателям больше гибкости в применении.

Слои usecase и domain в большинстве проектов нет смысла разделять.

Отчасти согласен, возможна ситуация, когда доменная сущность может быть перенесена в пакет конкретного usecase — как в виде сущности, так и через перенос логики в сам usecase. Однако, если потребуется переиспользовать логику в разных usecase, она вынужденно вернётся в слой domain. С моей точки зрения, заранее закладывать разделение domain/usecase — это выгодный подход, хотя понимаю, что кому-то он может казаться формальным.

Учитывая автоматическую генерацию моков зачастую удобнее сделать один "жирный" интерфейс с десятком-двумя методов чем делать два десятка интерфейсов по одному методу.

В этом вопросе мы с вами расходимся, но я не претендую на бескомпромиссность подхода. В данном контексте я придерживаюсь принципа разделения интерфейсов, чтобы делать их более лёгкими. «Жирный» интерфейс может привести к проблемам при его реализации, что, в свою очередь, нарушает принцип единственной ответственности. Другое дело, если хочется описать интерфейс UserGetter с 2–3 методами для получения пользователя — это вполне валидный кейс, который я считаю приемлемым.

Также не вижу проблемы в множестве мелких интерфейсов с точки зрения читабельности. Открывая конкретный usecase, сразу видно, от каких интерфейсов он зависит. К тому же их названия дают понять, о чём идёт речь.

Транзакции действительно должны быть полностью внутри конкретного метода репозитория, но при этом нет острой необходимости обязательно сохранять связь "один метод репо - одна модель"

В этом тезисе я с вами согласен. В некоторых случаях острая необходимость в агрегате может отсутствовать, и тогда его можно заменить передачей нескольких параметров — это уже вопрос вкуса при проектировании интерфейса репозитория. Однако, с точки зрения бизнес-логики агрегат — не просто контейнер для сущностей. Он подразумевает наличие поведения, затрагивающего дочерние сущности. Например, отмена заказа — это действие, которое влияет не только на сам заказ, но и на товары внутри него через единый метод агрегата. По моему мнению, в этом случае разбиение лишь усложнит интерфейс и излишне раскроет детали реализации.

Что до передачи функции бизнес-логики параметром в метод репозитория - это должно быть не базовой рекомендацией, а редчайшим исключением. Потому что эта функция бизнес-логики вполне может начать делать вызовы в другие сервисы и даже обратно в репозиторий, что, очевидно, создаст кучу проблем. 

Согласен, контроль над выполняемыми операциями внутри функции в любом случае необходим, без этого не обойтись.

Вместо использования такого подхода лучше использовать специализированные методы репо.

Согласен, если есть возможность построить взаимодействие с репозиторием через специализированное и простое API без передачи функций, следует идти этим путём — этому стоило бы уделить больше внимания в статье.

Хочу отметить, что в статье я пытался противопоставлять агрегаты (агрегаты + функции) как альтернативу менеджеру транзакций, а не default подход. В статье были рассмотрены два крайних случая: когда нужно просто выполнить операцию атомарно и когда важно знать текущее состояние ресурса, выполнять дополнительные проверки, обрабатывать бизнес-ошибки, а также обеспечивать транзакционность — то есть в тех ситуациях, когда возникает острая необходимость введения менеджера транзакций.

Я полностью согласен с вашим тезисом о том, что на уровне usecase возможна абстракция, описывающая именно «бизнес-транзакцию». Это не является «утечкой». Важный нюанс — определение необходимых гарантий и свойств этих «бизнес-транзакций». Предполагаю, что реализация паттерна saga примерно так и выглядит.

Подсвечу, что в статье я говорю о случаях, когда разработчики осознанно или неосознанно «сверлят дырочку», используя TransactionManager, через который «утекает» именно ACID-транзакция. Это создает жесткую связь кодовой базы и конкретной технологии, а также приводит к некорректному использованию свойств ACID-транзакций со всеми вытекающими последствиями.

так, все же протечка абстракции - это когда вы пользуетесь специфической возможностью реализации интерфейса, минуя абстракцию.

Выглядит так, что наши рассуждения не противоречат друг другу.

Поясню:

Если репозиторий является абстракцией, предоставляющей интерфейс для работы с хранилищем и скрывающей его конкретную природу (будь то РСУБД, NoSQL или текстовый файл), то раскрытие в этом интерфейсе информации о «транзакциях» как раз означает использование специфической возможности конкретной реализации. Например, реляционные БД поддерживают ACID-транзакции, а текстовые файлы — нет. В этом контексте это и является «утечкой абстракции».

Если же у вас есть бизнес-требование - совершать атомарно несколько операций с хранилищем, то вы вводите абстракцию, скажем, OneShotExecutor и реализуете ее поверх, скажем, бд-транзакции. Ура, у нас нет протечки.

Здесь я тоже не вижу противоречий. Возможно, дело в терминологии: агрегат и интерфейс взаимодействия с ним выполняют ту же роль, что и OneShotExecutor в вашей терминологии — отвечают за атомарность операций и предотвращают «утечку абстракции».

Все верно, с точки зрения usecase интерфейс для работы с агрегатом «order + products» и интерфейс для работы с агрегатом «order + client» — это разные интерфейсы.

Вопрос переиспользования кодовой базы репозитория, который реализует эти интерфейсы, решается через приватные методы репозитория.

На мой взгляд, с переиспользованием приватных методов репозитория, содержащих как raw SQL, так и ORM-модели, нужно быть аккуратным. Это особенно важно в контексте обновления данных, о чём я упоминаю в статье.

предлагается на каждую комбинацию репозиториев заводить агрегат и хардкодить его сохранение?

В рассматриваемом фрагменте статьи предлагается отказаться от введения абстракции менеджера транзакций, чтобы избежать «утечки» ACID-транзакции на уровень usecase через интерфейс репозитория. Вместо этого рекомендуется использовать агрегаты и проектировать соответствующие интерфейсы, что, в свою очередь, побуждает разработчика отходить от подхода «один репозиторий — одна таблица».

Надеюсь, я ответил на ваш вопрос. Если нет, пожалуйста, уточните вопрос :)

Не покидает ощущение попытки сделать из того, что вполне себе может оставаться простым - сложным, "по книжкам, как деды завещали"

Старался не использовать заезженные иллюстрации со слоями, но и выдумывать свою терминологию не решился. Заранее подозревал, что после прочтения статьи может показаться, будто у меня на столе лежит жёлтая книжечка Мартина вместо Библии, но это не так :)

Возможно, ощущение ошибочное, но если перед всеми другими призмами смотреть на код через призму KIS(s) - он заиграет совсем другими красками (но это не точно)

По моему мнению, описанный подход и рекомендации идеологически соонаправлены с принципом KISS, так как конечная цель — снижение сложности. В частности, это достигается за счёт изоляции бизнес-логики, отказа от перегруженных service в пользу простых usecase и уменьшения связанности системы.

Согласен, что выстраивание архитектуры — это не бесплатный процесс, но на личном опыте убедился: в продуктовых командах, работающих с бизнесом, рано или поздно встаёт вопрос о поддерживаемости и масштабировании системы.

У меня не было опыта работы в инфраструктурных командах, но предполагаю, что для "строгих" инженерных задач все эти слои и абстракции действительно могут оказаться избыточными.

Менеджер транзакций это про микросервисы?

Менеджер транзакций — это абстракция, которая позволяет работать с ACID-транзакциями вне репозитория. В данной статье рассматривается как антипаттерн, поскольку может привести к проблемам, о которых идёт речь в тексте статьи.

Скажите, го не имеет смысла использовать без микросервисов? Они глубоко в паттернах гошки

В статье не рассматривается выбор между микросервисной и монолитной архитектурами, так как это более высокоуровневый вопрос. Основное внимание уделяется построению архитектуры самого сервиса на уровне кодовой базы: слои, компоненты и их взаимодействие.

Если говорить вне контекста статьи, то Go можно использовать как для монолитов, так и для микросервисов. Выбор архитектурного стиля зависит не от языка программирования, а от требований к разделению зон ответственности.

Тоже заметил, что позиция автора в обсуждении непоследовательна.

В итоге, выглядит так, что спонсорства от комьюнити на фулл-тайм разраба может не хватить, а проекту от этого лучше не станет, да.

Судя по issue, эпопея так и не завершилась и автор по-прежнему остаётся мейнтейнером?

Правильно ли я понимаю, что форк с вашей стороны произошёл из-за медленного ревью PR?

Information

Rating
Does not participate
Registered
Activity