假设我们正在使用域驱动设计方法构建应用程序.我们有一个后端和前端部分.后端的所有域逻辑都带有公开的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中,我们有一个用于创建帖子的端点.这将在我们的Application目录下的PostController中路由createPost方法.
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模式的错误用例吗?
public function createPost($req,$command->getContent()); $this->createPostCommandHandler->handle($command); // How do we get the data of our newly created post to the response here? return $resp; }
总线引入了一个间接层,允许您将控制器与事件处理程序分离,但是您遇到的问题更为根本.
I’m not sure how to proceed with this problem from here
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) }
简而言之,客户端而不是域模型或持久层,负责生成新实体的id.应用程序组件可以读取命令本身中的标识符,并使用它来协调下一个状态转换.
在该实现中,应用程序简单地将消息从DTO表示转换为域表示.
替代实现使用命令标识符,并从该命令派生将使用的标识
$command = new CreatePostCommand( $this->createPostId($req->getMessageId()),$command->getContent());
Named UUIDs是后一种情况的常见选择;它们是确定性的,并且具有小的碰撞概率.
现在,这个答案是一种欺骗 – 我们实际上只是证明了在这种情况下我们不需要命令处理程序的结果.
一般来说,我们宁愿拥有一个; Post / Redirect / Get是用于更新域模型的好习惯,但是当客户端获取资源时,我们希望确保他们获得的版本包含他们刚刚编辑的版本.
如果您的读取和写入使用相同的记录簿,则这不是问题 – 无论您阅读的内容始终是最新版本.
但是,cqrs是域驱动设计中的常见架构模式,在这种情况下,写模型(处理帖子)将重定向到读取模型 – 通常是发布陈旧数据.因此,您可能希望在get请求中包含最小版本,以便处理程序知道刷新其过时的缓存.
Is there an elegant way to return the post’s data in the response?
您在问题中提供的代码示例中有一个示例:
public function createPost($req,$resp)
想一想:$req是http请求消息的表示,它大致类似于你的命令,$resp本质上是一个数据结构的句柄,你可以将结果写入.
换句话说,使用您的命令传递回调或结果句柄,并让命令处理程序填写详细信息.
当然,这取决于你的公共汽车支持回调;不保证.
另一种不需要更改命令处理程序签名的可能性是安排控制器订阅命令处理程序发布的事件.您在命令和事件之间协调correlation id,并使用它来提取您需要的结果事件.
具体细节并不重要 – 处理命令时生成的事件可以写入消息总线,或者复制到邮箱中,或者….