Как получить данные обратно из командной шины?

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

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

Мы строим нашу доменную логику с помощью команд и обработчиков команд, сопоставленных с командной шиной. В нашем доменном каталоге у нас есть команда для создания почтового ресурса с именем CreatePostCommand. Он сопоставлен с его обработчиком CreatePostCommandHandler через командную шину.

final class CreatePostCommand
{
private $title;
private $content;

public function __construct(string $title, string $content)
{
$this->title = $title;
$this->content= $content;

}

public function getTitle() : string
{
return $this->title;
}

public function getContent() : string
{
return $this->content;
}
}

final class CreatePostCommandHandler
{
private $postRepository;

public function __construct(PostRepository $postRepository)
{
$this->postRepository = $postRepository;
}

public function handle(Command $command)
{
$post = new Post($command->getTitle(), $command->getContent());
$this->postRepository->save($post);
}
}

В нашем API у нас есть конечная точка для создания поста. Это маршрутизируется метод createPost в PostController в нашем каталоге приложения.

final class PostController
{
private $commandBus;

public function __construct(CommandBus $commandBus)
{
$this->commandBus = $commandBus;
}

public function createPost($req, $resp)
{
$command = new CreatePostCommand($command->getTitle(), $command->getContent());
$this->commandBus->handle($command);

// How do we get the data of our newly created post to the response here?

return $resp;
}
}

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

Я не уверен, как решить эту проблему, вот несколько вопросов, которые приходят на ум:

  • Есть ли элегантный способ вернуть данные поста в ответ?
  • Я неправильно реализую шаблон Command / CommandHandler / CommandBus?
  • Это просто неправильный вариант использования шаблона Command / CommandHandler / CommandBus?

8

Решение

Во-первых, обратите внимание, что если мы подключим контроллер напрямую к обработчику команд, мы столкнемся с аналогичной проблемой:

    public function createPost($req, $resp)
{
$command = new CreatePostCommand($command->getTitle(), $command->getContent());
$this->createPostCommandHandler->handle($command);

// How do we get the data of our newly created post to the response here?
return $resp;
}

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

Я не уверен, как решить эту проблему отсюда

TL; DR — сообщить домену, какие идентификаторы использовать, а не спрашивать домен, какой идентификатор было используемый.

    public function createPost($req, $resp)
{
// TADA
$command = new CreatePostCommand($req->getPostId()
, $command->getTitle(), $command->getContent());

$this->createPostCommandHandler->handle($command);

// happy path: redirect the client to the correct url
$this->redirectTo($resp, $postId)
}

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

В этой реализации приложение просто переводит сообщение из представления DTO в представление домена.

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

        $command = new CreatePostCommand(
$this->createPostId($req->getMessageId())
, $command->getTitle(), $command->getContent());

Именованные UUID являются общим выбором в последнем случае; они являются детерминированными и имеют малые вероятности столкновения.

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

В общем, мы бы предпочли иметь один; Post / Redirect / Get — это хорошая идиома, используемая для обновления модели домена, но когда клиент получает ресурс, мы хотим убедиться, что он получает версию, которая включает только что сделанные правки.

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

Тем не мение, является общим архитектурным шаблоном в управляемом доменом дизайне, и в этом случае модель записи (обработка сообщения) будет перенаправлена ​​на модель чтения, которая обычно публикует устаревшие данные. Поэтому вы можете захотеть включить минимальную версию в запрос get, чтобы обработчик знал, как обновить устаревший кеш.

Есть ли элегантный способ вернуть данные поста в ответ?

В примере кода, который вы задали своим вопросом, есть пример:

public function createPost($req, $resp)

Подумайте об этом: $ req является представлением сообщения http-запроса, которое примерно аналогично вашей команде, а $ resp — это дескриптор структуры данных, в которую вы можете записать свой результат.

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

Конечно, это зависит от того, поддерживает ли ваш автобус обратные вызовы; не гарантировано.

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

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

8

Другие решения

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

Контроллер псевдокода

var cmd= new MyCommand();
var listener=mediator.GetListener(cmd.Id);
bus.Send(cmd);
//wait until we get a result or timeout
var result=listener.Wait();
return result;

Функция обработчика команды псевдокода

var result= new CommandResult();
add some data here
mediator.Add(result,cmd.Id);

Вот как вы получаете немедленную обратную связь. Однако это не следует использовать для реализации бизнес-процесса.

Между прочим, это не имеет ничего общего с DDD, это в основном подход CQS, управляемый сообщениями, который может быть использован в приложении DDD.

2

По вопросам рекламы [email protected]