【问题标题】:command to handler to aggregate root to repository flow in DDD and CQRS命令到处理程序以在 DDD 和 CQRS 中聚合根到存储库流
【发布时间】:2021-05-12 05:55:03
【问题描述】:

在学习 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);
      }
    }

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

【问题讨论】:

    标签: php domain-driven-design cqrs ddd-repositories


    【解决方案1】:

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

    “可以吗”——当然; 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

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

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2012-08-04
      • 2013-11-04
      • 2018-09-13
      • 1970-01-01
      • 2015-12-25
      • 2021-06-22
      • 2014-06-27
      相关资源
      最近更新 更多