我在设置命令处理架构时遇到了一些问题。我希望能够创建从i command派生的许多不同的命令;然后,创建从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
    }
}

我有一个helper类,可以创建适当类型的命令:
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)。当然,这是行不通的,除非TCommandICommandHandler<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类型的命令,因此可以为命令类型层次结构中的中间基类生成处理程序,或使用其他继承技巧。

08-26 07:34