How to instantiate a class as the interface that it derives from with constrained generic type parameter

172 views Asked by At

There's the following interface which defines a packet.

public interface IPacket
{
    int Size { get; }
}

There are two implementations, each with its own additional property.

public class FooPacket : IPacket
{
    public int Size => 10;
    public string FooProperty { get; set; }
}

public class BarPacket : IPacket
{
    public int Size => 20;
    public string BarProperty { get; set; }
}

The above is library code I have no control over. I want to create a handler for packets

public interface IPacketHandler<T> where T : IPacket
{
    void HandlePacket(T packet) ;
}

and create two implementations for the concrete packets.

public class FooPacketHandler : IPacketHandler<FooPacket>
{
    public void HandlePacket(FooPacket packet) { /* some logic that accesses FooProperty */ }
}

public class BarPacketHandler : IPacketHandler<BarPacket>
{
    public void HandlePacket(BarPacket packet) { /* some logic that accesses BarProperty */ }
}

I'd like to inject a list of packet handlers into a class that manages packet handling so that it can be extended in the future with additional packet handlers.

public class PacketHandlerManager
{
    public PacketHandlerManager(IEnumerable<IPacketHandler<IPacket>> packetHandlers)
    {

    }
}

The trouble I'm having is when creating the injected parameter. I cannot do

var packetHandlers = new List<IPacketHandler<IPacket>>
{
    new FooPacketHandler(),
    new BarPacketHandler()
};

because I cannot create an instance like so:

IPacketHandler<IPacket> packetHandler = new FooPacketHandler();

I get the error Cannot implicitly convert type 'FooPacketHandler' to 'IPacketHandler<IPacket>. An explicit conversion exists (are you missing a cast?)

I had a look at a similar question: Casting generic type with interface constraint. In that question, OP didn't show the members of the interface, only the definition of it from a generics point of view. From what I can see, if my interface didn't use the generic type parameter as an input, I could make it covariant using the out keyword, but that doesn't apply here.

How do I achieve making manager adhere to the open-closed principle? Is my only recourse changing the interface definition to

public interface IPacketHandler
{
    void HandlePacket(IPacket packet);
}

and then casting to a particular packet in the implementation?

3

There are 3 answers

2
Wiktor Zychla On

The core of the issue is that ultimately you would call your handler passing a concrete packet (of a concrete type) to it as an argument, even though you hide the argument behind IPacket.

Somehow then, trying to call the HandlePacket( FooPacket ) with BarPacket argument would have to fail, the only question is when/where it fails.

As you already noticed, introducing the generic parameter to the packet handler makes it fail in the compile time and there is no easy workaround over it.

Your idea to drop the generic parameter, i.e. to have

public interface IPacketHandler
{
   void HandlePacket(IPacket packet);
}

is a possible solution. It however pushes the possible failure to the runtime, where you now have to check if a handler is called with inappropriate argument.

What you could also do is to make this runtime check more explicit by introducing a contract for it:

public interface IPacketHandler
{
   bool CanHandlePacket(IPacket packet);
   void HandlePacket(IPacket packet);
}

This makes it cleaner for the consumer to safely call HandlePacket - assuming they get a positive result from calling CanHandlePacket before.

For example, a possible naive loop over a list of packets and calling your handlers would become

foreach ( var packet in _packets )
  foreach ( var handler in _handlers )
    if ( handler.CanHandlePacket(packet) )
      handler.HandlePacket(packet);
5
Matthew Watson On

You can solve this with a little bit of reflection.

Firstly, for convenience (and to help slightly with type-safety), introduce a "Tag" interface which all your IPacketHandler<T> interfaces will implement:

public interface IPacketHandlerTag // "Tag" interface.
{
}

This is not really necessary, but it means you can use IEnumerable<IPacketHandlerTag> instead of IEnumerable<object> later on, which does make things a little more obvious.

Then your IPacketHandler<T> interface becomes:

public interface IPacketHandler<in T> : IPacketHandlerTag where T : IPacket
{
    void HandlePacket(T packet);
}

Now you can write a PacketHandlerManager that uses reflection to pick out the method to use to handle a packet, and add it to a dictionary like so:

public class PacketHandlerManager
{
    public PacketHandlerManager(IEnumerable<IPacketHandlerTag> packetHandlers)
    {
        foreach (var packetHandler in packetHandlers)
        {
            bool appropriateMethodFound = false;
            var handlerType = packetHandler.GetType();
            var allMethods  = handlerType.GetMethods(BindingFlags.Public | BindingFlags.Instance);

            foreach (var method in allMethods.Where(m => m.Name == "HandlePacket"))
            {
                var args = method.GetParameters();

                if (args.Length == 1 && typeof(IPacket).IsAssignableFrom(args[0].ParameterType))
                {
                    _handlers.Add(args[0].ParameterType, item => method.Invoke(packetHandler, new object[]{item}));
                    appropriateMethodFound = true;
                }
            }

            if (!appropriateMethodFound)
                throw new InvalidOperationException("No appropriate HandlePacket() method found for type " + handlerType.FullName);
        }
    }

    public void HandlePacket(IPacket packet)
    {
        if (_handlers.TryGetValue(packet.GetType(), out var handler))
        {
            handler(packet);
        }
        else
        {
            Console.WriteLine("No handler found for packet type " + packet.GetType().FullName);
        }
    }

    readonly Dictionary<Type, Action<IPacket>> _handlers = new Dictionary<Type, Action<IPacket>>(); 
}

If a packet handler passed to the PacketHandlerManager constructor does not implement a method called HandlePacket with a single argument that is assignable from IPacket, it will throw an InvalidOperationException.

