Search
Write a publication
Pull to refresh
10
0
Андрей Алексеев @aa0ndrey

User

Send message

Тогда я вас попрашу показать, так как мне не удается увидеть проблему

Подскажите вы имеет ввиду проблему связанную с возможными ошибками? Если да, то об этом в статье упомяналось. Обработка ошибок намерено не была добавлена, чтобы не усложнять пример.

try {
  observers.forEach(observer -> observer.onStart(startEvent));

  //some domain code

  observers.forEach(observer -> observer.onEnd(endEvent));
} catch (Exception e) {
  observers.forEach(observer -> observer.onOrderCreationFailed(e);
}

Есть ли какие-либо противоречия с таким решением?

… а если у вас случилась произвольная ошибка, откатывать не надо? А ресурсы отпускать тоже не надо?

Если необходимо обрабатывать все ошибки, в классе OrderService необходимо будет добавить возможность отправки события о том, что произошла ошибка и вызывать событие onFailedOrderCreation в котором можно выполнить rollback.

И это мы еще не перешли к тому моменту, что вам надо обрабатывать ошибки внутри самих наблюдателей...

Нет никаких препятствий для обработки ошибок внутри наблюдателя. Там есть все знания о текущем процессе + знание инфраструктуры.

Проблема в том, что код OrderService больше не может быть уверенным в этой консистентности.

Почему это проблема? Мы можем организовывать код так, чтобы часть его не заботилась о консистентности. То есть открывая класс OrderService мы можем быть уверены, что не увидим множество инфтраструктурных задач, которые необходимо решить. Тем самым мы разбиваем ответственность разных частей кода.

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

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

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

Нет, не будет. Дедлоки возникают из-за порядка захвата ресурсов внутри потребителя (один потребитель сначала читает — и блокирует — пользователя, а потом продукты, а второй сначала читает продукты, но уже не может заблокировать пользователя), а наблюдатель ничего про это не знает.

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

Нет, вопрос в том, нужно ли нам в основной логике говорить о том, что нам нужны консистентные данные.

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

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

Консистентность — это не инфраструктурное знание.

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

Это вам кажется, что она слабо повлияла. На самом же деле, если у вас не было транзакции, а вы ее через свой подписчик добавили, то у вас внезапно могут начаться дедлоки (и поэтому код в модуле core должен ее учитывать), а если у вас была транзакция, а вы ее убрали, то у вас внезапно данные могут перестать быть консистентными (и код в core тоже должен это учитывать).

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

Более того, и вы это упоминаете в посте, чтобы была транзакция, должен быть не только start/end, а еще и откат при ошибке — и это тоже связность, которая все затрудняет.

Данный пример, если бы нужно было мог быть расширен дополнительным событием перед отправкой исключения. Например, afterCheckingUserBalanceFailed, для которого в наблюдателе CreateOrderObserverImpl был бы вызван TransactionManagerImpl.rollback()

Так это как раз неправильно. Сервису заказов нужна транзакция. Он, сервис, хочет быть уверенным, что данные консистентны. Так зачем вы это прячете?

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

Тут зависит от того, что вы хотите видеть в основной логике. Хотите ли вы видеть задачи связанные с управлением транзакцией, решение задачи по трассировке запросов, решение задачи по идемпотентности и реализации request-reply с correlationId в основной логике?

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

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

То есть класс OrderService не ожидает, что данные будут консистентны внутри запросов? И не защищается от блокировок, которые могут возникать благодаря транзакциям?

Тогда зачем вы изначально добавили транзакцию?

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

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

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

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

Уточню некоторый момент, чтобы быть уверенным, что мы одинаково пониманием контекст. В разделе 3 реализация TransactionManagerImpl осталась без изменений с семантикой методов begin и commit, а интерфейс TransactionManager был удален и заменён на CreateOrderObserver, у которого в реализации в обработке методов onStart и onEnd в классе CreateOrderObserverImpl вызываются методы begin и commit от класса TransactionManagerImpl.

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

Фактически, ваш OrderService все равно знает, что там внутри транзакция, просто она называется иначе; а CreateOrderObserverImpl знает, где конкретно вызываются эти события, чтобы сделать транзакцию — и тем самым эти модули стали тесно связаны, только через очень неявный интерфейс.

Прошу вас уточнить, что вы подразумеваете под "знает"? Если смотреть только на код модуля core и класс OrderService в частности отсутствует какое-либо знание о том, что будет открыта и зафиксирована транзакция. Также отсутствует знание о том, как это будет сделано, а именно какие нужно передавать значения в вызоваемые методы. Единственное знание, которое есть в OrderService, это то, что будет отправлено событие всем наблюдателям по ходу выполнения процесса.

Обратите внимание, когда есть прямой вызов метода, то мы отправляем команду в класс/модуль/сервис, который вызываем. Мы знаем кого вызываем и знаем, что необходимо сделать. Когда вместо прямого вызова происходит отправка события, то сторона, которая отправляет событие не знает, как и кем оно будет обработано.

Транзакция сама по себе достаточно понятная абстракция, не надо ее заменять на события. Если вам очень не нравятся транзакции — есть Unit of Work.

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

Information

Rating
Does not participate
Location
Санкт-Петербург, Санкт-Петербург и область, Россия
Registered
Activity

Specialization

Backend Developer, Software Architect
Java