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

问题描述

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

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

我担心的是

  • 对于验证,该命令是否也应验证输入以使其符合值对象,还是可以将其留给处理程序?
  • 我的整体流程是否正常,还是我在某处严重遗漏了?
  • 如果这个流程是正确的,我看到 Customer Aggregate root 将是一个巨大的类,具有许多函数,如 changeName、changeAddress、changePhoneNumber、deleteSavedPaymentMethod 等。 它将成为一个神类,这对我来说似乎有点奇怪,它实际上是 DDD 聚合根实现的正确方法吗?

// 值对象

    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);
      }
    }

附注。为简洁起见,跳过了一些类,如 UUID、Repo 等。

解决方法

该命令是否还应验证输入以使其符合值对象,还是可以将其留给处理程序?

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

也就是说,从长远来看,您可能会更好地设计代码,以便不同的概念是显式的,而不是隐式的。

例如:

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

这告诉我 - 您的代码库的新手 - ChangeNameCommand 是您的 HTTP API 架构的内存表示,也就是说它是您与您的合同的表示消费者。客户合同和领域模型不会因为相同的原因而改变,因此在您的代码中明确将两者分开可能是明智的(即使底层信息“相同”)。

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

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

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

我的整体流程是否正常,还是我在某处严重遗漏了?

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

将成为神级...

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

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

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