接口的协变与逆变: 为什么这个代码无法编译?

4
我想实现一个CommandBus,它可以将一些命令(Commands)派发(Dispatch)给命令处理器(CommandHandlers)。
- Command是一个简单的数据传输对象(DTO),描述应该发生的事情。例如:“将计数器增加5”。 - CommandHandler能够处理特定类型的Command。 - CommandBus获取一个Command并执行能够处理它的CommandHandler。
我编写的代码无法编译。
编译器报错“无法从'IncrementHandler'转换为'Handler<Command>'”。我不明白为什么,因为IncrementHandler实现了Handler<Increment>,而Increment实现了Command。
我尝试在泛型接口上使用in和out修饰符,但这并不能解决问题。
有没有只使用接口就能实现这个功能的方法?
[TestClass]
public class CommandBusTest
{
  [TestMethod]
  public void DispatchesProperly()
  {
    var handler = new IncrementHandler(counter: 0);
    var bus = new CommandBus(handler); // <--Doesn't compile: cannot convert from 'IncrementHandler' to 'Handler<Command>'
    bus.Dispatch(new Increment(5));
    Assert.AreEqual(5, handler.Counter);
  }
}

public class CommandBus
{
  private readonly Dictionary<Type, Handler<Command>> handlers;

  public CommandBus(params Handler<Command>[] handlers)
  {
    this.handlers = handlers.ToDictionary(
      h => h.HandledCommand,
      h => h);
  }

  public void Dispatch(Command commande) { /*...*/ }
}

public interface Command { }

public interface Handler<TCommand> where TCommand : Command
{
  Type HandledCommand { get; }
  void Handle(TCommand command);
}

public class Increment : Command
{
  public Increment(int value) { Value = value; }

  public int Value { get; }
}

public class IncrementHandler : Handler<Increment>
{
  // Handler<Increment>
  public Type HandledCommand => typeof(Increment);
  public void Handle(Increment command)
  {
    Counter += command.Value;
  }
  // Handler<Increment>

  public int Counter { get; private set; }

  public IncrementHandler(int counter)
  {
    Counter = counter;
  }
}

我不明白为什么你要传递一个数组。你有尝试过这样做吗? public CommandBus(Handler <Command> handlers) {   this.handlers = handlers.ToDictionary(     h => h.HandledCommand,     h => h); } - Kevin Raffay
我使用了 params,因为我想要能够传递任意数量的处理程序,就像这样:new CommandBus(new AHander(), new BHandler(), new CHandler())。 该总线将包含多个处理程序,因为会有多种类型的命令。 - Christophe Cadilhac
params 语法没问题,它将出现在构造函数中的 Handler<Command>[] 中,并且只有一个条目。 - Cee McSharpface
1
鉴于 Handler<TCommand> 的定义,您只能将其逆变为 TCommand,但是这样 IncrementHandler 就与 Handler<Command> 不兼容,因为 CommandIncrement 的超类型而不是所需的子类型。没有安全的方法可以解决这个问题,所以您必须在某个地方进行转换。 - Lee
3个回答

8
我不明白原因,因为IncrementHandler实现了Handler<Increment>,而Increment实现了Command
让我们解决你的误解,然后其余部分就会变得清楚。
假设你想做的是合法的。有什么问题吗?
IncrementHandler ih = whatever;
Handler<Command> h = ih; // This is illegal. Suppose it is legal.

现在我们要创建一个类。
public class Decrement : Command { ... }

现在我们将其传递给h:

Decrement d = new Decrement();
h.Handle(d);

这是合法的,因为Handler<Command>.Handle接受一个Command,而Decrement是一个Command
那么发生了什么?你刚刚通过h将递减命令传递给ih,但是ih是一个只知道如何处理递增命令的IncrementHandler
既然这是荒谬的,那么这里必须有些不合法的东西;你想让哪一行不合法?C#团队决定转换应该是不合法的。
更具体地说: 您的程序正在尝试绕过类型系统的安全检查使用反射,并且在编写不安全内容时抱怨类型系统阻止了您。为什么要使用泛型呢?
泛型部分是为了确保类型安全,然后你使用反射进行调度。这没有任何意义;不要采取措施来增加类型安全,然后进行英勇努力来规避它们。
明显,您希望规避类型安全,因此根本不要使用泛型。只需创建一个ICommand接口和一个接受命令的Handler类,然后有一些机制来解决如何调度命令。
但是我不明白为什么会有两种东西。如果您想执行命令,那么为什么不将执行逻辑放在命令对象上呢?
这里还有其他设计模式可以使用,而不是基于类型的笨重字典查找。例如:
  • 一个命令处理程序可以有一个方法,它接受一个命令并返回一个布尔值,指示处理程序是否可以处理此命令。现在你有一个命令处理程序列表,一个命令进来了,你只需沿着列表询问“你是我的处理程序吗?”直到找到一个。如果O(n)查找太慢,则构建MRU缓存或记忆化结果或类似的东西,摊销行为将得到改善。

  • 分发逻辑可以放入命令处理程序本身中。给定一个命令,一个命令处理程序要么执行它,要么递归调用其父命令处理程序。因此,您可以构建一个命令处理程序图形,根据需要将工作推迟给彼此。(这基本上就是COM中的QueryService的工作原理。)


