Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Invariant inheritance problem

I'm trying to implement a strategy pattern to allow me to allow me to apply some "benefit" to an "account". In the code below, I can't add my implementation of an interface to a dictionary expecting the interface. I think it's some kind of contravariance problem, but it feels like I should be able to do this:

EDIT:
Since the answer seems to be that it's just not possible, any suggestions on how to achieve what I'm going for here?

void Main()
{
    var provider = new BenefitStrategyProvider();

    var freeBenefit = new FreeBenefit();

    var strategy = provider.GetStrategy(freeBenefit);

    strategy.ApplyBenefit(freeBenefit, new Account());

}

public class BenefitStrategyProvider
{
    private Dictionary<Type, IBenefitStrategy<BenefitBase>> _strategies = new Dictionary<Type, IBenefitStrategy<BenefitBase>>();

    public BenefitStrategyProvider()
    {
        /* Why can't I add this? */
        _strategies.Add(typeof(FreeBenefit), new FreeBenefitStrategy());
    }

    public IBenefitStrategy<BenefitBase> GetStrategy(BenefitBase benefit)
    {
        return _strategies[benefit.GetType()];
    }

}

public class Account {}

public abstract class BenefitBase
{
    public string BenefitName {get;set;}    
}

public class FreeBenefit : BenefitBase {}

public interface IBenefitStrategy<T> where T: BenefitBase
{
    void ApplyBenefit(T benefit, Account account);
}

public class FreeBenefitStrategy : IBenefitStrategy<FreeBenefit>
{
    public void ApplyBenefit(FreeBenefit benefit, Account account)
    {
        Console.WriteLine("Free Benefit applied");
    }
}
like image 765
scottm Avatar asked May 07 '11 01:05

scottm


3 Answers

EDITED - The formatting engine had removed everything in <angled brackets>, which made it rather impossible to understand. Sorry if this was confusing!

FreeBenefitStrategy implements IBenefitStrategy<FreeBenefit>. It can apply only FreeBenefits, not any other kind of benefit. It is not an IBenefitStrategy<BenefitBase>, so you can't put it in a collection of those. Logically, IBenefiteStrategy could be contravariant in BenefitBase, but this doesn't help you here - an IBenefitStrategy<BenefitBase> claims to be able to apply all kinds of benefits, so an IBenefitStrategy<BenefitBase> is-an IBenefitStrategy<FreeBenefit>, but the converse is not true - an IBenefitStrategy<FreeBenefit> cannot apply any BenefitBase.

I don't think there's any way to have a heterogenous collection like you want without using type-casting. If you think about it, there's no method that you can invoke on both an IBenefitStrategy<FreeBenefit> and an IBenefitStrategy<ExpensiveBenefit> beyond those that they share from object, so it makes sense that a variable of type object is the only thing that can point to either. If you want to keep them in the same dictionary, you'll need to make it a Dictionary<Type, object>. You could change GetStrategy to be generic and apply appropriate type-casting, but do be careful when looking up your dictionary - think what will happen if the object passed in is of a sub-class of FreeBenefit.

like image 193
Weeble Avatar answered Oct 08 '22 00:10

Weeble


See: Covariance and Contravariance FAQ

How can I create variant generic interfaces and delegates myself?

The out keyword marks a type parameter as covariant, and the in keyword marks it as contravariant. The two most important rules to remember:
    You can mark a generic type parameter as covariant if it is used only as a method return type and is not used as a type of formal method parameters.
    And vice versa, you can mark a type as contravariant if it is used only as a type of formal method parameters and not used as a method return type.
like image 22
helloworld922 Avatar answered Oct 08 '22 00:10

helloworld922


You need to add out T to your interface:

public interface IBenefitStrategy<in T>
like image 1
Tejs Avatar answered Oct 08 '22 00:10

Tejs