Так и надо было делать, а еще проверить, не создана ли уже была транзакция ранее.
Если бы была цель написать про transactionManager, то это было бы безусловно верно. Статья не про TransactionManager. У вас есть сомнения, что предложенные методы могут и добавить rollback и проверку наличия транзакции?
С этим согласен, но предлагаемое решение -- реально жесть и мусор. Транзакции -- контекстно зависимая вещь. Чтобы скрыть детали реализации самым лучшим способом будет поставить AOP interceptor вручную или при помощи аннотации фреймворка. Если нет фреймворка -- то в тыщу раз лучше оставить TM как явную зависимость в бизнес коде.
Прошу выбирать выражения. Если вам не нравится решение, это можно выразить другим способом или вы считаете такую риторику уместной?
Об этом тоже указал в статье. Для transaction manager лучшее решение будет продемонстрировано во второй части с использованием модуля application. Тут нет цели продемонстрировать фреймворк или как пользоваться AOP.
Замечу, что AOP это по своей сути, по крайней мере если мы говорим про аналог реализации @Transactional , реализация паттерна декоратор. То что это решение (с декаратором) предпочтительнее я указал как в статье, так и в коментарии тут
Почему оставить TM в бизнес коде это плохо указал как в статье, так и в комментариях выше
Это очень хреновое решение, хуже может только использование EventBus. Обзерверы -- это антипаттерн, служащий в основном, чтобы слабо связать компоненты и передать событие от более низкого компонента в иерархии к более высокому. Он вводит в код неявные трудноразрешимые динамические зависимости, о которых ничего не известно из контекста кода. Соответственно трудно читать, трудно поддерживать, трудно отлаживать и вообще все плохо. Конкретно здесь не нужна слабая связанность нужно объявить въявную зависимость кода от TM. Тем более, зачем использовать списки, если в контексте есть только единственный хендлер. Делать излишнюю абстракцию для удовлетворения конкретного кейса себе и другим дороже.
Второй раз обращу внимание на ваш способ общения. Это деструктивно для ресурса. Если вы несогласны это можно выразить иначе. Желание вести с вами дискуссию и отвечать практически нет.
То что это антипаттерн это только по вашему мнению. Вы же сами отмечаете его сильные стороны. Он позволяет слабо связать компоненты для передачи данных. И вы правы в нем есть недостаток связанный с тем, что он менее явный.
Но вы драматизируете. Весь UI строится на наблюдателях, также большинство фреймворков в качестве механизма расширения используют наблюдателей. Тот же Spring с его PostConstruct и PostDestroy и другие события жизненного цикла бина. Принципиального отличия от onStart и onEnd в этом плане нет. Плагины расширения также строятся на наблюдателях. Есть событийно ориентированные архитектуры. Заявлять, что наблюдатель является антипаттерном очень смело с вашей стороны.
Как вседа оптимистичное программирование для идеального мира, где по небу летают розовые пони, а код никогда не ломается. Если свалится первый обработчик -- другие что, не получат сообщения? А должны? Если свалится дальнейший код в теле метода, обработчики не получат end, а транзакция никогда не закроется? А если первый обработчик получил onStart(), а второй свалился, нужно ли будет первому послать onEnd()? И еще куча пессимистичных нюансов, которые превратят данный паттерн в настоящий геморрой. Так что на свалку данный велосипед.
Нет никакого оптимистичного программирования. Если свалится первый обработчик, то либо в обработчике должна быть конструкцию try-catch, если мы подразумеваем, что что-то необходимо сделать в случае проблемы с обработкой, либо если обработчик упадет, и есть какая-то общая стратегия по обработки exception-ов, то exception должен пролететь как есть до места, где его словят. Если мы говорим про Spring, то это может быть ExceptionHandler.
Если вас интересуют возможность try - catch - finally, чтобы в одном месте открыть транзакцию, а в другом гарантировано закрыть, то посмотрите на сообщение тут
Так что на свалку данный велосипед.
Вы даже можете быть правы в сути вопроса, хотя я так не считаю. Но в данном случае предлагаю задуматься, где находится подходящее место для ваших навыков общения
-------------
Надеюсь данный пример решить все возникщие у вас вопросы.
Представим код без использования наблюдателей, с TransactionManager у которого есть 4 метода
Ха! Вам, я так понимаю, никогда не приходилось переписывать операцию получения данных с синхронной версии (у вас именно такие) на асинхронные? А это именно то, что у меня однажды произошло, когда мы заменили репозиторий-поверх-БД на репозиторий-поверх-сервиса.
Стабильная абстракция my ass.
Приходилось. Об этом я расскажу во второй части. Посмотрите в конце статьи анонс. Там есть идея о разбиении процесса на этапы.
1) Если у вас есть доступ к этому разделу (стабильная абстракция), странно что вы ссылались на вики и сообщали мне о том, что в DIP ничего не упоминается про стабильность абстракций.
Также странно, что вы игнорируете следующий абзац. Я его дословно приведу, наверняка у вас есть аналог. Раздел тот же
Действительно, хорошие дизайнеры и архитекторы программного обеспечения всеми силами стремятся ограничить изменчивость интерфейсов. Они стараются найти такие пути добавления новых возможностей в реализации, которые не потребуют изменения интерфейсов. Это основа проектирования программного обеспечения.
---
2) Обратите внимание на ваше решение. Ваш Scope с его Handler все больше похож на список наблюдателей. У вас также кстати появился список. Для которого также нужно решать задачу очерёдности. Также появились общие отвязанные от конкретики интерфейсы handler, которые неявно (в той же степени что и observer) что-то делают.
Ваше решение все сильнее приближается к списку наблюдателей. И начинает иметь те же недостатки.
Но вот чтобы его совсем не приблизить, вы просто говорите, что ваше решение не может быть вызвано в произвольном месте.
Также ваше решение не позволяет передавать данные по всем направлениям.
Если я приведу ещё примеры, а "что если"? Вы ещё сильнее измените решение. Возможно оно в конечном итоге превратиться в список наблюдателей, только это будет список handler-ов.
3) Я предлагаю завершить нашу дискуссию. У нас с вами разные мнения на этот счет. Это хорошо в плане развития темы. Но, к сожалению, я как и, скорее всего, вы потратили достаточно много времени
Нет, не противоречит. Вот формулировка dependency injection principle:
И
Здесь нет ничего ни про стабильность, ни про инфраструктуру.
К сожалению, я не буду перепечатывать раздел книги. Вам остаётся либо мне поверить, либо прочитать книгу, которая является первоисточником.
Чистая архитектура. Роберт Мартин. Издательство Питер, 2019 год. Глава 11 Принцип инверсии зависимостей. Раздел: Стабильные абстракции. ст. 101
Здесь больше нет никаких transaction и lock manager, здесь есть только consistency scope handlers, которые находятся на том же уровне абстракции, что и ваши наблюдатели, только они специфичны для задачи.
Да но только вы все ещё не показываете конкретных деталей)
Иногда необходимо передавать данные из основного кода в инфраструктуру. Иногда из инфраструктуры в основной код. Это очень удобно показывать пример без возвращаемых и принимаемых значений.
Как это сделать с помощью наблюдателей я также продемонстрировал в статье. Покажите и вы для scope.
Но он возможен? Значит, надо на него заложиться. Как это делается в архитектуре с наблюдателями?
Добавлением очередного события. А как это будет делаться в Scope? Предположу, что добавлением, очередного метода.
А какое вы тогда например дадите название этому методу для Scope? Вот допустим необходимо что-то сделать, перед проверкой баланса пользователя. В статье неважно что необходимо сделать в инфраструктуре и метод называется beforeCheckUserBalance.
Как будет называться метод в этом случае для scope? Важно ли что будет делаться внутри инфраструктуры, чтобы назвать этот метод? Пусть ещё туда необходимо передать userId.
Для меня строгая консистентность данных — это свойство бизнес-логики.
Ну и да, если вы будете делать вид, что ее нет, и про нее не надо думать, то однажды вас поймает и укусит дедлок из-за разного порядка операций… но об этом я уже писал.
Почему вы считаете, что я про это не думаю. Нет необходимости все задачи решать в одном месте
Мне все еще непонятно, почему использование интерфейса repository для вас допустимо, а использование UoW, который ровно того же уровня паттерн — нет. С моей точки зрения, это совершенно надуманное разделение.
Рад что вы спросили. Использование репозиториев и клиентов - это стабильные абстракции. Они не зависят от СУБД. Абстракция с методом findById для любой СУБД будет устойчива к изменениям инфраструктуры. Даже хоть мы откажемся от СУБД и будем получать данные от другого сервиса.
Это неправда. Во-первых, они стабильны по отношению к бизнес-логике.
Они должны быть стабильны по отношению к инфраструктуре.
Суть инверсии зависимостей заизолироваться стабильными абстракциями от инфраструктуры.
Уточню формулировку, transactionManager и lockManager - абстракции, которые нестабильны по отношению к инфраструктуре, что противоречит принципам DI(P)
А во-вторых, этих интерфейсов больше нет, откуда вы их вообще взяли?
К сожалению, вы не приводите достаточного количества примеров, чтобы понять ваши решения. Почему их нет? Где они есть?
Нет, не в любой, а только в той, куда вы воткнули вызов наблюдателя. И это, внезапно, очень важно, поскольку раз вы не знаете, что наблюдатели делают, вы не знаете, куда и какие вызовы надо втыкать. Обычно это заканчивается вызовами, грубо говоря, после каждой строки бизнес-кода (и с каждым изменением бизнес-кода добавляется вызов). Это то, что я называю нестабильностью по отношению к бизнес-логике.
Приведите пример. Вызов где-то посередине бизнес-логики редкий кейс. То что вы утверждаете необходимость втыкать после каждого строчки кода звучит фантастично.
Возвращаемся к тому, что ваша бизнес-логика никак не декларирует свои требования к консистентности. Вас это, видимо, устраивает.
Да, конечно, устраивает. Потому что в бизнес-логике я хочу решать другие задачи
… тогда зачем вы потратили день моего времени на этот спор?
Я отвечал на ваши неверные с моей точки зрения альтернативные решения.
Прошу обратить внимание на статью. В статье я подчеркивал, что конкретное решение для transactionManager через наблюдателя не является самым удачным.
Также в статье я подмечал, что более удачное решение будет продемонстрировано с модулем application
Обозначенные недостатки для наблюдателей я не отрицал. Но альтернатива с явными интерфейсами в модуле core давала больше недостатков (в моем понимании). На что я пытался всюду указать.
В статье в 3 разделе я отдельно описываю, почему считаю использование интерфейсов подобных TransactionManager в модуле core недопустимым.
Моя принципиальная позиция не в наблюдателях, а в отказе от использования интерфейса TransactionManager в модуле core. Наблюдатель - это одно из решений, которое мне удалось продемонстрировать в 1-ой части.
Я перечитал еще раз, и подумал, что писать развернутый комментарий нет смысла, потому что вы опираетесь на ошибочное предположение. Ваше предположение состоит в том, что мое решение — это совокупность всего, что я раньше вам накидал в ответ на разные ваши "а что если". Но на самом деле, ключевая часть моего решения — вот:
Это кстати демонстрирует потенциальный ход развития принимаемых вами решений. То есть при столкновении с очередным, "что если?" вы меняете достаточно сильно решения. И что важно вы это делаете в модуле core.
При этом да, я мог не угадать с интерфейсом. Например, в процессе развития системы выяснилось, что требования к консистентности зависят от конкретного метода конкретного сервиса, а не универсальные. Окей, непредусмотренная функциональность, идем, добавляем некоторое количество дженериков:
Тогда в ваших интерфейсах мало смысла, если вы их помещаете в модуль core, то вы тем самым можете сказать, что у вас не общий TransactionManager или LockManager а PostgresTransactionManager и RedisLockManager. Это будет правдой.
Моя принципиальная позиция в том, что интерфейс типа TransactionManager и LockManager в core модуле не выполняет поставленных задач по изолированию решения. Эти абстракции нестабильны.
Если вы эти интерфейсы оставляете и используете в модуле core, то с точки зрения DI(P) нет разницы куда вы его спрячете внутри модуля core.
Иными словами, важно, что граница консистентности явно задана в коде (я исхожу из того, что она не всегда совпадает с границей метода, потому что иначе можно было бы уже перейти на Command/Handler pattern и унести консистентность в промежуточный обработчик). Все остальное — детали реализации. Если я когда-нибудь обнаружу себя в позиции, когда мне надо поддерживать такую сложную и меняющуюся инфраструктуру, как вы описываете ("а вот тут постгрес, а вот тут — редис, а вот тут еще что-то"), я… спрячу это в ConsistencyManagerImpl:
Граница может быть не всегда явно задана. Тут специально подобранный пример с TransactionManager, чтобы показать типичную ситуацию с взаимодействием с инфраструктурными механизмами в начале и конце метода. Но это необязательно и в статье неоднократно отмечал, что наблюдатели позволяют отправлять события в любой точке, т.е. позволяют монтировать произвольную инфраструктурную логику в любом месте.
---
Смотрите на самом деле я с вами согласен относительно многих недостатков решения с наблюдателем. Очевидно, что явное использование transactionManager и отсутствие необходимости упорядочивать наблюдателей это преимущество.
И в этом плане во второй части, у меня планируется раздел, который решает эту проблему. Он избавляет в большинстве случаев от использования наблюдателей совсем, при этом, что очень важно (по крайней мере для меня), не добавляет в модуль core интерфейсы типа TransactionManager и LockManager, UoM, ConsistancyManager и подобные им.
Мне не хотелось сейчас об этом писать, т.к. это раскрывают будущий раздел. Но, мне кажется, в этом уже есть необходимость, чтобы прийти к какому-то завершению.
Я тезисно накидаю суть:
Использование наблюдателей обладает рядом недостатков, (тут мы их подробно обсудили).
Стоит отметить, что большинство (но не все) инфраструктурных задач решаются в начале и конце основной логики
Тогда можно добавить дополнительный модуль application, для которого разрешить использование инфраструктурных интерфейсов, типа TransactionManager или LockManager и т.д.
Слой application - промежуточный слой. Которому будет позволительно поддаваться большему влиянию инфраструктуры. Но при этом он закрыт от инфраструктуры стандартным решением через инверсию зависимостей.
Зависимости между модулями следующие:
postgres/redis зависит от application и core
application зависит от core
core ни от кого не зависит
В слое application есть внешний класс сервиса, пусть будет OrderAppService, который выполняет функцию декоратора над OrderService.
class OrderAppService {
private OrderService coreService;
private TransactionManager transactionManager;
private LockManager lockManager;
public void create(CreateOrderRequest request) {
//transactionManager and lockManager usage
coreService.create(request);
//transactionManager and lockManager usage
}
}
Все приведенные мною вопросы, а "что если?" решаются на уровне модуля application без изменений модуля core и без добавления в модуль core transactionManager интерфейса и ему подобных.
В модуле core не будет использованы никакие наблюдатели, т.к. они после использования модуля application ненужны.
Также отмечу что у решения с декоратором (которым выступает OrderAppService) есть один недостаток. Он может добавлять логику только перед и после основного метода.
Тогда там будет предложена общая стратегия следующего плана:
Не добавлять transactionManager, lockManager и прочие интерфейсы в модуль core
Добавить их в модуль application и решать все возможные подобные задачи в нем. Скорее всего это будет 99% решений.
Если возникнет потребность внедрить, код по работе с инфраструктурой где-то посередине основной логики в модуле core, то только в этом случае переключаться на решение с наблюдателями.
---
Отмечу, что вы говорили, например, про отдельный модуль consitency-manager и вам показалось это странным решением. Что это модуль ради модуля. Но application модуль будет брать на себя больше ответственности. Об этом также расскажу в следующей части.
Да вы правы, эти недостатки есть в наблюдателе. Но он дает более стабильную абстракцию. К сожалению, второй поток мне будет тяжело поддерживать, поэтому сошлюсь на ответ
Если вы не забыли добавить в наблюдатель параметр контекста, который можно передавать между его вызовами.
Аналогично и если я не забыл, что у транзакции или блокировки есть состояние, ничего в core менять не придется.
Только вот ваши изменения будут зависеть от инфраструктурных изменений/ошибок. Зачем используется DI(P)? Чтобы снизить зависимость одно модуля от другого. Если в вашем подходе приходится чаще вносить изменения в core из-за каких-либо инфраструктурных изменений, значит он хуже справляется.
Это утверждение несколько подрывает DI(P), нет?
Нет, утверждение подрывает успешность создания конкретного интерфейса. Если вы посмотрите в книжку Clean Code, то в главе про DIP есть раздел про стабильные абстракции. Вы предлагаете делать абстракцию, которая близка к реализации. На что я подмечаю, что она менее стабильна, чем абстракция построенная на событиях. Наблюдатель также использует связку интерфейс-реализация, но более стабилен как абстракция.
И неустойчива к изменениям в бизнес-логике, а это, предположительно, тот кусок, который меняется чаще всего.
Так решение должно быть более устойчиво к изменениям в инфраструктурном модуле, а не к бизнес-логике. В этом и суть.
Но вообще меня удивляет, конечно: "большинство обобщений не справляется", но вот ваше решение — которое тоже обобщение! — справляется.
Да, потому что мое решение не ставит целью подвести напрямую все технологии под общий интерфейс
… потому что оно предполагает создание абстракций. То самое создание, которое вы ставите в вину моему решению.
Создание стабильных абстракций. Вашему решению я ставлю в вину стабильность.
И все их надо просмотреть, а потом понять порядок их вызовов.
Да, для этого достаточно всего лишь посмотреть на цифру в аннотации @Order
Ну вот в нашем накарманном примере их уже пять: одна для транзакций, две для блокировок (чтобы был разный порядок на входе и на выходе) и две для аудита (чтобы был разный порядок на входе и на выходе). Или три, но тогда порядок определяется сложнее, что тоже не упрощает чтение.
В моем примере их максимум 3. Два для блокировок и один для транзакций.
Аудит не должен быть частью общего процесса. Он должен уже "подключаться" (через мост, абстракции и т.д.) к самим модулям postgres/redis и через их интерфейс/абстракции получать информацию. Аудит про интеграцию с базами, а не с модулем core.
---
Смотрите, чем ваше решение мне не нравится. Вы создаете интерфейс в модуле core. При этом:
Вы себя ограничиваете
Вы допускаете возможность ошибиться
Вы создаете менее стабильную абстракцию.
Ваши интерфейсы это иллюзия. Например, в MongoDb долгое время не было транзакций на несколько объектов. Создав интерфейс TransactionManager#beginRepeatableRead() вы обманываете себя, что ваш интерфейс подойдёт к замене с MonogoDb. И если делать такую замену, то вы будете править ваш TransactionManager.beginRepeatableRead() потому что ваш интерфейс не сможет дать гарантии RepeatableRead().
Далее мы говорили, например, что может понадобиться использовать Redis для того, чтобы вынести задачу по захвату блокировок. Новое изменение заставит вас внести новый интерфейс LockManager, что опять приведет к правкам в core модуле.
Дальше, например, вы выравниваете ваш LockManager в соответствии с библиотекой redisson и используете возможность создания redlock, которые создаются на определенный промежуток времени.
Но потом, вы решили отказаться от redisson и для блокировок решили использовать advisory lock, который есть в postgres. Только вот он не умеет создаваться на определенный промежуток времени, но зато умеет привязываться к транзакции. И вы опять поменяете LockManager. И вы опять сделаете правки в модуле core.
Дальше представим, что мы используем postgres и для него использование уровня транзакции SERIALIZABLE предпочтительнее использования блокировок, потому что там используется MVCC. Поэтому на уровне core вы можете потенциально для postgres использовать именно этот уровень изоляции.
Но если по какой-то причине вы поменяете реализацию с postgres на другую, в которой нет реализации SERIALIZABLE с помощью MVCC, то вам придётся всюду, где вы использовали вместо блокировок уровень изоляции SERIALIZABLE поменять на использование блокировок типа for update/for share любо другие аналоги, т.к. использование SERIALIZABLE для другой БД это может быть антипаттерном. И вы опять внесёте изменения в модуль core.
На самом деле в этом плане вы больше создаете implicit смыслов на код, полагаясь на ваши инфраструктурные интерфейсы, на которые вы не можете полагаться. Они хуже защищают вас от изменений инфраструктуры
Но все обычно становится куда хуже. В большинстве случаев все эти задачи по управлению транзакциями, блокировками, решению задач с очередями не будет красиво спрятаны в ваш класс CreateOrderManagerService. Если интерфейс доступен на уровне core модуля, то он будет напрямую использоваться в коде core модуля.
Инфраструктурные знания расползутся по коду, их будет потом не собрать.
Учитывая, сколько этих обсерверов будет, и то, что им нужна ручная конфигурация порядка — да, жесть.
Таких наблюдателей не будет много. Это больше редкий случай, чем частая практика. Пример конфигурации представлен тут. Стоит отметить, что этот пример будет отличаться от примера предложенного @lair при конфигурации только тем, что будет добавлена аннотация @Order и только в случае, если наблюдателя более одного. Т.е. проблем с конфигкрацией особых нет.
Во второй части будет представлен дополнительный application модуль, который исключит необходимость использования наблюдателей в большинстве случаев. Но при этом при необходимости организовать вызов инфраструктурного интерфейса где-то посередине метода основной логики, будет выгодно использование именно наблюдателя.
А идентификатор вы откуда возьмете для коммита? Из тред-контекста? Так я могу то же самое сделать в имплементации менеджера в постгресе. Из прокидываемого между вызовами параметра? Так это надо было заранее заложить, и если вы забыли заложить, то вам точно так же придется все менять.
Но ничего не придётся менять в модуле core.
Почему "либо"? Я буду продумывать, как решения обобщить, и при этом ограничивать возможности во имя читаемости намерений.
Повторюсь, это специальное осознанное решение. Explicit is better than implicit.
Тут уж каждому свое, что лучше. Вы говорите что это преимущество, я наоборот что недостаток, что вы намерено будете ограничивать себя в каких-то возможностях, при этом беря на себя непростую задачу, с которой есть шанс не справится.
Кстати говоря, в большинстве случаев такие обобщения не справляются. Например, если у вас был опыт с ORM, в частности hibernate, то возможно для особо сложных запросов или полезных функций вам приходилось рушить абстракцию, используя NativeQuery вместо JPQL.
Заметьте, альтернатива которая предлагается в статье, с одной стороны более устойчива к инфраструктурным изменениям, с другой стороны позволяет использовать все её возможности в полной мере. Вы правы, для этого приходится использовать лишний интерфейс и возможно, что это менее явно. Но лично для меня это дает больше преимушеств.
Абстракций, да. Вы определение DI(P) помните?
Предложенное решение никак не противоречит DI(P). Мне кажется, данный вопрос провакационный и лишний с вашей стороны.
Это означает, что я не могу перейти к именно той реализации CreateOrderObserver, которая используется в OrderService, из OrderService. Мне нужно просмотреть все реализации, чтобы понять, что происходит, и как оно влияет на бизнес-код. Это именно то, чего я хочу избежать.
Вы можете перейти ко всем реализациям, если вы используете IDE то при нажитии на интерфейс у вас будет, скорее всего, полный список всех реализаций. Обычно их не будет более одной или двух.
То же самое и с интерфейсом наблюдателя, не вижу разницы.
Ранее приводил пример. Он, конечно, искуственный, но тем не менее. Пусть был интерфейс: TransactionManager.commit(); А затем он стал: TransactionManager.commit(transactionId); В этом случае пострадают все ConsistencyManager в core модуле. А также TransactionManagerImpl в postgres модуле.
В случае наблюдателей пострадают TransactionManager и все ObserverImpl только в postgres модуле.
Да. Потому что я хочу писать прозрачный и читаемый код. Это требует ограничения возможностей.
Не согласен с вашим подходом. Вы либо будете себя ограничивать в каких-то возможностях, либо будете продумывать, как решения можно обобщить, либо не будете этим заниматься и где-то ошибетесь.
Хочу вот еще что заметить. Попытки обобщить какие-либо технологии принимались и принимаются множество раз. Например, различные ORM-технологии, те же TransactionManager, SQL стандарт и т.д. Но всюду эти решения обладают одним общим недостатком.
Они дают меньшую свободу действий и механизмы и меньшую производительность, чем конкретные реализации.
Использование наблюдатейлей предлагает решение, которое позволит напрямую переключится к конкретной реализации в определенном наблюдателе не раскрывая при этом деталей в OrderService.
Ваш же подход предполагает создание обобщений и работу с ними. В таком подходе вы либо тратите время и отказываетесь от чего-то, чтобы построить правильные интерфейсы, либо они у вас будут очень похожи на конкретную библиотеку. И тогда толку от того, что у вас будет отделен модуль core от postgres мало.
Я повторю свой вопрос: где задается список и порядок наблюдателей?
В приведенном примере нигде не задается. Об этом будет вторая часть. Там будет добавлен модуль application, который создает список наблюдателей. Примерно такой код:
@Configuration
class OrderConfiguration {
private final List<CreateOrderObserver> observers;
@Bean
public OrderService orderService() {
return new OrderService(
observers
);
}
}
При этом для CreateOrderObserverImpl будет добавлена аннотация @Order:
@Order(1)
@Component
class CreateOrderObserverImpl implements CreateOrderObserver {
}
Не более, чем за поддержку интерфейсов наблюдателей.
Причины для изменений разные. Интерфейсы для инфраструктрных интерфейсов могут меняться например при смене реализации или при необходимости использовать доп. возможность.
При этом в решении с наблюдателями, если бизнес-логика не изменилась, то скорее всего изменятся только реализации наблюдателей в соответствующих модулях
Нет, это причина, почему я делаю такое решение — потому что я не хочу дублировать код блокировок между разными СУБД.
Если вы ошибетесь при создании интерфейса, то при смене решения, вы будете всюду менять использование этого интерфейса в модуле core, так как вы ещё и поменяете сигнатуру интерфейса.
Также вы себя ограничиваете в возможностях. Вам нужно думать о данной проблеме и нельзя напрямую инлайнить решения из фреймворка.
Иными словами, в случае с менеджером бизнес-код точно знает, на что он опирается, а в случае с наблюдателем — нет. И это именно то, с чего я начал.
Не понимаю вас. Что означает бизнес-код точно знает? Необходимо, чтобы разработчик точно знал. В строготипизированных языках программирования перейти от интерфейса к реализации можно за один шаг. Поэтому не будет особой разницы для понимания, если вы перейдете к CreateOrderConsistencyManager или к CreateOrderObserverImpl.
А то, что вы начинаете брать на себя ответственность за поддержку обоих интерфейсов. Ещё возможно, если вы захотите сделать общие решения, вам придётся очень сильно постараться в плане обобщения.
Задача обобщения решения для множества альтернатив не самая приятная. А если вы ошибетесь, то у вас будут интерфейсы, которые очень хорошо мапятся на конкретную библиотеку. Но тогда почему бы просто не подключить postgres в core и все выносить в ConsistencyManager в том же модуле?
Это будет аналогично, так как ваши интерфейсы при ошибки в обобщении не спасут вас от деталей.
Это код, который не должен зависеть от конкретной реализации СУБД или механизма блокировок.
Это ограничение на предложенное вами решение. Если такие обобщения невозможно сделать, то у вас будут интерфейсы, которые мапятся только на выбранную СУБД.
---
Также обратите внимание, что ваше решение становится очень сильно похоже на CreateOrderObserver с той лишь разницей, что вы отправляете команду в CreateOrderConsistencyManager, а в CreateOrderObserverImpl событие. Но плюсом добавляются все недостатки описанные выше.
В модуле core, опираясь на отдельные интерфейсы LockManager и TransactionManager.
Если СonsistencyManager c его методами, которые опираются на интерфейсы TransactionManager и LockManager находится в core, то и интерфейсы TransactionManager и LockManager тоже находятся в core.
В данном случае, вы просто перенесли код из одного класса в другой в одном и том же модуле. То есть из core модуля все также происходит управление транзакциями и блокировками.
Такого же эффекта можно было добиться добавив приватные методы begin и commit в OrderService.
Я правильно понимаю, что интерфейс CreateOrderObserver напрямую привязан к OrderService.create, и нигде больше быть использован не может? Иными словами, один бизнес-метод — один интерфейс наблюдателя?
Тогда я построил примеры, которые видимо как-то отличаются от того, что вы пытаетесь объяснить. Я прошу вас показать, как с помощью TransactionManager и/или UoM будет решены обе эти задачи в OrderService. Иначе мне сложно понять, что вы предлагаете. Под задачами я понимаю, что:
Необходимо как-то указать конкретный уровень изоляции для транзакции
Необходимо добавить блокировку данных в другой базе
Наша дискуссия достигла огромных размеров. За ней уже тяжело следить по уровням ответов. Если я пропустил или не ответил на какие-то важные вопросы, прошу их продублировать в этом новом треде, если хотите продолжить)
Это очень здорово, что получилось обсудить данный вопрос так подробно. Думаю, что остальным участникам будет достаточно интересно узнать разные мнения по этому поводу.
Выше также вам отвечал, что преимущество есть. Оно заключается в том, что основаная логика сконцентрирована в одном месте, а использование инфраструктурных деталей вынесено в соответствующие методы.
Также всюду выше просил вас привести пример того, как будет выглядить OrderService, если добавить больше инфраструктурных деталей. Например, мною было предложено показать как решить поставленную задачу, если потребуется:
Указать явно уровень изоляции при открытии транзакции
Добавить захват блокировок из другой базы данных, например Redis.
Решение, которое получается в результате дополнительных требований в предложенном в статье подходе можно посмотреть тут.
Также мною было продемонстрировано предположение о том, что будет сделано если использовать конкретные интерфейсы или UoM (позицию которую вы защищаете) тут
Этими примерами, мне кажется, что удалось продемонстрировать:
UoM не скрывает детали реализации лучше чем TransactionManager
Основная логика начинает обрастать инфраструктурными деталями, если использовать UoM или TransactionManager.
Также я хочу подчеркнуть, что предложенные вами решения обладают одним общим недостатком. Вы предлагаете отправлять команды. Тут неважно TransactionManager или UoM. Но команды заставляют вас знать детали. Вам нужно знать об уровнях изоляции, о необходимости захвата блокировок, о наличии скоупа транзакции. И все эти знания добавляются в OrderService с основной логикой.
Использование событий - это решение, которое позволяет избавиться от лишних знаний.
Я говорю про то, что ваше решение будет примерно таким:
Для явных интерфейсов
class OrderService {
private TransactionManager transactionManager;
private LockManager lockManager;
public void create(CreateOrderRequest request) {
lockManager.lock(request.getUserId());
lockManager.lock(request.getProductId());
var consistencyContext = transactionManager.begin("READ COMMITTED");
//domain code
lockManager.unlock(request.getProductId());
lockManager.unlock(request.getUserId());
transactionManager.commit(consistencyContext);
}
}
Для UoM
class OrderService {
public void create(CreateOrderRequest request) {
var uomContext = UoM.create();
uomContext.lock(request.getUserId());
uomContext.lock(request.getProductId());
uomContext.begin("READ COMMITTED");
//domain code
uomContext.unlock(request.getProductId());
uomContext.unlock(request.getUserId());
uomContext.commit();
}
}
Обратите внимание, что для uomContext вы как то укажите уровень изоляции. Это уже раскроет деталь реализации. Также вы будете явно захватывать блокировки.
UoM почти не отличается от использования интерфейсов TransactionManager и LockManager. Поэтому утверждение о том, что UoM скрывает детали реализации неверно.
В обоих примерах всё больше деталей проникает в основной код. Ситуацию можно ещё сильнее усугубить добавив очереди сообщений с соответствующими задачами оттуда
Если бы была цель написать про transactionManager, то это было бы безусловно верно. Статья не про TransactionManager. У вас есть сомнения, что предложенные методы могут и добавить rollback и проверку наличия транзакции?
Прошу выбирать выражения. Если вам не нравится решение, это можно выразить другим способом или вы считаете такую риторику уместной?
Об этом тоже указал в статье. Для transaction manager лучшее решение будет продемонстрировано во второй части с использованием модуля application. Тут нет цели продемонстрировать фреймворк или как пользоваться AOP.
Замечу, что AOP это по своей сути, по крайней мере если мы говорим про аналог реализации @Transactional , реализация паттерна декоратор. То что это решение (с декаратором) предпочтительнее я указал как в статье, так и в коментарии тут
Почему оставить TM в бизнес коде это плохо указал как в статье, так и в комментариях выше
Второй раз обращу внимание на ваш способ общения. Это деструктивно для ресурса. Если вы несогласны это можно выразить иначе. Желание вести с вами дискуссию и отвечать практически нет.
То что это антипаттерн это только по вашему мнению. Вы же сами отмечаете его сильные стороны. Он позволяет слабо связать компоненты для передачи данных. И вы правы в нем есть недостаток связанный с тем, что он менее явный.
Но вы драматизируете. Весь UI строится на наблюдателях, также большинство фреймворков в качестве механизма расширения используют наблюдателей. Тот же Spring с его PostConstruct и PostDestroy и другие события жизненного цикла бина. Принципиального отличия от onStart и onEnd в этом плане нет. Плагины расширения также строятся на наблюдателях. Есть событийно ориентированные архитектуры. Заявлять, что наблюдатель является антипаттерном очень смело с вашей стороны.
Нет никакого оптимистичного программирования. Если свалится первый обработчик, то либо в обработчике должна быть конструкцию try-catch, если мы подразумеваем, что что-то необходимо сделать в случае проблемы с обработкой, либо если обработчик упадет, и есть какая-то общая стратегия по обработки exception-ов, то exception должен пролететь как есть до места, где его словят. Если мы говорим про Spring, то это может быть ExceptionHandler.
Если вас интересуют возможность try - catch - finally, чтобы в одном месте открыть транзакцию, а в другом гарантировано закрыть, то посмотрите на сообщение тут
Вы даже можете быть правы в сути вопроса, хотя я так не считаю. Но в данном случае предлагаю задуматься, где находится подходящее место для ваших навыков общения
-------------
Надеюсь данный пример решить все возникщие у вас вопросы.
Представим код без использования наблюдателей, с TransactionManager у которого есть 4 метода
Теперь рассмотрим тот же пример, но с наблюдателями
0)
Приходилось. Об этом я расскажу во второй части. Посмотрите в конце статьи анонс. Там есть идея о разбиении процесса на этапы.
1) Если у вас есть доступ к этому разделу (стабильная абстракция), странно что вы ссылались на вики и сообщали мне о том, что в DIP ничего не упоминается про стабильность абстракций.
Также странно, что вы игнорируете следующий абзац. Я его дословно приведу, наверняка у вас есть аналог. Раздел тот же
Действительно, хорошие дизайнеры и архитекторы программного обеспечения всеми силами стремятся ограничить изменчивость интерфейсов. Они стараются найти такие пути добавления новых возможностей в реализации, которые не потребуют изменения интерфейсов. Это основа проектирования программного обеспечения.
---
2) Обратите внимание на ваше решение. Ваш Scope с его Handler все больше похож на список наблюдателей. У вас также кстати появился список. Для которого также нужно решать задачу очерёдности. Также появились общие отвязанные от конкретики интерфейсы handler, которые неявно (в той же степени что и observer) что-то делают.
Ваше решение все сильнее приближается к списку наблюдателей. И начинает иметь те же недостатки.
Но вот чтобы его совсем не приблизить, вы просто говорите, что ваше решение не может быть вызвано в произвольном месте.
Также ваше решение не позволяет передавать данные по всем направлениям.
Если я приведу ещё примеры, а "что если"? Вы ещё сильнее измените решение. Возможно оно в конечном итоге превратиться в список наблюдателей, только это будет список handler-ов.
3) Я предлагаю завершить нашу дискуссию. У нас с вами разные мнения на этот счет. Это хорошо в плане развития темы. Но, к сожалению, я как и, скорее всего, вы потратили достаточно много времени
И
К сожалению, я не буду перепечатывать раздел книги. Вам остаётся либо мне поверить, либо прочитать книгу, которая является первоисточником.
Чистая архитектура. Роберт Мартин. Издательство Питер, 2019 год. Глава 11 Принцип инверсии зависимостей. Раздел: Стабильные абстракции. ст. 101
Да но только вы все ещё не показываете конкретных деталей)
Иногда необходимо передавать данные из основного кода в инфраструктуру. Иногда из инфраструктуры в основной код. Это очень удобно показывать пример без возвращаемых и принимаемых значений.
Как это сделать с помощью наблюдателей я также продемонстрировал в статье. Покажите и вы для scope.
Добавлением очередного события. А как это будет делаться в Scope? Предположу, что добавлением, очередного метода.
А какое вы тогда например дадите название этому методу для Scope? Вот допустим необходимо что-то сделать, перед проверкой баланса пользователя. В статье неважно что необходимо сделать в инфраструктуре и метод называется beforeCheckUserBalance.
Как будет называться метод в этом случае для scope? Важно ли что будет делаться внутри инфраструктуры, чтобы назвать этот метод? Пусть ещё туда необходимо передать userId.
Почему вы считаете, что я про это не думаю. Нет необходимости все задачи решать в одном месте
Рад что вы спросили. Использование репозиториев и клиентов - это стабильные абстракции. Они не зависят от СУБД. Абстракция с методом findById для любой СУБД будет устойчива к изменениям инфраструктуры. Даже хоть мы откажемся от СУБД и будем получать данные от другого сервиса.
Они должны быть стабильны по отношению к инфраструктуре.
Суть инверсии зависимостей заизолироваться стабильными абстракциями от инфраструктуры.
Уточню формулировку, transactionManager и lockManager - абстракции, которые нестабильны по отношению к инфраструктуре, что противоречит принципам DI(P)
К сожалению, вы не приводите достаточного количества примеров, чтобы понять ваши решения. Почему их нет? Где они есть?
Приведите пример. Вызов где-то посередине бизнес-логики редкий кейс. То что вы утверждаете необходимость втыкать после каждого строчки кода звучит фантастично.
Да, конечно, устраивает. Потому что в бизнес-логике я хочу решать другие задачи
Я отвечал на ваши неверные с моей точки зрения альтернативные решения.
Прошу обратить внимание на статью. В статье я подчеркивал, что конкретное решение для transactionManager через наблюдателя не является самым удачным.
Также в статье я подмечал, что более удачное решение будет продемонстрировано с модулем application
Обозначенные недостатки для наблюдателей я не отрицал. Но альтернатива с явными интерфейсами в модуле core давала больше недостатков (в моем понимании). На что я пытался всюду указать.
В статье в 3 разделе я отдельно описываю, почему считаю использование интерфейсов подобных TransactionManager в модуле core недопустимым.
Моя принципиальная позиция не в наблюдателях, а в отказе от использования интерфейса TransactionManager в модуле core. Наблюдатель - это одно из решений, которое мне удалось продемонстрировать в 1-ой части.
Это кстати демонстрирует потенциальный ход развития принимаемых вами решений. То есть при столкновении с очередным, "что если?" вы меняете достаточно сильно решения. И что важно вы это делаете в модуле core.
Тогда в ваших интерфейсах мало смысла, если вы их помещаете в модуль core, то вы тем самым можете сказать, что у вас не общий TransactionManager или LockManager а PostgresTransactionManager и RedisLockManager. Это будет правдой.
Моя принципиальная позиция в том, что интерфейс типа TransactionManager и LockManager в core модуле не выполняет поставленных задач по изолированию решения. Эти абстракции нестабильны.
Если вы эти интерфейсы оставляете и используете в модуле core, то с точки зрения DI(P) нет разницы куда вы его спрячете внутри модуля core.
Граница может быть не всегда явно задана. Тут специально подобранный пример с TransactionManager, чтобы показать типичную ситуацию с взаимодействием с инфраструктурными механизмами в начале и конце метода. Но это необязательно и в статье неоднократно отмечал, что наблюдатели позволяют отправлять события в любой точке, т.е. позволяют монтировать произвольную инфраструктурную логику в любом месте.
---
Смотрите на самом деле я с вами согласен относительно многих недостатков решения с наблюдателем. Очевидно, что явное использование transactionManager и отсутствие необходимости упорядочивать наблюдателей это преимущество.
И в этом плане во второй части, у меня планируется раздел, который решает эту проблему. Он избавляет в большинстве случаев от использования наблюдателей совсем, при этом, что очень важно (по крайней мере для меня), не добавляет в модуль core интерфейсы типа TransactionManager и LockManager, UoM, ConsistancyManager и подобные им.
Мне не хотелось сейчас об этом писать, т.к. это раскрывают будущий раздел. Но, мне кажется, в этом уже есть необходимость, чтобы прийти к какому-то завершению.
Я тезисно накидаю суть:
Использование наблюдателей обладает рядом недостатков, (тут мы их подробно обсудили).
Стоит отметить, что большинство (но не все) инфраструктурных задач решаются в начале и конце основной логики
Тогда можно добавить дополнительный модуль application, для которого разрешить использование инфраструктурных интерфейсов, типа TransactionManager или LockManager и т.д.
Слой application - промежуточный слой. Которому будет позволительно поддаваться большему влиянию инфраструктуры. Но при этом он закрыт от инфраструктуры стандартным решением через инверсию зависимостей.
Зависимости между модулями следующие:
postgres/redis зависит от application и core
application зависит от core
core ни от кого не зависит
В слое application есть внешний класс сервиса, пусть будет OrderAppService, который выполняет функцию декоратора над OrderService.
Все приведенные мною вопросы, а "что если?" решаются на уровне модуля application без изменений модуля core и без добавления в модуль core transactionManager интерфейса и ему подобных.
В модуле core не будет использованы никакие наблюдатели, т.к. они после использования модуля application ненужны.
Также отмечу что у решения с декоратором (которым выступает OrderAppService) есть один недостаток. Он может добавлять логику только перед и после основного метода.
Тогда там будет предложена общая стратегия следующего плана:
Не добавлять transactionManager, lockManager и прочие интерфейсы в модуль core
Добавить их в модуль application и решать все возможные подобные задачи в нем. Скорее всего это будет 99% решений.
Если возникнет потребность внедрить, код по работе с инфраструктурой где-то посередине основной логики в модуле core, то только в этом случае переключаться на решение с наблюдателями.
---
Отмечу, что вы говорили, например, про отдельный модуль consitency-manager и вам показалось это странным решением. Что это модуль ради модуля. Но application модуль будет брать на себя больше ответственности. Об этом также расскажу в следующей части.
Да вы правы, эти недостатки есть в наблюдателе. Но он дает более стабильную абстракцию. К сожалению, второй поток мне будет тяжело поддерживать, поэтому сошлюсь на ответ
Только вот ваши изменения будут зависеть от инфраструктурных изменений/ошибок. Зачем используется DI(P)? Чтобы снизить зависимость одно модуля от другого. Если в вашем подходе приходится чаще вносить изменения в core из-за каких-либо инфраструктурных изменений, значит он хуже справляется.
Нет, утверждение подрывает успешность создания конкретного интерфейса. Если вы посмотрите в книжку Clean Code, то в главе про DIP есть раздел про стабильные абстракции. Вы предлагаете делать абстракцию, которая близка к реализации. На что я подмечаю, что она менее стабильна, чем абстракция построенная на событиях. Наблюдатель также использует связку интерфейс-реализация, но более стабилен как абстракция.
Так решение должно быть более устойчиво к изменениям в инфраструктурном модуле, а не к бизнес-логике. В этом и суть.
Да, потому что мое решение не ставит целью подвести напрямую все технологии под общий интерфейс
Создание стабильных абстракций. Вашему решению я ставлю в вину стабильность.
Да, для этого достаточно всего лишь посмотреть на цифру в аннотации
@Order
В моем примере их максимум 3. Два для блокировок и один для транзакций.
Аудит не должен быть частью общего процесса. Он должен уже "подключаться" (через мост, абстракции и т.д.) к самим модулям postgres/redis и через их интерфейс/абстракции получать информацию. Аудит про интеграцию с базами, а не с модулем core.
---
Смотрите, чем ваше решение мне не нравится. Вы создаете интерфейс в модуле core. При этом:
Вы себя ограничиваете
Вы допускаете возможность ошибиться
Вы создаете менее стабильную абстракцию.
Ваши интерфейсы это иллюзия. Например, в MongoDb долгое время не было транзакций на несколько объектов. Создав интерфейс TransactionManager#beginRepeatableRead() вы обманываете себя, что ваш интерфейс подойдёт к замене с MonogoDb. И если делать такую замену, то вы будете править ваш TransactionManager.beginRepeatableRead() потому что ваш интерфейс не сможет дать гарантии RepeatableRead().
Далее мы говорили, например, что может понадобиться использовать Redis для того, чтобы вынести задачу по захвату блокировок. Новое изменение заставит вас внести новый интерфейс LockManager, что опять приведет к правкам в core модуле.
Дальше, например, вы выравниваете ваш LockManager в соответствии с библиотекой redisson и используете возможность создания redlock, которые создаются на определенный промежуток времени.
Но потом, вы решили отказаться от redisson и для блокировок решили использовать advisory lock, который есть в postgres. Только вот он не умеет создаваться на определенный промежуток времени, но зато умеет привязываться к транзакции. И вы опять поменяете LockManager. И вы опять сделаете правки в модуле core.
Дальше представим, что мы используем postgres и для него использование уровня транзакции SERIALIZABLE предпочтительнее использования блокировок, потому что там используется MVCC. Поэтому на уровне core вы можете потенциально для postgres использовать именно этот уровень изоляции.
Но если по какой-то причине вы поменяете реализацию с postgres на другую, в которой нет реализации SERIALIZABLE с помощью MVCC, то вам придётся всюду, где вы использовали вместо блокировок уровень изоляции SERIALIZABLE поменять на использование блокировок типа for update/for share любо другие аналоги, т.к. использование SERIALIZABLE для другой БД это может быть антипаттерном. И вы опять внесёте изменения в модуль core.
На самом деле в этом плане вы больше создаете implicit смыслов на код, полагаясь на ваши инфраструктурные интерфейсы, на которые вы не можете полагаться. Они хуже защищают вас от изменений инфраструктуры
Но все обычно становится куда хуже. В большинстве случаев все эти задачи по управлению транзакциями, блокировками, решению задач с очередями не будет красиво спрятаны в ваш класс CreateOrderManagerService. Если интерфейс доступен на уровне core модуля, то он будет напрямую использоваться в коде core модуля.
Инфраструктурные знания расползутся по коду, их будет потом не собрать.
Таких наблюдателей не будет много. Это больше редкий случай, чем частая практика. Пример конфигурации представлен тут. Стоит отметить, что этот пример будет отличаться от примера предложенного @lair при конфигурации только тем, что будет добавлена аннотация
@Order
и только в случае, если наблюдателя более одного. Т.е. проблем с конфигкрацией особых нет.Во второй части будет представлен дополнительный application модуль, который исключит необходимость использования наблюдателей в большинстве случаев. Но при этом при необходимости организовать вызов инфраструктурного интерфейса где-то посередине метода основной логики, будет выгодно использование именно наблюдателя.
Но ничего не придётся менять в модуле core.
Тут уж каждому свое, что лучше. Вы говорите что это преимущество, я наоборот что недостаток, что вы намерено будете ограничивать себя в каких-то возможностях, при этом беря на себя непростую задачу, с которой есть шанс не справится.
Кстати говоря, в большинстве случаев такие обобщения не справляются. Например, если у вас был опыт с ORM, в частности hibernate, то возможно для особо сложных запросов или полезных функций вам приходилось рушить абстракцию, используя NativeQuery вместо JPQL.
Заметьте, альтернатива которая предлагается в статье, с одной стороны более устойчива к инфраструктурным изменениям, с другой стороны позволяет использовать все её возможности в полной мере. Вы правы, для этого приходится использовать лишний интерфейс и возможно, что это менее явно. Но лично для меня это дает больше преимушеств.
Предложенное решение никак не противоречит DI(P). Мне кажется, данный вопрос провакационный и лишний с вашей стороны.
Вы можете перейти ко всем реализациям, если вы используете IDE то при нажитии на интерфейс у вас будет, скорее всего, полный список всех реализаций. Обычно их не будет более одной или двух.
Ранее приводил пример. Он, конечно, искуственный, но тем не менее. Пусть был интерфейс: TransactionManager.commit();
А затем он стал: TransactionManager.commit(transactionId);
В этом случае пострадают все ConsistencyManager в core модуле. А также TransactionManagerImpl в postgres модуле.
В случае наблюдателей пострадают TransactionManager и все ObserverImpl только в postgres модуле.
Не согласен с вашим подходом. Вы либо будете себя ограничивать в каких-то возможностях, либо будете продумывать, как решения можно обобщить, либо не будете этим заниматься и где-то ошибетесь.
Хочу вот еще что заметить. Попытки обобщить какие-либо технологии принимались и принимаются множество раз. Например, различные ORM-технологии, те же TransactionManager, SQL стандарт и т.д. Но всюду эти решения обладают одним общим недостатком.
Они дают меньшую свободу действий и механизмы и меньшую производительность, чем конкретные реализации.
Использование наблюдатейлей предлагает решение, которое позволит напрямую переключится к конкретной реализации в определенном наблюдателе не раскрывая при этом деталей в OrderService.
Ваш же подход предполагает создание обобщений и работу с ними. В таком подходе вы либо тратите время и отказываетесь от чего-то, чтобы построить правильные интерфейсы, либо они у вас будут очень похожи на конкретную библиотеку. И тогда толку от того, что у вас будет отделен модуль core от postgres мало.
В приведенном примере нигде не задается. Об этом будет вторая часть. Там будет добавлен модуль application, который создает список наблюдателей.
Примерно такой код:
При этом для CreateOrderObserverImpl будет добавлена аннотация
@Order
:Причины для изменений разные. Интерфейсы для инфраструктрных интерфейсов могут меняться например при смене реализации или при необходимости использовать доп. возможность.
При этом в решении с наблюдателями, если бизнес-логика не изменилась, то скорее всего изменятся только реализации наблюдателей в соответствующих модулях
Если вы ошибетесь при создании интерфейса, то при смене решения, вы будете всюду менять использование этого интерфейса в модуле core, так как вы ещё и поменяете сигнатуру интерфейса.
Также вы себя ограничиваете в возможностях. Вам нужно думать о данной проблеме и нельзя напрямую инлайнить решения из фреймворка.
Не понимаю вас. Что означает бизнес-код точно знает? Необходимо, чтобы разработчик точно знал. В строготипизированных языках программирования перейти от интерфейса к реализации можно за один шаг. Поэтому не будет особой разницы для понимания, если вы перейдете к CreateOrderConsistencyManager или к CreateOrderObserverImpl.
А то, что вы начинаете брать на себя ответственность за поддержку обоих интерфейсов. Ещё возможно, если вы захотите сделать общие решения, вам придётся очень сильно постараться в плане обобщения.
Задача обобщения решения для множества альтернатив не самая приятная. А если вы ошибетесь, то у вас будут интерфейсы, которые очень хорошо мапятся на конкретную библиотеку. Но тогда почему бы просто не подключить postgres в core и все выносить в ConsistencyManager в том же модуле?
Это будет аналогично, так как ваши интерфейсы при ошибки в обобщении не спасут вас от деталей.
Это ограничение на предложенное вами решение. Если такие обобщения невозможно сделать, то у вас будут интерфейсы, которые мапятся только на выбранную СУБД.
---
Также обратите внимание, что ваше решение становится очень сильно похоже на CreateOrderObserver с той лишь разницей, что вы отправляете команду в CreateOrderConsistencyManager, а в CreateOrderObserverImpl событие. Но плюсом добавляются все недостатки описанные выше.
Если СonsistencyManager c его методами, которые опираются на интерфейсы TransactionManager и LockManager находится в core, то и интерфейсы TransactionManager и LockManager тоже находятся в core.
В данном случае, вы просто перенесли код из одного класса в другой в одном и том же модуле. То есть из core модуля все также происходит управление транзакциями и блокировками.
Такого же эффекта можно было добиться добавив приватные методы begin и commit в OrderService.
Покажите где будет лежать реализация, в каком модуле?
Пусть есть 3 модуля: core, postgres и redis.
Приведите пример его реализации.
Как consistencyManager узнает какие userId и productId необходимо заблокировать?
Да, верно
Тогда я построил примеры, которые видимо как-то отличаются от того, что вы пытаетесь объяснить. Я прошу вас показать, как с помощью TransactionManager и/или UoM будет решены обе эти задачи в OrderService. Иначе мне сложно понять, что вы предлагаете. Под задачами я понимаю, что:
Необходимо как-то указать конкретный уровень изоляции для транзакции
Необходимо добавить блокировку данных в другой базе
Наша дискуссия достигла огромных размеров. За ней уже тяжело следить по уровням ответов. Если я пропустил или не ответил на какие-то важные вопросы, прошу их продублировать в этом новом треде, если хотите продолжить)
Это очень здорово, что получилось обсудить данный вопрос так подробно. Думаю, что остальным участникам будет достаточно интересно узнать разные мнения по этому поводу.
Выше также вам отвечал, что преимущество есть. Оно заключается в том, что основаная логика сконцентрирована в одном месте, а использование инфраструктурных деталей вынесено в соответствующие методы.
Также всюду выше просил вас привести пример того, как будет выглядить OrderService, если добавить больше инфраструктурных деталей. Например, мною было предложено показать как решить поставленную задачу, если потребуется:
Указать явно уровень изоляции при открытии транзакции
Добавить захват блокировок из другой базы данных, например Redis.
Решение, которое получается в результате дополнительных требований в предложенном в статье подходе можно посмотреть тут.
Также мною было продемонстрировано предположение о том, что будет сделано если использовать конкретные интерфейсы или UoM (позицию которую вы защищаете) тут
Этими примерами, мне кажется, что удалось продемонстрировать:
UoM не скрывает детали реализации лучше чем TransactionManager
Основная логика начинает обрастать инфраструктурными деталями, если использовать UoM или TransactionManager.
Также я хочу подчеркнуть, что предложенные вами решения обладают одним общим недостатком. Вы предлагаете отправлять команды. Тут неважно TransactionManager или UoM. Но команды заставляют вас знать детали. Вам нужно знать об уровнях изоляции, о необходимости захвата блокировок, о наличии скоупа транзакции. И все эти знания добавляются в OrderService с основной логикой.
Использование событий - это решение, которое позволяет избавиться от лишних знаний.
Я говорю про то, что ваше решение будет примерно таким:
Для явных интерфейсов
Для UoM
Обратите внимание, что для uomContext вы как то укажите уровень изоляции. Это уже раскроет деталь реализации. Также вы будете явно захватывать блокировки.
UoM почти не отличается от использования интерфейсов TransactionManager и LockManager. Поэтому утверждение о том, что UoM скрывает детали реализации неверно.
В обоих примерах всё больше деталей проникает в основной код. Ситуацию можно ещё сильнее усугубить добавив очереди сообщений с соответствующими задачами оттуда