Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Interface covariance contravariance : why is this not compiling?

I want to implement a CommandBus that can Dispatch some Commands to CommandHandlers.

  • A Command is a simple a DTO describing what should happen. For instance : "Increment counter by 5"
  • A CommandHandler is able to handle a precise type of Command.
  • The CommandBus takes a Command and executes the CommandHandler that is able to handle it.

The code I wrote does not compile.

Compiler complains cannot convert from 'IncrementHandler' to 'Handler<Command>'. I don't understand why, because IncrementHandler implements Handler<Increment> and Increment implements Command

I've tried both in and out modifiers on the generic interfaces, it doesn't solve the problem.

Is there a way to achieve this with only interfaces ?

[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;
  }
}
like image 502
Christophe Cadilhac Avatar asked Feb 05 '23 09:02

Christophe Cadilhac


1 Answers

I don't understand why, because IncrementHandler implements Handler<Increment> and Increment implements Command

Let's fix your misunderstanding, and then the rest will become clear.

Suppose what you wanted to do was legal. What goes wrong?

IncrementHandler ih = whatever;
Handler<Command> h = ih; // This is illegal. Suppose it is legal.

now we make a class

public class Decrement : Command { ... }

And now we pass it to h:

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

This is legal, because Handler<Command>.Handle takes a Command, and a Decrement is a Command.

So what happened? You just passed a decrement command to ih, via h, but ih is an IncrementHandler that only knows how to handle increments.

Since that is nonsensical, something in here has to be illegal; which line would you like to be illegal? The C# team decided that the conversion is the thing that should be illegal.

More specifically:

Your program is using reflection in an attempted end-run around the type system's safety checks, and then you are complaining that the type system is stopping you when you write something unsafe. Why are you using generics at all?

Generics are (in part) to ensure type safety, and then you are doing a dispatch based on reflection. This doesn't make any sense; don't take steps to increase type safety and then do heroic efforts to work around them.

Plainly you wish to work around type safety, so don't use generics at all. Just make an ICommand interface and a Handler class that takes a command, and then have some mechanism for working out how to dispatch commands.

What I don't understand though is why there are two kinds of things at all. If you want to execute a command, then why not simply put the execution logic on the command object?

There are also other design patterns you could use here other than this clunky dictionary lookup based on types. For example:

  • a command handler could have a method that takes a command and returns a boolean, whether the handler can handle this command or not. Now you have a list of command handlers, a command comes in, and you just run down the list asking "are you my handler?" until you find one. If O(n) lookup is too slow, then build a MRU cache or memoize the result or some such thing, and the amortized behaviour will improve.

  • the dispatch logic could be put into the command handler itself. A command handler is given a command; it either executes it, or it recurses, calling its parent command handler. You can thus build a graph of command handlers that defer work to each other as necessary. (This is basically how QueryService works in COM.)

like image 70
Eric Lippert Avatar answered Feb 06 '23 22:02

Eric Lippert