Я довольно плохо знаком с концепциями проектирования, управляемыми доменом, и столкнулся с проблемой возврата правильных ответов в 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 мы хотим вернуть данные нашего вновь созданного поста в наш объект ответа, чтобы наше интерфейсное приложение могло знать о вновь созданном ресурсе. Это хлопотно, поскольку мы знаем, что по определению командная шина не должна возвращать никаких данных. Так что теперь мы застряли в запутанной ситуации, когда мы не знаем, как добавить наш новый пост к объекту ответа.
Я не уверен, как решить эту проблему, вот несколько вопросов, которые приходят на ум:
Во-первых, обратите внимание, что если мы подключим контроллер напрямую к обработчику команд, мы столкнемся с аналогичной проблемой:
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 — это хорошая идиома, используемая для обновления модели домена, но когда клиент получает ресурс, мы хотим убедиться, что он получает версию, которая включает только что сделанные правки.
Если ваши операции чтения и записи используют одну и ту же книгу записей, это не проблема — что бы вы ни читали, всегда самая последняя доступная версия.
Тем не мение, CQRS является общим архитектурным шаблоном в управляемом доменом дизайне, и в этом случае модель записи (обработка сообщения) будет перенаправлена на модель чтения, которая обычно публикует устаревшие данные. Поэтому вы можете захотеть включить минимальную версию в запрос get, чтобы обработчик знал, как обновить устаревший кеш.
Есть ли элегантный способ вернуть данные поста в ответ?
В примере кода, который вы задали своим вопросом, есть пример:
public function createPost($req, $resp)
Подумайте об этом: $ req является представлением сообщения http-запроса, которое примерно аналогично вашей команде, а $ resp — это дескриптор структуры данных, в которую вы можете записать свой результат.
Другими словами, передайте обратный вызов или дескриптор результата с вашей командой, и позвольте обработчику команды заполнить детали.
Конечно, это зависит от того, поддерживает ли ваш автобус обратные вызовы; не гарантировано.
Другая возможность, которая не требует изменения подписи вашего обработчика команд, заключается в том, чтобы контроллер подписывался на события, опубликованные обработчиком команд. Вы координируете идентификатор корреляции между командой и событием, и используйте ее для получения нужного вам результата события.
Специфика не имеет большого значения — событие, сгенерированное при обработке команды, может быть записано в шину сообщений, скопировано в почтовый ящик или …
Я использую этот подход и возвращаю результаты команд. Тем не менее, это решение, которое работает только если обработчики команд являются частью одного и того же процесса. По сути, я использую посредник, контроллер и обработчик команд получают его экземпляр (обычно в зависимости от конструктора).
Контроллер псевдокода
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.