Command + CommandHandler 和 Service 之间有什么区别?

66

我一直在阅读关于使用命令对象来代表我们域公开的用例以及使用命令处理器对象来处理这些命令的文章。

例如:

  • RegisterUserCommand
  • RegisterUserCommandHandler

但是,这看起来和拥有一个RegisterUserService完全相同,其中命令对象将表示registerUser()方法的参数。

当然,如果该方法具有太多参数,我会创建一个对象来包装它们,并且该对象将与RegisterUserCommand相同。

那么为什么要使用不同的模式来代表相同的东西呢? 从我的经验来看,服务是普遍存在的,而不是命令; 我错过了什么区别?简而言之,我为什么要使用其中之一而不是另一个?


4
好问题!我也在想完全相同的问题。 - Sebastian Brózda
2个回答

42
拥有命令可以让你享受到好老的命令模式的好处:
  • 您可以使用命令来参数化对象,例如UI元素,以执行操作
  • 您可以存储Command并稍后执行它,例如在队列或事务日志中
  • 您可以跟踪执行的命令,为实现撤消提供基础
如果您的服务很大,每个服务拥有许多复杂的方法(如果这些方法不复杂,则您可能不应该使用DDD或CQRS),那么将每个方法移动到Command Handler中可能会通过使其更加可组合、易于测试等方面来改进您的应用程序。无疑,对于那些直接从大型服务重构到Commands/Command Handlers的人们来说,他们认为这是后者模式的一个优点是很常见的。但是,您也可以通过将大型服务分解成较小的服务来获得相同的好处(如您的示例中非常具体的服务所建议的那样),因此严格来说这并不是服务和Commands/Command Handlers之间的区别。

5
你可以存储一个命令并在以后执行它。- 这句话让我的一天变得美好!谢谢! - Cristian E.
关于能够存储的好处和命令,如果想要应用事件溯源,适配器可以将命令转换为事件,这是正确的吗,Dave? - m1lt0n
1
在最简单的情况下,每个命令都是您想要持久化的事件,您只需持久化所有命令即可实现事件溯源。或者您可能只想将某些命令存储为事件,但这些命令仍然只是事件。我也可以想象您所说的,每个或某些命令会产生事件而不仅仅是它们本身,尽管我没有具体的例子在脑海中。 - Dave Schweisguth

38

我认为你完全正确地质疑这两个概念在上下文中似乎相似。因此,回顾一下它们的实际用途是值得的。

DDD服务

在领域驱动设计中,有不同类型的服务,例如应用程序服务(通常是用户界面服务)、基础设施服务和域服务。

Jimmy Bogard很好地解释了这些

简而言之:

域服务

域服务的工作是执行通常不适合一个实体的功能。当您需要进行一项功能并需要访问多种实体(聚合/值对象)时,请考虑使用域服务。例如:计算抵押贷款可能花费多少,您需要详细了解购房者的收入/就业情况,还需要购房者的信用历史记录,最后还需要有关正在考虑抵押贷款的房屋的信息。

pricingService.CalculateMortageEstimate(BuyerIncomingDetails bid, BuyerCreditHistory bch, BuildingOverview bo)

应用服务

例如作为UI的一部分使用的服务。

基础设施服务

通常与外部资源(如电子邮件发送器、文件系统、xml文件、ftp等)通信的服务。

命令/命令处理程序(CQRS)

命令查询责任分离。正如其名称所示,将以下内容分开:

  1. 对数据源运行查询
  2. 通过命令修改您的数据

使用CQRS并不总是正确的选择,但在我的经验中,当数据分布在多个数据源时,人们倾向于使用它。

因此,对于命令,您明确要求执行工作单元(不要与UnitOfWork模式混淆),例如AddFraudRecordCommand或UpdateNoteCommand。


通过这些关于DDD服务和CQRS命令之间的差异的小刷新,我想指出以下几点:

  1. 我是否需要命令/命令处理程序?我能得到什么好处,我直接转到服务是不是更好?

  2. 我的命令处理程序的工作是处理命令的逻辑(命令是非常具体的请求)。而DDD服务有不同的工作(领域服务:协调多个实体的功能,基础设施服务:与外部服务(例如电子邮件)协作)

  3. 也许可以这样考虑: CommandHandler工作-执行代码以运行特定命令(这可能包括使用多个服务)。 服务工作-根据服务类型而定。

不是最好的例子,但我希望它能为我试图说的内容带来一些启示:

public class CalculateFraudProbabilityCommandHandler : CommandHandler<CalculateFraudProbabilityCommand>
{
         IFraudService _fraudService;
         IEmailNotifier _notifier;
         ICustomerRepository _customerRepo;


  public CalculateFraudProbabilityCommandHandler(ICustomerRepository customerRepo, IFraudService fraudService, IEmailNotifier notifier) 
  {     
        _fraudService = fraudService; //Domain Service  
        _notifier = notifier;         //Infrastructure Service  
        _customerRepo = customerRepo; //Repository
  }

 //Execute Command
 public void Execute(CalculateFraudProbabilityCommand command) {

     Customer customer = _customerRepository.GetById(command.CustomerId);
     FraudHistory fraudHistory = _fraudService.RetrieveFraudHistory(customer);

     //any fraud recently? if so, let someone know!
      if(fraudHistory.FraudSince(DateTime.Now.AddYear(-1)) {
           _notifier.SendEmail(_fraudService.BuildFraudWarningEmail(customer,      fraudHistory));
      }     

   }

}

4
谢谢您提供详细的回答。但是我不确定我理解您的观点,我没有看到您解释DDD服务(当然是领域)和命令之间的利弊在哪里? - Matthieu Napoli
我想当我开始写这篇答案时,我试图解释我认为使用CommandHandler和Domain Services之间的区别。考虑到这一点,我并没有将其视为处理程序与域服务之间的对比,因为它们用于不同的工作。尽管如此,我从不同的角度偏离了问题。 :) - Mike
1
如果我可以问一个(非常)晚的问题,您的示例中的fraudService是否违反了单一职责原则?一方面,它似乎负责检索欺诈历史详细信息,但另一方面,它也负责构建电子邮件。我有时很难找到将相关功能分组和严格遵守SOLID原则之间的平衡。 - Robba
3
将CommandHandler视为需要执行操作以完成操作任务的“协调者”。在现实世界中,CommandHandlers通常由以下两个步骤组成:a)使用命令中的属性查找一些额外数据,然后b)执行相应的操作。话虽如此,在回顾时,“发送电子邮件”应该是CalculateFraudProbabilityCommandHandler的一个事件... - Mike
3
CalculateFraudProbabilityCommandHandler应该引发一个事件,例如RaiseEvent(FraudProbabilityCalculatedEvent),并且有事件监听器来监听此事件并运行操作,例如发送电子邮件。 FraudProbabilityCalculatedEvent对象可能有一个名为public bool FraudFound { get; set; }的属性。请注意保持翻译内容的准确性和原意,同时使其通俗易懂。 - Mike
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接