在简单注入器中为泛型接口配置装饰器,并将所有实例注入带有非泛型接口参数的构造函数



我一直在使用与这篇优秀文章中描述的非常相似的模式,将命令和查询作为对象。我也使用SimpleInjector作为DI容器。

唯一显著的区别是,控制器显式依赖于一些ICommandHandler<TCommand>,我希望控制器依赖于对象(Dispatcher),该对象将采用ICommand实例并解析该命令的正确处理程序。这将减少构造函数需要接受的参数数量,并使整个过程更容易使用。

所以我的Dispatcher对象构造函数看起来像这样:

public CommandAndQueryDispatcher(IEnumerable<ICommandHandler> commandHandlers,
    IEnumerable<IQueryHandler> queryHandlers)
{
}

和我的CommandHandler接口是这样的:

public interface ICommandHandler<in TCommand> : ICommandHandler 
    where TCommand : ICommand
{
    void Execute(TCommand command, ICommandAndQueryDispatcher dispatcher);
}
public interface ICommandHandler
{
    void Execute(object command, ICommandAndQueryDispatcher dispatcher);
}

一个典型的命令处理程序看起来像:

public abstract class CommandHandlerBase<TCommand> : ICommandHandler<TCommand> 
    where TCommand : ICommand
{
    public abstract void Execute(TCommand command, ICommandAndQueryDispatcher dispatcher);
    public void Execute(object command, ICommandAndQueryDispatcher dispatcher)
    {
        Execute((TCommand) command, dispatcher);
    }
}
internal class DeleteTeamCommandHandler : CommandHandlerBase<DeleteTeamCommand>
{
    public DeleteTeamCommandHandler(){        }
    public override void Execute(DeleteTeamCommand command, 
        ICommandAndQueryDispatcher dispatcher)
    {
       ... functionality here...
    }
}

然而,这个变化有一些敲击,现在我想添加一些装饰到我的命令和查询,我有一些问题。

为了将所有的命令和查询注入Dispatcher,我使它们都有一个基本的,无泛型的接口ICommandHandlerIQueryHandler,然后询问实际收到的实例(这是通用的)以获得他们处理的命令类型来注册它们,这样我就可以根据给定命令的类型查找处理程序。

现在,当我尝试使用装饰器,如在例子中所示,我似乎不能得到任何注入到我的Dispatcher,作为装饰的实例被注册为泛型类型,所以不要得到解决作为基本的ICommandHandler实例。如果我尝试使装饰器非泛型,则注入的实例没有任何泛型类型参数,因此我找不到其处理程序的命令类型。

我觉得我一定错过了一些相当简单的东西。

所以我的问题是要么

  • 我如何从容器转换作为传递到我的Dispatcher的基接口获得开放泛型类型的所有实例?

  • 是否有更好的方法让我实现调度功能,以便我的控制器可以不知道哪个处理程序将处理命令/查询,这与SimpleInjector更适合?

这将减少构造函数所需的参数数量为了让整个过程更容易使用

请密切关注这一点,因为这样做,你可能会隐藏你的控制器做得太多的事实;违反单一责任原则。违反SRP往往会导致以后的可维护性问题。你提到的那篇文章的作者(顺便说一句,就是我)甚至有一篇后续文章说:

我当然不会提倡ICommandProcessorICommandAndQueryDispatcher[在您的示例中]用于执行命令消费者不太可能依赖于许多命令如果他们这样做,可能会违反SRP。(源)

本文甚至讨论了查询的解决方案,但您也可以将其应用于命令。但是您应该考虑剥离您的解决方案并删除非泛型接口。你不需要他们。

应该定义如下:

public interface ICommandHandler<TCommand> : where TCommand : ICommand
{
    void Execute(TCommand command);
}

请注意以下几点:

  1. 没有非泛型接口
  2. 你没有通过ICommandAndQueryDispatcher。这太丑了。ICommandAndQueryDispatcher是一个服务,服务需要通过构造函数注入传递。另一方面,command是运行时数据,运行时数据通过方法参数传递。因此,如果有一个命令或查询处理程序需要调度程序:通过构造函数注入。
  3. TCommand没有in关键字。因为命令是用例,所以在命令和命令处理程序实现之间应该有一对一的映射。然而,指定'in'意味着一个命令类可以映射到多个处理程序,但情况不应该是这样。另一方面,当处理事件和事件处理程序时,这将是一个更明显的方法。
  4. No more CommandHandlerBase<TCommand>。你不需要那个。我认为好的设计几乎不需要基类。

另一件事,不要试图将命令的调度程序与查询的调度程序混合使用。两个职责意味着两个类。命令调度程序是这样的:

// This interface is defined in a core application layer
public interface ICommandDispatcher
{
    void Execute(ICommand command);
}
// This class is defined in your Composition Root (where you wire your container)
// It needs a dependency to the container.
sealed class CommandDispatcher : ICommandDispatcher
{
    private readonly Container container;
    public CommandDispatcher(Container container) {
        this.container = container;
    }
    public void Execute(ICommand command) {
        var handlerType = typeof(ICommandHandler<>)
            .MakeGenericType(command.GetType());
        dynamic handler = container.GetInstance(handlerType);
        handler.Handle((dynamic)command);
    }
}

请注意,这里不是注入命令处理程序集合,而是从容器请求一个处理程序。此代码将只包含基础设施而不包含业务逻辑,因此,如果将此实现放置在负责连接容器的代码附近,则不会滥用Service Locator反模式,这是一种有效的方法。在这种情况下,应用程序的其余部分仍然不依赖于DI框架。

您可以按以下方式注册此CommandDispatcher:

container.RegisterSingle<ICommandDispatcher>(new CommandDispatcher(container));

如果您采用这种方法,因为您通过ICommandHandler<TCommand>接口请求处理程序,容器将自动使用必须根据您的配置和您应用的泛型类型约束应用的任何装饰器来包装处理程序。

最新更新