CQRS 模式 - 接口

CQRS pattern - interfaces

我是 CQRS 模式的新手,但我想了解为什么您应该使用两个接口:

public interface IQuery{}
public interface ICommand{}

而不只是一个接口(例如 IExecutable 或其他...)
比你还有一个处理程序(例如 IExecutionHandler 或其他...)
如果你愿意,你仍然可以把它分成一个 ICommandExecutionHandler 和一个 IQueryExecutionHandler

更新:一次尝试

下一个代码只是我如何看待它的一个例子。有可能我完全错了...所以请分享您的 concerns/my 错误。我只是想了解这一点。

public interface IExecutable { }

public interface ICommand : IExecutable { }

public interface IReturnCommand<TOutput>: ICommand
{
    TOutput Result { get; set; }
}

public interface IQuery<TOutput>: IExecutable
{
    TOutput Result { get; set; }
}

public interface IExecutionHandler<in T>: IDisposable where T : IExecutable
{
    void Execute(T executable);
}

public class CreateAttachments : IReturnCommand<List<Guid>>
{
    public List<Attachment> Attachments { get; set; }

    public List<Guid> Result { get; set; }    
}

public abstract class BaseExecutionHandler : IDisposable
{
    protected readonly IUnitOfWork UnitOfWork;
    private bool _disposed;

    protected BaseExecutionHandler(IUnitOfWork unitOfWork)
    {
        UnitOfWork = unitOfWork;
    }

    protected virtual void Dispose(bool disposing)
    {
        if (!_disposed)
        {
            if (disposing)
            {
                UnitOfWork.Dispose();
            }
        }
        _disposed = true;
    }

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }
}

public class AttachmentCommandHandler : BaseExecutionHandler,
    IExecutionHandler<CreateAttachments>
{
    public AttachmentCommandHandler(IUnitOfWork unitOfWork) : base(unitOfWork)
    {
    }

    public void Execute(CreateAttachments command)
    {
        command.Result =  command.Attachments.Select(x => UnitOfWork.Create(x)).ToList();
    }
}

public interface IProcessor : IDisposable
{
    void Process<TExecutable>(TExecutable command) where TExecutable : IExecutable;
}

public class Processor : IProcessor
{
    private readonly Dictionary<IExecutable, IExecutionHandler<IExecutable>> _handlers;
    private readonly IUnitOfWork _unitOfWork;
    private bool _disposed;

    public Processor(IUnitOfWork unitOfWork)
    {
        _handlers = new Dictionary<IExecutable, IExecutionHandler<IExecutable>>();
        _unitOfWork = unitOfWork;
    }

    private IExecutionHandler<IExecutable> GetHandler<TExecutable>(TExecutable executable) where TExecutable: IExecutable
    {
        if (_handlers.ContainsKey(executable))
        {
            return _handlers[executable];
        }
        var handlerType = typeof(IExecutionHandler<>).MakeGenericType(executable.GetType());
        var handler = Activator.CreateInstance(handlerType, _unitOfWork) as IExecutionHandler<IExecutable>;
        _handlers.Add(executable, handler);
        return handler;
    }

    protected virtual void Dispose(bool disposing)
    {
        if (!_disposed)
        {
            if (disposing)
            {
                foreach (var handler in _handlers.Values)
                {
                    handler.Dispose();
                }
            }
        }
        _disposed = true;
    }

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    public void Process<TExecutable>(TExecutable executable) where TExecutable : IExecutable
    {
        var handler = GetHandler(executable);
        handler.Execute(executable);
    }
}

public class AttachmentController : ApiController
{
    private readonly IProcessor _processor;

    public AttachmentController(IProcessor processor)
    {
        _processor = processor;
    }

    public List<Guid> Post(List<Attachment> attachments)
    {
        var command = new CreateAttachments { Attachments = attachments };
        _processor.Process(command);
        return command.Result;
    }

    [EnableQuery]
    public IQueryable<Attachment> Get()
    {
        var query = new GetAllAttachments { };
        _processor.Process(query);
        return query.Result;
    }

    protected override void Dispose(bool disposing)
    {
        _processor.Dispose();
        base.Dispose(disposing);
    }
}

如果我没听错的话,你是在混淆首字母缩略词。从你的问题来看,你似乎并不是真的在问 Command and Query Responsibility Segregation pattern, but you might ask about the Command-Query Separation 原则。

在这种情况下,basics 简而言之是:

命令

Change the state of a system but do not return a value

查询

Return a result and do not change the observable state of the system (are free of side effects).

