【问题标题】:CQRS code duplication in commands命令中的 CQRS 代码重复
【发布时间】:2015-01-05 11:28:01
【问题描述】:

我有一个关于 CQRS 原则的命令端代码重复的问题。

一直在关注以下文章:

https://www.cuttingedge.it/blogs/steven/pivot/entry.php?id=91 https://www.cuttingedge.it/blogs/steven/pivot/entry.php?id=92

在我看来,在从数据存储中检索实体时,这种将每个命令分隔在其自己的类中的方法会产生一些代码重复。

可能有点做作,但假设我有一个命令,我想在给定他的电子邮件地址的情况下重置用户密码,还有一个命令我想更新用户的最后一个 登录日期。

public class ResetPasswordCommandHandler : CommandHandler<ResetPasswordCommand>
{
    public override void Execute(ResetPasswordCommand command)
    {
       **// duplication here**
        var user = from c in db.Users
            where c.EmailAddress = command.EmailAddress
            select c;

        user.EmailAddress = command.EmailAddress;
        ...
        db.Save();
   }
}

public class UpdateLastLoginCommandHandler : CommandHandler<UpdateLastLoginCommand>
{
    public override void Execute(UpdateLastLoginCommand command)
    {
       **// duplication here**
        var user = from c in db.Users
            where c.EmailAddress = command.EmailAddress
            select c;

        user.LastLogin = DateTime.Now;
        ...
        db.Save();
   }
}

在这两个命令中,我都根据用户的电子邮件地址检索用户。现在,如果我想在查询数据库之前修剪 UI 输入,我必须在两个地方进行更改。

我当然可以创建一个 UserRepository,例如,有一个 GetUserByEmailAddress 方法并将该 IUserRepository 插入到我的 CommandHandlers 的构造函数中。但是,这最终不会创建一个包含 Save、GetById、GetByUsername 等的“上帝存储库”吗?

如果我创建一个存储库,为什么要创建单独的 Query 对象?

如何保持此代码干燥?

【问题讨论】:

    标签: repository dry cqrs duplication


    【解决方案1】:

    为什么不将您的命令处理程序重构为一个实现多个接口的处理程序:

    public class UserCommandHandler : CommandHandlerBase, 
                                      IHandle<ResetPasswordCommand>,
                                      IHandle<UpdateLastLoginCommand>
    {
        public void Execute(ResetPasswordCommand command)
        {
            var user = GetUserByEmail(command.EmailAddress);
    
            user.EmailAddress = command.EmailAddress;
            ...
            db.Save();
       }
    
        public void Execute(UpdateLastLoginCommand command)
        {
            var user = GetUserByEmail(command.EmailAddress);
    
            user.LastLogin = DateTime.Now;
            ...
            db.Save();
       }
    
       private User GetUserByEmail(string email) {
                return (from c in db.Users
                       where c.EmailAddress = command.EmailAddress
                       select c).FirstOrDefault();
       }
    }
    

    这样,您可以在命令处理程序中重构私有帮助方法,您的命令处理程序可以处理类似的命令,并减少代码重复。您也不需要“上帝”存储库。

    就我个人而言,我宁愿将私有 GetUserByEmail 助手作为一个单独的查询类,我通过构造函数将 db 上下文注入,因此 GetUserByEmail 是一个非常具体的类,可以让我获得 @987654325 @。

    希望这会有所帮助。

    【讨论】:

    • 不会重构命令处理程序来实现多个接口只是创建一个“上帝”命令处理程序而不是臃肿的存储库吗?
    • 我喜欢第二部分。也许创建一个扩展方法。比如:public static IQueryable&lt;User&gt; WithEmailAddress(this IQueryable&lt;User&gt; source, string emailaddress)
    【解决方案2】:

    它不会创建一个上帝存储库。它将是一个适当的存储库,具有命令用例使用的方法。但这意味着您使用的是正确的存储库模式,这意味着没有公开 IQueryable 或 EF。请记住,域存储库具有仅域用例所需的“查询”方法,并且存储库仅处理域聚合根(整个域对象)。

    您当前的方法是在您的应用程序服务中使用 EF(最终是 DAO),这意味着应用程序层和可能的域层也与 EF 耦合。您的高级服务(命令处理程序最终是服务,实现用例)不应该进行查询,因为从他们的角度来看,没有 rdbms,即他们不了解查询。

    如果你的应用足够简单或者你知道以后不会有太大变化,那么你可以走直接使用EF的捷径,但是如果你决定用这种方式简化,那么你就不需要CQRS了以及基于消息的架构。

    【讨论】:

      【解决方案3】:

      我知道这看起来像代码重复,我知道您似乎违反了 DRY 原则,但我可以向您保证,在行为中使用共享服务存储库代码绝不是一个好主意。其中一个问题是,每种行为实际上可能需要通过电子邮件调用的 GetUser 实现略有不同。一个我需要全名,另一个可能不需要。通过共享此代码,您可以有效地紧密耦合两个调用。一种行为现在可能需要返回一条额外的数据,但是现在当您紧密耦合时,只要其他实现调用此“共享服务”,它就会产生不需要的所有额外数据的开销。

      如果您想在执行存储库调用时共享代码,请使用称为规范模式和/或策略模式的东西。对于上面的示例,您不会遇到任何令人讨厌的紧密耦合问题,而且作为奖励,您的代码将更好地阅读,因为意图而不是实现是最重要的。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2012-01-15
        • 2018-03-28
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2013-03-31
        • 2018-11-27
        相关资源
        最近更新 更多