【问题标题】:Contravariance? Covariance? What's wrong with this generic architecture...?逆变?协方差?这种通用架构有什么问题......?
【发布时间】:2014-05-22 14:56:12
【问题描述】:

我在设置命令处理架构时遇到了一些问题。我希望能够创建许多从 ICommand 派生的不同命令;然后,创建多个从 ICommandHandler 派生的不同命令处理程序;

这是我已经开始定义的接口和类:

interface ICommand {}

class CreateItemCommand : ICommand {}

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

class CreateItemCommandHandler : ICommandHandler<CreateItemCommand> {
    public void Handle(CreateItemCommand command) {
        // Handle the command here
    }
}

我有一个可以创建适当类型命令的辅助类:

class CommandResolver {
    ICommand GetCommand(Message message) {
        return new CreateItemCommand(); // Handle other commands here
    }
}

还有一个帮助类,用于创建适当的处理程序;这就是我遇到麻烦的地方:

class CommandHandlerResolver {
    public ICommandHandler<TCommand> GetHandler<TCommand>(TCommand command) {

        // I'm using Ninject and have an instance of an IKernel 
        // The following code throws an exception despite having a proper binding
        //    _kernel.GetService(typeof(ICommandHandler<TCommand>))

        var bindingType = typeof(ICommandHandler<>).MakeGenericType(command.GetType());
        var handler = _kernel.GetService(bindingType);
        return handler as ICommandHandler<TCommand>; 
        // handler will be null after the cast
    }
}

这里是主要的运行方法

CommandResolver _commandResolver;
HandlerResolver _handlerResolver;

void Run() {

    // message is taken from a queue of messages    

    var command = _commandResolver.GetCommand(message);

    var handler = _handlerResolver.GetHandler(command);
    // handler will always be null

    handler.Handle(command);
}

我可以想出几种不同的方法来重构代码,我确信可以避免这个问题,但我发现自己对这个问题有点困惑,想了解更多发生的事情。

这个设计看起来应该可行。

【问题讨论】:

  • 1) handler as CommandHandler&lt;TCommand&gt;; 为什么要转换到类,而不是接口? 2) 为什么是as 演员?演员阵容不应该失败。 |将其替换为return (ICommandHandler&lt;TCommand&gt;)handler;
  • 考虑 1) 将 ICommandHandler 替换为代表。 2) 考虑将其TCommand 类型参数设为in 参数(逆变)。
  • 我试图使 TCommand 逆变,但它没有效果。据我了解,这只会影响我将通用 ICommandHandler 类型作为参数传递给方法,而不是我怀疑我遇到的分配问题,即将 CreateItemCommandHandler 分配给 ICommandHandler
  • @Sambo 是的,当你有应该处理 CreateItem 命令的东西时会发生什么?例如缓存操作以及您的业务逻辑或记录器。 CreateItemHandler2?始终以组件的功能命名组件,而不是它们的使用方式
  • @GeorgeMauer 我认为这里的设计意味着每个命令类型必须有一个处理程序,该处理程序的工作是执行该命令。缓存操作或日志记录等其他问题不会使用ICommandHandler 机制,而是使用其他方式,例如ICommandHandlingInterceptor 或其他。从一般意义上说,根据用途而不是用途来命名事物是正确的,但这里两者是相同的。

标签: c# generics architecture covariance contravariance


【解决方案1】:

问题

您的问题是您混合了静态类型和运行时类型:您编写的代码依赖于已构造的泛型类型,但随后您使用基本接口类型调用它。

让我们看看你的主要流程:

您的CommandResolver 始终返回静态类型ICommand。当你说:

var command = _commandResolver.GetCommand(message);
var handler = _handlerResolver.GetHandler(command);

command 的类型绑定到ICommand,然后传递给GetHander,后者调用GetHandler&lt;ICommand&gt;。也就是说,此调用中的TCommand 始终绑定到ICommand

这是这里的主要问题。因为TCommand总是ICommand,所以:

_kernel.GetService(typeof(ICommandHandler<TCommand>))

...不起作用(它寻找ICommandHandler&lt;ICommand&gt; 而内核没有它);即使它确实有效,您也必须将其返回为ICommandHandler&lt;ICommand&gt;,因为这是该方法的返回类型。

