PHP: Вам может не понадобиться шина запроса
"Можете ли вы сделать запрос шины с SimpleBus?" Вопрос задавался много раз. Я всегда говорил нет. По сути, потому что я не встроил опцию возврата чего-либо из обработчика команд. Таким образом, обработчик никогда не может стать обработчиком запросов, поскольку запрос, конечно, должен что-то возвращать.
Я всегда думал, что спрос на шину запросов является просто признаком необходимости симметрии. Если у вас есть методы команд и запросов, то почему бы не использовать шины команд и запросов ? Стремление к симметрии само по себе не плохо. Симметрия привлекательна, потому что она кажется естественной, и это чувство может служить обратной связью дизайна. Например, вы можете использовать отсутствие симметрии, чтобы выяснить, какой аспект дизайна все еще отсутствует, или найти альтернативные решения.
Тем не менее, я думаю, что нам может вообще не понадобиться шина запросов.
Тип возврата шины запроса - «смешанный»
Интерфейс команды или шины запроса будет выглядеть примерно так:
interface Bus { /** * @return mixed */ public function handle(object $message); }
Пример запроса и обработчик запроса будет выглядеть так:
final class GetExchangeRate { // ... } final class GetExchangeRateHandler { public function handle(GetExchangeRate $query): ExchangeRate { // ... } }
Когда вы передаете экземпляр GetExchangeRate
, Bus::handle()
в конце концов вызовет GetExchangeRateHandler::handle()
и вернет значение. Но Bus::handle()
имеет неизвестный тип возврата, который мы бы назвали «смешанным». Теперь вы знаете, что тип возвращаемого значения будет ExchangeRate
, но компилятор не будет знать. И ваша IDE тоже.
// Какой тип значения `$result`? $result = $bus->handle(new GetExchangeRate(/* ... */));
Эта ситуация напоминает мне о проблеме локатора службы (или контейнера, используемого в качестве локатора), который предлагает универсальный метод для получения служб:
interface Container { public function get(string $id): object; }
Вы не знаете, что вы собираетесь получить, пока не получите это. Тем не менее, вы полагаетесь на то, чтобы вернуть то, что ожидали получить.
Неявные зависимости
Это приводит меня к следующему возражению: если вы знаете, какая служба будет отвечать на ваш запрос и какой тип ответа будет, почему вы зависите от другой службы?
Если я увижу сервис, которому нужен обменный курс, я бы ожидал, что у этого сервиса будет зависимость ExchangeRateRepository
, или ExchangeRateProvider
, или что-то еще, но не QueryBus
, или даже Bus
. Мне нравится видеть, каковы фактические зависимости службы.
final class CreateInvoice { // Для чего этому сервису нужен `Bus` ?! public function __construct(Bus $bus) { // ... } }
Фактически, этот аргумент также действителен для самой командной шины; нам это может даже не понадобиться, поскольку для данной команды есть один обработчик команд. Почему бы не вызвать обработчик напрямую? Для автоматического переноса транзакции базы данных? Я на самом деле предпочитаю иметь дело с транзакцией только в реализации репозитория. Автоматическая отправка событий? Я делаю это вручную в моей службе приложений.
Действительно, главное, что, я надеюсь, принесла нам командная шина, - это тенденция моделировать варианты использования в качестве сервисов приложений, которые не зависят от инфраструктуры приложения. И я ввел тип возврата void
для обработчиков команд, чтобы предотвратить попадание сущностей модели записи в представления. Однако с годами я стал гораздо менее догматичным: в эти дни я с радостью возвращаю идентификаторы новых сущностей из своих служб приложений.
Нет необходимости в промежуточном программном обеспечении
На самом деле, идея о том, что командная шина имеет промежуточное программное обеспечение, которое могло бы что-то делать до или после выполнения обработчика команд, была довольно изящной. Работа с транзакциями базы данных, диспетчеризация событий, ведение журнала, проверки безопасности и т.д. Однако промежуточное ПО также имеет тенденцию скрывать важные факты от случайного читателя. Тем не менее один тип промежуточного программного обеспечения достаточно мощный: тот, который сериализует входящее сообщение и добавляет его в очередь для асинхронной обработки. Это особенно хорошо работает с командами, потому что они все равно ничего не возвращают.
Я не уверен, что какое-либо из этих промежуточных решений будет интересно для шины запросов. Запросы не должны выполняться внутри транзакции базы данных. Они не будут отправлять события, им не нужно регистрироваться и т.д. В частности, их не нужно ставить в очередь. Это не сделает своевременный ответ на ваш запрос вероятным.
Обработчик запросов, который не нуждается в промежуточном программном обеспечении, также не нуждается в шине. Единственное, что может сделать шина в этом случае, - это напрямую переслать запрос нужному обработчику. И, как я уже говорил, если есть только один обработчик, и вы его написали, почему бы не сделать его явной зависимостью и не вызвать ее напрямую?
Предлагаемый рефакторинг: замена шины запросов на сервисную зависимость
Не удивительно, что мой совет - заменить использование шины запросов реальной зависимостью от службы. Это дает вам следующие преимущества:
- Сервисные зависимости будут явными
- Типы возврата будут конкретными
Рефакторинг в случае кейса GetExchangeRate
выглядит следующим образом:
// Before:final class GetExchangeRate { public function __construct(Currency $from, Currency $to, Date $date) { // ... } } final class GetExchangeRateHandler { public function handle(GetExchangeRate $query): ExchangeRate { // ... } } // After:final class ExchangeRateProvider { public function getExchangeRateFor(Currency $from, Currency $to, Date $date): ExchangeRate { // ... } }
Кроме того, каждый сервис, который раньше зависел от шины для ответа на их запрос GetExchangeRate
, теперь будет зависеть от этой зависимости ExchangeRateProvider
и должен вводить ее как аргумент конструктора.
final class CreateInvoice { public function __construct(ExchangeRateProvider $exchangeRateProvider) { // ... } }
Необязательный рефакторинг: введите параметр объекта
Как вы могли заметить, аргументы конструктора объекта запроса теперь являются аргументами метода getExchangeRateFor()
. Это означает, что мы применили противоположность рефакторингу объекта ввода параметров. Я нахожу, что в некоторых случаях все еще стоит сохранить объект запроса. В частности, если это что-то, что представляет собой сложный запрос, с несколькими вариантами фильтрации, поиска, ограничения и т.д. В этом случае иногда конструктор может давать довольно элегантные результаты:
final class Invoices { public static function all(): self { // ... } public function createdAfter(Date $date): self { // ... } public function thatHaveBeenPaid(): self { // ... } } $this->invoiceRepository->find( Invoices::all() ->createdAfter(Date::fromString('2019-06-30'), ->thatHaveBeenPaid() );
Перевод статьи: You may not need a query bus