矛盾?协方差?什么';这个通用体系结构错了吗
本文关键字:错了 体系结构 方差 什么 矛盾 | 更新日期: 2023-09-27 18:04:42
我在设置命令处理体系结构时遇到了一些问题。我希望能够从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);
}
我可以想出几种不同的方法来重构代码,我相信这些方法可以避免这个问题,但我发现自己对这个问题有点困惑,想了解更多的情况
这个设计看起来应该可行。
问题
您的问题是混合了静态类型和运行时类型:您编写的代码依赖于构造的泛型类型,但随后使用基本接口类型调用它。
让我们来了解一下您的主要流程:
您的CommandResolver
总是返回静态类型ICommand
。当你说:
var command = _commandResolver.GetCommand(message);
var handler = _handlerResolver.GetHandler(command);
command
的类型绑定到ICommand
,然后传递到GetHander
,后者调用GetHandler<ICommand>
。也就是说,此调用中的TCommand
始终绑定到ICommand
。
这是这里的主要问题。由于TCommand
总是ICommand
,因此执行:
_kernel.GetService(typeof(ICommandHandler<TCommand>))
不起作用(它查找ICommandHandler<ICommand>
,而内核没有它(;即使它确实有效,也必须将其作为ICommandHandler<ICommand>
返回,因为这是该方法的返回类型。
通过在不知道(编译时(命令的真实类型的情况下调用GetHandler
,您失去了有效使用泛型的能力,TCommand
变得毫无意义。
因此,您可以尝试解决这个问题:您的解析器使用命令的运行时类型(command.GetType()
(来反射地构造类型ICommandHandler<SomeCommandType>
,并尝试在内核中找到。
假设您已经为该类型注册了一些内容,那么您将获得一个ICommandHandler<SomeCommandType>
,然后尝试将其强制转换为ICommandHandler<ICommand>
(请记住,TCommand
绑定到ICommand
(。这当然不会起作用,除非TCommand
在ICommandHandler<TCommand>
中被声明为协变,因为您正在类型层次结构中向上投射;但即使是这样,那也不是你想要的,因为无论如何,你会用ICommandHandler<ICommand>
做什么?
简单地说:你不能将ICommandHandler<SomeCommand>
强制转换为ICommandHandler<ICommand>
,因为这意味着你可以传递任何类型的ICommand
,它会很乐意处理它——这不是真的。如果要使用泛型类型参数,则必须在整个流程中将它们绑定到真实的命令类型。
解决方案
这个问题的一个解决方案是在命令和命令处理程序的整个解析过程中保持TCommand
与真实命令类型的绑定,例如,通过具有类似FindHandlerAndHandle<TCommand>(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<SomeBaseCommandType>
,它可以处理SomeDerivedCommandType
类型的命令,因此您可以在命令类型层次结构中为中间基类构建处理程序,或者使用其他继承技巧。