For example, attempting to use an instance of the following class would cause the constructor to throw:

public class BadPacketHandler: IPacketHandlerTag
{
    public void HandlePacket(string packet)
    {
        Console.WriteLine("Handling string");
    }
}

Now you can call use it thusly:

    var packetHandlers = new List<IPacketHandlerTag>
    {
        new FooPacketHandler(),
        new BarPacketHandler()
    };

    var manager = new PacketHandlerManager(packetHandlers);

    var foo = new FooPacket();
    var bar = new BarPacket();
    var baz = new BazPacket();

    manager.HandlePacket(foo);
    manager.HandlePacket(bar);
    manager.HandlePacket(baz);

Putting it all together into a compilable console app:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;

namespace ConsoleApp1
{
    public interface IPacket
    {
        int Size { get; }
    }

    public class FooPacket : IPacket
    {
        public int    Size        => 10;
        public string FooProperty { get; set; }
    }

    public class BarPacket : IPacket
    {
        public int    Size        => 20;
        public string BarProperty { get; set; }
    }

    public class BazPacket : IPacket
    {
        public int    Size        => 20;
        public string BazProperty { get; set; }
    }

    public interface IPacketHandlerTag // "Tag" interface.
    {
    }

    public interface IPacketHandler<in T> : IPacketHandlerTag where T : IPacket
    {
        void HandlePacket(T packet);
    }

    public class FooPacketHandler : IPacketHandler<FooPacket>
    {
        public void HandlePacket(FooPacket packet)
        {
            Console.WriteLine("Handling FooPacket");
        }
    }

    public class BarPacketHandler : IPacketHandler<BarPacket>
    {
        public void HandlePacket(BarPacket packet)
        {
            Console.WriteLine("Handling BarPacket");
        }
    }

    public class PacketHandlerManager
    {
        public PacketHandlerManager(IEnumerable<IPacketHandlerTag> packetHandlers)
        {
            foreach (var packetHandler in packetHandlers)
            {
                bool appropriateMethodFound = false;
                var handlerType = packetHandler.GetType();
                var allMethods  = handlerType.GetMethods(BindingFlags.Public | BindingFlags.Instance);

                foreach (var method in allMethods.Where(m => m.Name == "HandlePacket"))
                {
                    var args = method.GetParameters();

                    if (args.Length == 1 && typeof(IPacket).IsAssignableFrom(args[0].ParameterType))
                    {
                        _handlers.Add(args[0].ParameterType, item => method.Invoke(packetHandler, new object[]{item}));
                        appropriateMethodFound = true;
                    }
                }

                if (!appropriateMethodFound)
                    throw new InvalidOperationException("No appropriate HandlePacket() method found for type " + handlerType.FullName);
            }
        }

        public void HandlePacket(IPacket packet)
        {
            if (_handlers.TryGetValue(packet.GetType(), out var handler))
            {
                handler(packet);
            }
            else
            {
                Console.WriteLine("No handler found for packet type " + packet.GetType().FullName);
            }
        }

        readonly Dictionary<Type, Action<IPacket>> _handlers = new Dictionary<Type, Action<IPacket>>(); 
    }

    class Program
    {
        public static void Main()
        {
            var packetHandlers = new List<IPacketHandlerTag>
            {
                new FooPacketHandler(),
                new BarPacketHandler()
            };

            var manager = new PacketHandlerManager(packetHandlers);

            var foo = new FooPacket();
            var bar = new BarPacket();
            var baz = new BazPacket();

            manager.HandlePacket(foo);
            manager.HandlePacket(bar);
            manager.HandlePacket(baz);
        }
    }
}

The output of this is:

Handling FooPacket

Handling BarPacket

No handler found for packet type ConsoleApp1.BazPacket

0
Moss On

Thanks for the answers. The solution I ended up with is this, starting with the library code:

public enum PacketType
{
    Foo,
    Bar
}

public interface IPacket
{
    PacketType Type { get; }
}

public class FooPacket : IPacket
{
    public PacketType Type => PacketType.Foo;
    public string FooProperty { get; }
}

public class BarPacket : IPacket
{
    public PacketType Type => PacketType.Bar;
    public string BarProperty { get; }
}

The above version is a better approximation of the real thing.

public interface IPacketHandler
{
    void HandlePacket(IPacket packet);
}

public abstract class PacketHandler<T> : IPacketHandler where T : IPacket
{
    public abstract PacketType HandlesPacketType { get; }

    public void HandlePacket(IPacket packet)
    {
        if (packet is T concretePacket)
        {
            HandlePacket(concretePacket);
        }
    }

    protected abstract void HandlePacket(T packet);
}

public class FooPacketHandler : PacketHandler<FooPacket>
{
    public override PacketType HandlesPacketType => PacketType.Foo;

    protected override void HandlePacket(FooPacket packet) { /* some logic that accesses FooProperty */ }
}

public class BarPacketHandler : PacketHandler<BarPacket>
{
    public override PacketType HandlesPacketType => PacketType.Bar;

    protected override void HandlePacket(BarPacket packet) { /* some logic that accesses BarProperty */ }
}

public class PacketHandlerManager
{
    public PacketHandlerManager(Library library, IEnumerable<IPacketHandler> packetHandlers)
    {
        foreach (var packetHandler in packetHandlers)
        {
            library.Bind(packetHandler.HandlesPacketType, packetHandler.HandlePacket);
        }
    }
}

There's some more logic in PacketHandlerManager which I've omitted here. library dispatches packets to handlers, so I don't have to deal with that explicitly after I register handlers using the Bind method.

It's not exactly what I imagined, but it'll do.