Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Contravariance? Covariance? What's wrong with this generic architecture...?

I'm having some problems setting up a command handling architecture. I want to be able to create a number of different commands derived from ICommand; then, create a number of different command handlers derived from ICommandHandler;

Here's the interface and classes I have begun to define:

interface ICommand {}

class CreateItemCommand : ICommand {}

interface ICommandHandler<TCommand> where TCommand : ICommand {
    void Handle(TCommand command);
}

class CreateItemCommandHandler : ICommandHandler<CreateItemCommand> {
    public void Handle(CreateItemCommand command) {
        // Handle the command here
    }
}

I have a helper class that can create the appropriate type of command:

class CommandResolver {
    ICommand GetCommand(Message message) {
        return new CreateItemCommand(); // Handle other commands here
    }
}

And, a helper class that creates the appropriate handler; this is where I'm having trouble:

class CommandHandlerResolver {
    public ICommandHandler<TCommand> GetHandler<TCommand>(TCommand command) {

        // I'm using Ninject and have an instance of an IKernel 
        // The following code throws an exception despite having a proper binding
        //    _kernel.GetService(typeof(ICommandHandler<TCommand>))

        var bindingType = typeof(ICommandHandler<>).MakeGenericType(command.GetType());
        var handler = _kernel.GetService(bindingType);
        return handler as ICommandHandler<TCommand>; 
        // handler will be null after the cast
    }
}

Here's the main running method

CommandResolver _commandResolver;
HandlerResolver _handlerResolver;

void Run() {

    // message is taken from a queue of messages    

    var command = _commandResolver.GetCommand(message);

    var handler = _handlerResolver.GetHandler(command);
    // handler will always be null

    handler.Handle(command);
}

I can think of a few different ways to refactor the code that I'm sure would avoid the issue, but I found myself a bit perplexed by the problem and wanted to understand more of what was going on.

This design looks like it should work.

like image 602
Sambo Avatar asked May 22 '14 14:05

Sambo


People also ask

What is covariance and contravariance in generics?

Covariance and contravariance are terms that refer to the ability to use a more derived type (more specific) or a less derived type (less specific) than originally specified. Generic type parameters support covariance and contravariance to provide greater flexibility in assigning and using generic types.

Why is covariance and contravariance important?

In C#, covariance and contravariance enable implicit reference conversion for array types, delegate types, and generic type arguments. Covariance preserves assignment compatibility and contravariance reverses it.

What is covariance and contravariance in generics in Java?

Covariance can be translated as "different in the same direction," or with-different, whereas contravariance means "different in the opposite direction," or against-different. Covariant and contravariant types are not the same, but there is a correlation between them. The names imply the direction of the correlation.

What is the difference between covariance and contravariance in delegates?

Covariance permits a method to have return type that is more derived than that defined in the delegate. Contravariance permits a method that has parameter types that are less derived than those in the delegate type.


1 Answers

Problem

Your problem is that you're mixing static types and runtime types: you're writing code that relies on having constructed generic types, but then you're invoking it with the base interface types.

Let's follow through your main flow:

Your CommandResolver always returns the static type ICommand. When you say:

var command = _commandResolver.GetCommand(message);
var handler = _handlerResolver.GetHandler(command);

The type of command is bound to ICommand and then gets passed to GetHander, which invokes GetHandler<ICommand>. That is, TCommand in this invocation is always bound to ICommand.

This is the main problem here. Since TCommand is always ICommand, doing:

_kernel.GetService(typeof(ICommandHandler<TCommand>))

...doesn't work (it looks for a ICommandHandler<ICommand> and the kernel doesn't have it); and even if it did work, you'd have to return it as ICommandHandler<ICommand> since that's the return type of the method.

By calling GetHandler without knowing (at compile time) the real type of the command, you lost the ability to use generics effectively and TCommand becomes meaningless.

So, you try to work around this: your resolver uses the command's runtime type(command.GetType()) to reflectively construct the type ICommandHandler<SomeCommandType> and tries to find that in the kernel.

Assuming you have something registered for that type, you'll get an ICommandHandler<SomeCommandType>, which you'll then try to cast to ICommandHandler<ICommand> (remember that TCommand is bound to ICommand). This of course won't work, unless TCommand is declared covariant in ICommandHandler<TCommand>, since you're casting up the type hierarchy; but even if it did, that's not what you want because what would you do with a ICommandHandler<ICommand> anyway?

Simply put: you can't cast an ICommandHandler<SomeCommand> to a ICommandHandler<ICommand> because that would imply that you can pass it any kind of ICommand and it'll happily handle it -- which is not true. If you want to use generic type parameters, you'll have to keep them bound to the real command type throughout the entire flow.

Solution

One solution to this problem is to keep TCommand bound to the real command type throughout the resolution of both the command and the command handler, e.g. by having something like FindHandlerAndHandle<TCommand>(TCommand command) and invoking it by reflection using the command's runtime type. But this is smelly and clumsy, and for a good reason: you're abusing generics.

Generic type-parameters are meant to help you when you know, in compile time, the type you want, or what you can unify it with another type parameter. In cases such as these, where you don't know that runtime type, trying to use generics only gets in your way.

A cleaner way to solve this is by separating the context when you know the command's type (when you write a handler for it) from the context when you don't know it (when you try to generically find a handler for a generic command). A good way to do that is by using an "untyped interface, typed base class" pattern:

public interface ICommandHandler // Look ma, no typeparams!
{
   bool CanHandle(ICommand command);
   void Handle(ICommand command);
}

public abstract class CommandHandlerBase<TCommand> : ICommandHandler
  where TCommand : ICommand
{
  public bool CanHandle(ICommand command) { return command is TCommand; }
  public void Handle(ICommand command) 
  {
    var typedCommand = command as TCommand;
    if (typedCommand == null) throw new InvalidCommandTypeException(command);

    Handle(typedCommand);
  }

  protected abstract void Handle(TCommand typedCommand);
}

This is a common way to bridge the generic and non-generic worlds: you use the non-generic interfaces when invoking them, but take advantage of the generic base-class when implementing. Your main flow now looks like this:

public void Handle(ICommand command)
{
    var allHandlers = Kernel.ResolveAll<ICommandHandler>(); // you can make this a dependency

    var handler = allHandlers.FirstOrDefault(h => h.CanHandle(command));
    if (handler == null) throw new MissingHandlerException(command);

    handler.Handle(command);
}

This is also somewhat more robust in the sense that the actual runtime type of the command doesn't have to match one-by-one with the type of the handler, so if you have a ICommandHandler<SomeBaseCommandType> it can handle commands of type SomeDerivedCommandType, so you can build handlers for intermediate base classes in the command type hierarchy, or use other inheritance tricks.

like image 181
Avish Avatar answered Oct 12 '22 21:10

Avish