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

时间:2014-05-22 14:56:13

标签: c# generics architecture 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
    }
}

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

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 个答案:

答案 0 :(得分:7)

问题

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

让我们按照您的主要流程进行操作:

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

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

command的类型绑定到ICommand,然后传递给调用GetHander的{​​{1}}。也就是说,此调用中的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 1}})。这当然不会起作用,除非在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); } 的命令,因此您可以在命令类型层次结构中为中间基类构建处理程序,或使用其他继承技巧。