处理程序的命令以在 DDD 和 CQRS 中聚合根到存储库流

command to handler to aggregate root to repository flow in DDD and CQRS

在学习 DDD 和 cqrs 的过程中,有一点我需要澄清一下。在购物环境中,我有一个客户,我认为它是我的聚合根,我想实现更改客户名称的简单用例。

据我所知,这是我使用 DDD/CQRS 实现的。

我的顾虑是

// 值对象

    class CustomerName
    {
      private string $name;
    
      public function __construct(string $name)
      {
          if(empty($name)){
            throw new InvalidNameException();
          }
          $this->name = $name;
      }
    }

//聚合根

    class Customer
    {
      private UUID $id;
      private CustomerName $name;
    
      public function __construct(UUID $id, CustomerName $name)
      {
        $this->id = $id;
        $this->name = $name;
      }
      public function changeName(CustomerName $oldName, CustomerName $newName) {
        if($oldName !== $this->name){
          throw new InconsistencyException('Probably name was already changed');
        }
        $this->name = $newName;
      }
    }

//命令

    class ChangeNameCommand
    {
      private string $id;
      private string $oldName;
      private string $newName;
    
      public function __construct(string $id, string $oldName, string $newName)
      {
        if(empty($id)){ // only check for non empty string
          throw new InvalidIDException();
        }
        $this->id = $id;
        $this->oldName = $oldName;
        $this->newName = $newName;
      }
    
      public function getNewName(): string
      {
        return $this->newName; // alternately I could return new CustomerName($this->newName)] ?
      }
    
      public function getOldName(): string
      {
        return $this->oldName;
      }
    
      public function getID(): string
      {
        return $this->id;
      }
    }
    

//处理程序

    class ChangeNameHandler
    {
      private EventBus $eBus;
    
      public function __construct(EventBus $bus)
      {
        $this->eBus = $bus;
      }
    
      public function handle(ChangeNameCommand $nameCommand) {
        try{
          // value objects for verification
          $newName = new CustomerName($nameCommand->getNewName());
          $oldName = new CustomerName($nameCommand->getOldName());
          $customerTable = new CustomerTable();
          $customerRepo = new CustomerRepo($customerTable);
          $id = new UUID($nameCommand->id());
          $customer = $customerRepo->find($id);
          $customer->changeName($oldName, $newName);
          $customerRepo->add($customer);
          $event = new CustomerNameChanged($id);
          $this->eBus->dispatch($event);
        } catch (Exception $e) {
          $event = new CustomerNameChangFailed($nameCommand, $e);
          $this->eBus->dispatch($event);
        }
      }
    }

//控制器

    class Controller
    {
      public function change($request)
      {
          $cmd = new ChangeNameCommand($request->id, $request->old_name, $request->new_name);
          $eventBus = new EventBus();
          $handler = new ChangeNameHandler($eventBus);
          $handler->handle($cmd);
      }
    }

PS。为简洁起见,跳过了一些 classUUID、Repo 等。

should the command also validate the input to make it conform with the value object or is it okay to leave it to handler?

“可以吗”——当然可以; DDD警察不会来找你的。

就是说,您可能最好在漫长的 运行 设计代码中使不同的概念明确而不是隐含。

例如:

$cmd = new ChangeNameCommand($request->id, $request->old_name, $request->new_name);

这告诉我——你的代码库的新手——ChangeNameCommand 是你的 HTTP API 模式的内存表示,也就是说它是一个代表您与消费者之间的合同。客户合同和领域模型不会因为同样的原因而改变,因此在您的代码中明确区分两者可能是明智的(即使基础信息是“相同的”)。

验证出现在 http 请求中的值确实满足客户模式的要求应该发生在控制器附近,而不是模型附近。毕竟,如果有效负载不满足模式(例如:422 Unprocessable Entity),则控制器负责返回客户端错误。

验证输入令人满意后,您可以将信息(如有必要)从信息的 HTTP 表示形式转换为域模型的表示形式。这应该总是 Just Work[tm]——如果不是,则表明您在某处存在需求差距。

这种翻译发生在哪里并不特别重要;但是如果你想象有多个不同的模式,或者接受这些信息的不同接口(命令行应用程序,或者队列读取服务,或者其他东西),那么翻译代码可能属于接口,而不是域模型.

is my overall flow alright or am I heavily missing somewhere?

您的组合选择看起来很可疑 - 特别是 EventBus 的生命周期属于 Controller::change 而 CustomerRepo 的生命周期属于 ChangeNameHander::handle。

It will become a god class...

那就分手吧。参见 Mauro Servienti's 2019 talk

事实是:仅存储外部世界提供的信息副本的数据模型并不是特别有趣。真正证明工作投入的好处是状态机,它根据外界提供的信息决定事情。

如果状态机不使用一条信息来做出决定,那么该信息属于“其他地方”——要么是不同的状态机,要么是不那么复杂的地方,比如数据库或缓存。