1
典型的情况——我用了几百个单词来尝试解释某件事,然后Eric Lippert出现了,用5行代码更好地解释了它。 ;) - BJ Myers
谢谢Eric。在写这篇文章的时候,我想你可能会出现并回答 :) 你提出了关于我使用的模式可能改进的好观点。这段代码是关于实现最近一次聚会上我听说的CQRS版本的。所以我尝试了建议的设计,并遇到了编译问题。我认为这是一个需要改变某些东西的信号。我会研究一下的! - Christophe Cadilhac

0
问题在于Increment实现了Command(我在下面的代码中将其重命名为ICommand以使其更清晰)。 因此,它不再被接受为Handler<Command>,这是构造函数所期望的(子类型而不是所需的超类型,正如@Lee在评论中指出的那样)。

如果您可以概括使用ICommand,它将起作用:

public class CommandBusTest
{
    public void DispatchesProperly()
    {
        var handler = new IncrementHandler(counter: 0);
        var bus = new CommandBus((IHandler<ICommand>)handler); 
        bus.Dispatch(new Increment(5));
    }
}

public class CommandBus
{
    private readonly Dictionary<Type, IHandler<ICommand>> handlers;

    public CommandBus(params IHandler<ICommand>[] handlers)
    {
        this.handlers = handlers.ToDictionary(
          h => h.HandledCommand,
          h => h);
    }

    public void Dispatch(ICommand commande) { /*...*/ }
}

public interface ICommand { int Value { get; } }

public interface IHandler<TCommand> where TCommand : ICommand
{
    Type HandledCommand { get; }
    void Handle(TCommand command);
}

public class Increment : ICommand
{
    public Increment(int value) { Value = value; }

    public int Value { get; }
}

public class IncrementHandler : IHandler<ICommand>
{
    // Handler<ICommand>
    public Type HandledCommand => typeof(Increment);

    public void Handle(ICommand command)
    {
        Counter += command.Value;
    }

    // Handler<ICommand>

    public int Counter { get; private set; }

    public IncrementHandler(int counter)
    {
        Counter = counter;
    }
}

0

这里的问题在于你对 Handler<TCommand> 的定义要求 TCommand 同时具有协变性和逆变性 - 这是不允许的。

为了将 Handler<Increment> 传递到期望一个 Handler<Command>CommandBus 构造函数中,你必须在 Handler 中将 Command 声明为协变类型参数,像这样:

public interface Handler<out TCommand> where TCommand : Command

进行这个改变,使得你可以在任何需要 Handler<Command> 的地方传递一个 Handler<AnythingThatImplementsCommand>,因此你的 CommandBus 构造函数现在可以工作了。
但是,这引入了对下面一行代码的新问题:
void Handle(TCommand command);

由于TCommand是协变的,因此可以将Handler<Increment>分配给Handler<Command>引用。然后,您将能够调用Handle方法,但传递实现Command任何内容 - 显然这不会起作用。为了使调用正确,您必须允许TCommand成为逆变

由于您无法同时执行两者,因此必须在某个地方做出让步。一种方法是在Handler<TCommand>中使用协变,但在Handle方法中强制进行显式转换,如下所示:

public interface Handler<out TCommand> where TCommand : Command
{
    Type HandledCommand { get; }
    void Handle(Command command);
}

public class IncrementHandler : Handler<Increment>
{
    public void Handle(Command command)
    {
        Counter += ((Increment)command).Value;
    }
}

这并不能防止某人创建一个IncrementHandler,然后传入错误类型的Command,但如果处理程序仅由CommandBus使用,则可以在CommandBus.Dispatch中检查类型,并具有类似类型安全的功能。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接