Delegating general class to specific classes

I have the following interfaces:

public interface IModel
{
    ModelTypes ModelType { get; } // ModelTypes is an enum
}

public interface IModelConverter<T>
{
    byte[] ToBytes(T model);
}

Also, I have 3 implementations of IModel: ModelA,ModelB,ModelC, and the following classes:

public class ModelAConverter : IModelConverter<ModelA>

public class ModelBConverter : IModelConverter<ModelB>

public class ModelCConverter : IModelConverter<ModelC>

I want to take an IModel and convert it with ToBytes method. Obviously, I don't want the caller to know each and every implementation of the converters, so I created the DelegatingConverter class:

public class DelegatingConverter : IModelConverter<IModel>
{
    private readonly Dictionary<ModelTypes, IModelConverter<IModel>> _modelConverters;

    public DelegatingConverter()
    {
        _modelConverters = new Dictionary<ModelTypes, IModelConverter<IModel>>
        {
            {ModelTypes.TypeA, new ModelAConverter()}, // Argument type ModelAConverter is not assignable to parameter type IModelConverter<IModel>
            {ModelTypes.TypeB, new ModelBConverter()}, // Argument type ModelBConverter is not assignable to parameter type IModelConverter<IModel>
            {ModelTypes.TypeC, new ModelCConverter()}  // Argument type ModelCConverter is not assignable to parameter type IModelConverter<IModel>
        };
    }

    public byte[] ToBytes(IModel model)
    {
        // Here is the delegation..
        return _modelConverters[model.ModelType].ToBytes(model);
    }
}

Everything goes great, until I add some converters to the delegating dictionary, _modelConverters.

The error is:

Argument type ModelXConverter is not assignable to parameter type IModelConverter<IModel>

And I know the solution here should be using covariance on T in IModelConverter, so the decleration should be:

public interface IModelConverter<out T>

When I add it, the following error occurs:

Parameter must be input-safe. Invalid variance: The type parameter 'T' must be contravariantly valid on IModelConverter.ToBytes(T). 'T' is covariant.

Is there any way to make it better? I know I can make every ModelConverter implementation implement IModelConverter<IModel>, but then I need an obvious cast in the beginning of each implementation.

Jon Skeet
people
quotationmark

And I know the solution here should be using covariance on T in IModelConverter

No, not really. That would only be the case if any specific IModelConverter could be regarded as a more general IModelConverter - and that's not the case. If a converter only knows how to convert ModelB to bytes, what would you expect it to do with a ModelC?

The simplest approach is probably to write something more like this:

// This class is general...
public sealed class DelegatingConverter<T> : IModelConverter<IModel>
    where T : IModel
{
    private readonly IModelConverter<T> originalConverter;

    public DelegatingConverter(IModelConverter<T> originalConverter)
    {
        this.originalConverter = originalConverter;
    }

    public byte[] ToBytes(IModel model)
    {
        return originalConverter.ToBytes((T) model);
    }
}

Then:

public sealed class KnownModelConverter : IModelConverter<IModel>
{
    private static readonly Dictionary<ModelTypes, IModelConverter<IModel>>
        = new Dictionary<ModelTypes, IModelConverter<IModel>>
    {
        { ModelTypes.TypeA, new DelegatingConverter<ModelA>(new ModelAConverter()) },
        { ModelTypes.TypeB, new DelegatingConverter<ModelB>(new ModelBConverter()) },
        { ModelTypes.TypeC, new DelegatingConverter<ModelC>(new ModelCConverter()) },
    };

    public byte[] ToBytes(IModel model)
    {
        // Here is the delegation..
        return _modelConverters[model.ModelType].ToBytes(model);
    }
}

Basically the key is the cast within DelegatingConverter<T> - you need to make sure you only use pass instances of the right type to its ToBytes method - but assuming model.ModelType is correct, that should be fine. (And if it's not, an exception is probably the right behaviour.)

people

See more on this question at Stackoverflow