问题描述
在学习 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。
事实是:仅存储外部世界提供的信息副本的数据模型并不是特别有趣。真正证明工作投资合理的好点是状态机,它根据外部世界提供的信息决定事情。
如果状态机不使用一条信息来做出决定,那么该信息属于“其他地方”——或者是不同的状态机,或者是一些不太复杂的地方,比如数据库或缓存。