1
votes

while learning DDD and cqrs, here is something I need to clarify. In a shopping context, I have a customer which I believe is my aggregate root and I would like to implement the simple use-case of change customer name.

Here is my implementation take on this using DDD/CQRS as much as I know.

My concerns are

  • for validation, should the command also validate the input to make it conform with the value object or is it okay to leave it to handler?
  • is my overall flow alright or am I heavily missing somewhere?
  • If this flow is right, I see that Customer Aggregate root will be a huge class with numerous functions like changeName, changeAddress, changePhoneNumber, deleteSavedPaymentMethod and so on. It will become a god class, and that seems a bit odd to me, is it actually the right way of DDD aggregate root implementation?

// Value Object

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

// Aggregate Root

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

// the command

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

//the handler

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

//controller

    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. some classes like UUID, Repo etc skipped for brevity.

1

1 Answers

2
votes

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

"Is it okay" -- of course; the DDD police are not going to come after you.

That said, you may be better off in the long run designing your code so that the different concepts are explicit, rather than implicit.

For example:

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

What this tells me -- a newcomer to your code base -- is that ChangeNameCommand is an in memory representation of the schema of your HTTP API, which is to say it is a representation of your contract with your consumers. Customer contracts and domain models don't change for the same reasons, so it may be wise to make the separation of the two explicit in your code (even though the underlying information is "the same").

Validation that the values that appear in the http request really do satisfy the requirements of the customer schema should happen out near the controller, rather than in near the model. It's the controller, after all, that is responsible for returning client errors if the payload doesn't satisfy the schema (ex: 422 Unprocessable Entity).

Having verified that the input is satisfactory, you can then transform the information (if necessary) from the HTTP representation of the information to the domain model's representation. That should always Just Work[tm] -- if it doesn't it indicates that you have a requirements gap somewhere.

It doesn't particularly matter where this translation happens; but if you were to imagine having multiple different schemas, or different interfaces that accept this information (a command line app, or a queue reading service, or something), then the translation code probably belongs with the interface, rather than with the domain model.

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

Your composition choices look suspicious - in particular the fact that the lifetime of the EventBus belongs to Controller::change but the lifetime of the CustomerRepo belongs to ChangeNameHander::handle.

It will become a god class...

Then break it up. See Mauro Servienti's 2019 talk.

Truth is: data models that are just storing copies of information provided by the outside world aren't particularly interesting. The good bits, that really justify the investment of work, are the state machines that decide things based on the information provided by the outside world.

If a state machine doesn't use a piece of information to make decisions, then that information belongs "somewhere else" - either a different state machine, or someplace less complicated like a database or a cache.