Ковариация интерфейса контравариантность: почему это не компиляция?
Я хочу реализовать 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 ответа:
Давайте исправим ваше недоразумение, и тогда все остальное станет ясно. Предположим, то, что вы хотели сделать, было законным. Что же идет не так?Я не понимаю почему, потому что
IncrementHandlerреализуетHandler<Increment>иIncrementреализуетCommandIncrementHandler 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принимает aCommand, а ADecrementявляется aCommand.Так что же произошло? Вы только что приняли декрет команда
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и иметь что-то похожее на безопасность типа.