DevGang
Авторизоваться

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

#PHP
Комментарии
Чтобы оставить комментарий, необходимо авторизоваться