逆变? 协方差? 这个通用架构有什么问题……?

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

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

interface ICommand {} class CreateItemCommand : ICommand {} interface ICommandHandler where TCommand : ICommand { void Handle(TCommand command); } class CreateItemCommandHandler : ICommandHandler { 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 GetHandler(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)) var bindingType = typeof(ICommandHandler).MakeGenericType(command.GetType()); var handler = _kernel.GetService(bindingType); return handler as ICommandHandler; // 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); } 

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

这个设计看起来应该有效。

问题

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

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

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

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

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

这是这里的主要问题 。 由于TCommand 始终ICommand ,所以:

 _kernel.GetService(typeof(ICommandHandler)) 

…不起作用(它查找ICommandHandler并且内核没有它); 即使它确实有效,你也必须将它作为ICommandHandler返回,因为这是该方法的返回类型。

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

因此,您尝试解决此问题:您的解析器使用命令的运行时类型command.GetType() )reflection性地构造类​​型ICommandHandler并尝试在内核中找到

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

简单地说:你不能将ICommandHandler转换为ICommandHandler因为这意味着你可以将它传递给任何类型的ICommand并且它会愉快地处理它 – 这不是真的。 如果要使用generics类型参数,则必须在整个流程中将它们绑定到实际命令类型。

该问题的一个解决方案是在命令和命令处理程序的整个分辨率中保持TCommand绑定到实际命令类型,例如通过使用FindHandlerAndHandle(TCommand command)并使用命令的运行时类型通过reflection调用它。 。 但这很臭,很笨拙,原因很简单: 你在滥用仿制品

当您在编译时知道所需的类型或者可以将其与另一个类型参数统一时,通用类型参数可以帮助您。 在这些情况下,如果您不知道运行时类型,尝试使用generics只会妨碍您。

解决这个问题的一种更简洁的方法是,当您不知道命令的类型时(当您为它编写处理程序时)分离上下文(当您尝试一般性地找到generics命令的处理程序时) 。 一个好方法是使用“无类型接口,类型化基类”模式:

 public interface ICommandHandler // Look ma, no typeparams! { bool CanHandle(ICommand command); void Handle(ICommand command); } public abstract class CommandHandlerBase : 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); } 

这是桥接generics和非generics世界的常用方法:在调用它们时使用非generics接口,但在实现时利用通用基类。 您的主要流程现在看起来像这样:

 public void Handle(ICommand command) { var allHandlers = Kernel.ResolveAll(); // 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它可以处理类型的命令SomeDerivedCommandType ,因此您可以在命令类型层次结构中为中间基类构建处理程序,或使用其他inheritance技巧。