我将尝试演示具有通用接口(及其实现)和非通用接口之间的区别。此演示中显示的类似思维方式适用于通用查询处理程序。

解决您问题的技术方面

通用命令处理程序接口为:

public interface ICommandHandler<TCommand>
{
    void Handle(TCommand command);
}

其示例实现:

public class ExampleCommandHandler : ICommandHandler<ExampleCommand> 
{
    public void Handle(ExampleCommand command)
    {
        // Do whatever logic needed inside this command handler
    }
}

您传递给命令处理程序的示例 Command

public class ExampleCommand
{
    public int Id { get; set; }
    public string Name { get; set; }
}

最后是命令处理程序的示例消费者:

public class ExampleService
{
    private readonly ICommandHandler<ExampleCommand> commandHandler;

    public ExampleService(ICommandHandler<ExampleCommand> handler)
    {
        commandHandler = handler;
    }

    public void DoStuff(int id, string name)
    {
        var command = new ExampleCommand
        {
            Id = id,
            Name = name
        };

        commandHandler.Handle(command);
    }
}

使用通用的好处ICommandHandler

使用通用命令处理程序让用户依赖于此抽象而不是完全实现的命令处理程序。

如果您依赖于此 ExampleCommandHandler 的确切实现,而该实现不会实现通用接口,则示例服务的构造函数将具有如下依赖关系:

public ExampleService(ExampleCommandHandler handler)

在这种情况下,您无法修饰此处理程序,因为它没有实现接口。

还值得注意的是,使用此设置,您只需对命令处理程序进行单元测试,而不需要对服务的 DoStuff() 方法进行单元测试,因为行为在命令处理程序中。

关于 CQRS 的注意事项

图中的 CQRS 与 CQS 等 OOP 方法存在技术差异。

I'd like to understand why you should use two interfaces, instead of just one interface

如果查询和命令具有不同的行为契约,您应该使用两个接口。

所以充实这个问题的方法是开始考虑在每个接口中声明哪些签名,以及常见的签名是否真正意味着同一件事。

命令和查询都是不可变的;如果您稍加思考,就会发现您确实不希望编码到命令或查询中的状态在运行中被修改。所以接口中的函数应该都是查询,在 CQS 意义上 - return 对象状态的副本而不以任何方式改变它。

鉴于此,命令和查询有什么共同点?也许是一堆元数据,以便调用正确类型的处理程序,以便您可以将响应与请求相关联,等等。所有这一切的抽象是一条消息(参见 企业集成模式,Gregor Hohpe)。

所以你当然可以证明

public interface IMessage {...}

所以你可能

public interface ICommand : IMessage {...}
public interface IQuery : IMessage {...}

取决于是否存在所有命令通用但并非所有消息通用的查询。您的实施甚至可能需要

public interface CQCommonThing : IMessage {...}
public interface ICommand : CQCommonThing {...}
public interface IQuery : CQCommonThing {...}

但我很难想出任何不属于 Message 的查询和命令的共同查询示例。

另一方面,如果您正在考虑 标记接口,您实际上并没有在其中指定合同,如下所示:

public interface IQuery{}
public interface ICommand{}

那么我不知道您出于什么原因想要将它们结合起来,除非您可能想要使用 IMessage 代替。

查看您的实施,您似乎在某处丢失了情节。

public class AttachmentCommandHandler : BaseExecutionHandler,
    IExecutionHandler<CreateAttachments>
{
    public void Execute(CreateAttachments command)
    {
        command.Result =  command.Attachments.Select(x => UnitOfWork.Create(x)).ToList();
    }
}

这是命令 "create a bunch of entities in my system of record" 还是查询 "return to me a list of created entities"?尝试同时执行这两项操作违反了 CQS,这表明您走错了路。

换句话说,这个构造在这里

public interface IReturnCommand<TOutput>: ICommand
{
    TOutput Result { get; set; }
}

很奇怪 -- 为什么在使用 CQRS 模式时需要这样的东西?

以 CreateAttachments 为例,您当前的实现要求客户端发送到命令处理程序,并在 return 中接收匹配的 guid 列表。这很难实施——但您不必选择那样做。在客户端生成 ID 并将它们作为命令的一部分有什么问题?您是否认为客户端生成的 GUID 在某种程度上不如服务器生成的 GUID 唯一?

public class CreateAttachments : ICommand
{
    // or a List<Pair<Guid, Attachment> if you prefer
    // or maybe the ID is part of the attachment
    public Map<Guid, Attachment> Attachments { get; set; }
}

"Look, Ma, no Result." 调用方只需要命令的确认(这样它就可以停止发送);然后它可以通过查询同步。