通过在不知道(在编译时)命令的真实类型的情况下调用GetHandler,您将失去有效使用泛型的能力,TCommand 变得毫无意义。

因此,您尝试解决此问题:您的解析器使用命令的运行时类型(command.GetType()) 反射性地构造类​​型ICommandHandler&lt;SomeCommandType&gt; 并尝试找到那个 在内核中。

假设您为该类型注册了一些东西,您将获得一个ICommandHandler&lt;SomeCommandType&gt;,然后您将尝试将其转换为ICommandHandler&lt;ICommand&gt;(请记住TCommand 绑定到ICommand)。这当然行不通,除非TCommandICommandHandler&lt;TCommand&gt; 中声明为协变,因为您正在向上 转换类型层次结构;但即使它确实如此,那也不是你想要的,因为无论如何你会用 ICommandHandler&lt;ICommand&gt; 做什么?

简单地说:您不能将 ICommandHandler&lt;SomeCommand&gt; 转换为 ICommandHandler&lt;ICommand&gt;,因为这意味着您可以将任何类型的 ICommand 传递给它,它会很乐意处理它 - - 这不是真的。如果您想使用泛型类型参数,则必须在整个流程中将它们绑定到真正的命令类型。

解决方案

此问题的一个解决方案是在命令和命令处理程序的整个解析过程中将TCommand 绑定到实际命令类型,例如通过拥有类似FindHandlerAndHandle&lt;TCommand&gt;(TCommand command) 的东西并使用命令的运行时类型通过反射来调用它。但这又臭又笨拙,而且有一个很好的理由:你在滥用泛型

泛型类型参数旨在帮助您在编译时了解您想要的类型,或者您可以将其与另一个类型参数统一起来。在这种情况下,您不知道运行时类型,尝试使用泛型只会妨碍您。

解决这个问题的一种更简洁的方法是,当你知道命令的类型时(当你为它编写处理程序时)将上下文与你不知道它的上下文(当你试图一般地为一个通用命令)。一个很好的方法是使用“非类型化接口,类型化基类”模式:

public interface ICommandHandler // Look ma, no typeparams!
{
   bool CanHandle(ICommand command);
   void Handle(ICommand command);
}

public abstract class CommandHandlerBase<TCommand> : ICommandHandler
  where TCommand : ICommand
{
  public bool CanHandle(ICommand command) { return command is TCommand; }
  public void Handle(ICommand command) 
  {
    var typedCommand = command as TCommand;
    if (typedCommand == null) throw new InvalidCommandTypeException(command);

    Handle(typedCommand);
  }

  protected abstract void Handle(TCommand typedCommand);
}

这是连接泛型和非泛型世界的常用方法:在调用它们时使用非泛型接口,但在实现时利用泛型基类。您的主要流程现在如下所示:

public void Handle(ICommand command)
{
    var allHandlers = Kernel.ResolveAll<ICommandHandler>(); // you can make this a dependency

    var handler = allHandlers.FirstOrDefault(h => h.CanHandle(command));
    if (handler == null) throw new MissingHandlerException(command);

    handler.Handle(command);
}

这在某种意义上也更加健壮,因为命令的实际运行时类型不必与处理程序的类型一一匹配,因此如果您有 ICommandHandler&lt;SomeBaseCommandType&gt; 它可以处理命令SomeDerivedCommandType 类型的,因此您可以为命令类型层次结构中的中间基类构建处理程序,或使用其他继承技巧。

【讨论】:

  • 感谢出色的解释和代码示例。我现在做错了什么是有道理的。我试图做的一件事(原始问题中没有提到)是让 ICommandHandler 的一个实现能够处理许多不同的命令。我想实现另一个 CommandHandlerBase 作为 CommandHandlerBase 将是做到这一点的一种方法。感谢您的精彩回答!
  • 使用无类型接口的好处是你可以轻松地实现它来做任何你想做的事情——处理两种或十几种命令(使用例如命令类型和句柄之间的映射)委托),或仅处理具有某些属性的命令等。抽象基类仅在为特定类型的命令构建处理程序的常见情况下为您提供帮助,但您根本不必使用它。
猜你喜欢
  • 1970-01-01
  • 2014-09-01
  • 2011-10-06
  • 2016-03-22
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-09-01
  • 1970-01-01
相关资源
最近更新 更多