处理程序的命令以在 DDD 和 CQRS 中聚合根到存储库流
command to handler to aggregate root to repository flow in DDD and CQRS
在学习 DDD 和 cqrs 的过程中,有一点我需要澄清一下。在购物环境中,我有一个客户,我认为它是我的聚合根,我想实现更改客户名称的简单用例。
据我所知,这是我使用 DDD/CQRS 实现的。
我的顾虑是
- 对于验证,该命令是否还应验证输入以使其符合值对象,还是可以将其留给处理程序?
- 我的整体流程是否正常,或者我是否严重遗漏了某个地方?
- 如果此流程正确,我会看到 Customer Aggregate 根将是一个巨大的 class,具有许多功能,例如 changeName、changeAddress、changePhoneNumber、deleteSavedPaymentMethod 等等。
会成神class,我觉得有点奇怪,这到底是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。为简洁起见,跳过了一些 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。
事实是:仅存储外部世界提供的信息副本的数据模型并不是特别有趣。真正证明工作投入的好处是状态机,它根据外界提供的信息决定事情。
如果状态机不使用一条信息来做出决定,那么该信息属于“其他地方”——要么是不同的状态机,要么是不那么复杂的地方,比如数据库或缓存。
在学习 DDD 和 cqrs 的过程中,有一点我需要澄清一下。在购物环境中,我有一个客户,我认为它是我的聚合根,我想实现更改客户名称的简单用例。
据我所知,这是我使用 DDD/CQRS 实现的。
我的顾虑是
- 对于验证,该命令是否还应验证输入以使其符合值对象,还是可以将其留给处理程序?
- 我的整体流程是否正常,或者我是否严重遗漏了某个地方?
- 如果此流程正确,我会看到 Customer Aggregate 根将是一个巨大的 class,具有许多功能,例如 changeName、changeAddress、changePhoneNumber、deleteSavedPaymentMethod 等等。 会成神class,我觉得有点奇怪,这到底是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。为简洁起见,跳过了一些 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。
事实是:仅存储外部世界提供的信息副本的数据模型并不是特别有趣。真正证明工作投入的好处是状态机,它根据外界提供的信息决定事情。
如果状态机不使用一条信息来做出决定,那么该信息属于“其他地方”——要么是不同的状态机,要么是不那么复杂的地方,比如数据库或缓存。