逆变?协方差?这个通用架构出了什么问题......?

Sam*_*mbo 7 c# architecture generics covariance contravariance

我在设置命令处理架构时遇到了一些问题.我希望能够创建从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
    }
}
Run Code Online (Sandbox Code Playgroud)

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

class CommandResolver {
    ICommand GetCommand(Message message) {
        return new CreateItemCommand(); // Handle other commands here
    }
}
Run Code Online (Sandbox Code Playgroud)

并且,一个帮助类创建适当的处理程序; 这是我遇到麻烦的地方:

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
    }
}
Run Code Online (Sandbox Code Playgroud)

这是主要的运行方法

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);
}
Run Code Online (Sandbox Code Playgroud)

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

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

Avi*_*ish 7

问题

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

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

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

var command = _commandResolver.GetCommand(message);
var handler = _handlerResolver.GetHandler(command);
Run Code Online (Sandbox Code Playgroud)

command绑定的类型ICommand然后传递给GetHander,调用GetHandler<ICommand>.也就是说,TCommand在这个调用中始终是绑定的ICommand.

这是这里的主要问题.由于TCommand始终 ICommand,这样做:

_kernel.GetService(typeof(ICommandHandler<TCommand>))
Run Code Online (Sandbox Code Playgroud)

...不起作用(它寻找一个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);
}
Run Code Online (Sandbox Code Playgroud)

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

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);
}
Run Code Online (Sandbox Code Playgroud)

从命令的实际运行时类型不必逐个匹配处理程序的类型的意义上来说,这也有点强大,所以如果你有一个ICommandHandler<SomeBaseCommandType>它可以处理类型的命令SomeDerivedCommandType,那么你可以在命令类型层次结构中为中间基类构建处理程序,或使用其他继承技巧.