Ковариация интерфейса контравариантность: почему это не компиляция?
Я хочу реализовать 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
реализует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
принимает 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
и иметь что-то похожее на безопасность типа.