Ковариация интерфейса контравариантность: почему это не компиляция?


Я хочу реализовать CommandBus, который может Dispatch Некоторые Commands к CommandHandlers.

  • A Command - это простое A DTO, описывающее, что должно произойти. Например: "счетчик инкремента на 5"
  • A CommandHandler способен обрабатывать точный тип Command.
  • CommandBus берет Command и выполняет CommandHandler, который способен его обработать.
Написанный мною код не компилируется.

Компилятор жалуется cannot convert from 'IncrementHandler' to '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;
  }
}
3 3

3 ответа:

Я не понимаю почему, потому что 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 принимает a Command, а A Decrement является a Command.

Так что же произошло? Вы только что приняли декрет команда ih, через h, но ih - это IncrementHandler, который знает только, как обрабатывать приращения.

Поскольку это бессмысленно, что-то здесь должно быть незаконным; какую линию вы хотели бы сделать незаконной? Команда C# решила, что преобразование-это то, что должно быть незаконным.

Более конкретно:

Ваша программа использует отражение в попытке завершить проверку безопасности системы типов, а затем вы жалуетесь, что тип система останавливает вас, когда вы пишете что-то небезопасное. Почему вы вообще используете дженерики ?

Генераторы предназначены (частично) для обеспечения безопасности типов, и затем вы выполняетеотправку, основанную на отражении . Это не имеет никакого смысла; не предпринимайте шагов по повышению безопасности типов , а затем делайте героические усилия, чтобы обойти их.

Очевидно, что вы хотите обойти безопасность типов, поэтомувообще не используйте дженерики . Просто сделайте интерфейс ICommand и класс Handler, который принимает команду, а затем имеет некоторый механизм для разработки того, как отправлять команды.

Но вот чего я не понимаю, так это почему вообще существуют два вида вещей. Если вы хотите выполнить команду, то почему бы просто не поместить логику выполнения на объект команды?

Есть также другие шаблоны проектирования, которые вы можете использовать здесь, кроме этого неуклюжего поиска по словарю, основанного на типах. Например:

  • Обработчик команд может иметь метод, который принимает команду и возвращает логическое значение, независимо от того, может ли обработчик обработать эту команду или нет. Теперь у вас есть список обработчиков команд, приходит команда, и вы просто запускаете список, спрашивая: "Вы мой обработчик?- пока ты не найдешь его. Если поиск O(n) слишком медленный, то создайте кэш MRU или запомните результат или что-то подобное, и амортизированное поведение улучшится.

  • Логика отправки может быть введена в сам обработчик команд. Обработчику команд присваивается команда; она либо выполняет ее, либо рекурсирует, вызывая обработчик родительской команды. Таким образом, вы можете построить график обработчиков команд, которые при необходимости передают работу друг другу. (Это в основном то, как QueryService работает в COM.)

Проблема здесь в том, что 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;
    }
}

Проблема здесь в том, что ваше определение Handler<TCommand> требует, чтобы TCommand было одновременно ковариантным и контравариантным - и это недопустимо.

Чтобы передать Handler<Increment> в конструктор CommandBus (который ожидает Handler<Command>), Необходимо объявить Command как параметр ковариантного типа в Handler, например:

public interface Handler<out TCommand> where TCommand : Command

Внесение этого изменения позволяет вам передавать Handler<AnythingThatImplementsCommand> везде, где запрашивается Handler<Command>, поэтому ваш конструктор для 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 и иметь что-то похожее на безопасность типа.