Best practice for enforcing type safety in polymorphic inheritance hierarchies [closed]
Asked Answered
E

2

3

I seem to run into this situation quite a lot and have yet to find a solution that I find acceptable.

Quite often I will have parallel inheritance hierarchies where a method in one hierarchy gets passed the matching class from the other hierarchy as a parameter.

Here is an example that probably explains this better.

abstract class Animal
{
    public virtual void Eat(Food f)
    {
    }
}

abstract class Food
{
}

class LionFood : Food
{
}

class ElephantFood : Food
{
}

class Lion : Animal
{
    public override void Eat(Food f)
    {
        // It is only ever valid to pass LionFood here as the parameter.
        // passing any other type of Food is invalid and should be prevented
        // or at least throw an exception if it does happen.
    }
}

In the past, I have usually made the base class generic to allow the implementing concrete class to define the type as follows..

abstract class Animal<T> where T : Food
{
    public abstract void Eat(T f);
}

class Lion : Animal<LionFood>
{
    public override void Eat(LionFood f)
    {
    }
}

At first this seems like a very good solution because it provides compile-time type safety. But the more I use it, the more I am starting to think that using generics in this way is infact an anti-pattern. The problem is that the Animal base class cannot be used in a polymorphic way. You cannot, for example, easily write a method that will process any type of Animal regardless of its actual concrete type.

Every time I use this generics solution, I always seem to end up with covariant and contravariant interfaces all over the place just to try and provide the polymorphic behaviour I want. This gets out of hand pretty quickly and some functionality is not possible simply because the correct interface cannot be provided.

Of course another option is to not use generics and perform runtime type checking in the Eat method like this:

    public override void Eat(Food f)
    {
        if (f.GetType() != typeof(LionFood))
        {
            throw new Exception();
        }
    }

This is better than nothing I suppose but I'm not a huge fan of it simply because of the lack of compile-time type safety.

So after all that.. My question is.. What is the best practice to provide polymorphic behaviour while at the same time ensuring some type safety?

Is there some OO design trick or pattern that I am missing that will allow me to avoid the parallel inheritance hierarchies all together?

I appreciate that this question is somewhat subjective, but there are points available for everyone who contributes and I'll choose the best response as the answer.

Thanks for looking.

Edit:

Having thought about this I realise that my example given doesn't really make sense. Of course it is not possible to use Animal in a polymorphic way because the type passed to Eat will always depend on the actual underlying type of Animal (which the initiator of a polymorphic call will not know)! I need to think of a better example that illustrates my actual situation.

Entourage answered 21/3, 2012 at 14:37 Comment(7)
Can you give an example of code for which you see problems with your design?Salesroom
Why not just make generic methods instead of generic class ?Stymie
@VictorSorokin I have made this as general as I can, so I am not sure what further code examples I can provide. The problems with my design are that the generics solution doesn't allow polymorphic behaviour (without getting pretty messy) and the type checking doesn't provide any compile-time safety.Entourage
I don't understand what exactly you want. You want Animals to eat something common, but yet, you don't want them to be able to eat anything in common?Pod
@Pod Yes, I think you're hinting at something that I'm doing wrong. I just need to figure out what is the correct way. Lions will only ever Eat LionFood and Elephants will only ever Eat ElephantFood. But in Food there is some common Eat functionality that is common to all Animals.Entourage
So, what do you really want? - That every animal eats something? - That everything that gets eaten has some common functionality? Something else?Pod
I think Martyn refers to the common method Eat, not necessarily to a common food implementation. I believe somewhere it's a method which gets an Animal and a Food as arguments so you do animal.Eat(food) and you let the polymorphism do its job.Thirza
T
3

I think common sense and the requirements of the domain will dictate the proper approach. Working with this example, I'd do like this

 public class Lion:Animal
 {
       public override void Eat(Food f)
        {
           Eat(f as LionFood);
         }
       public void Eat(LionFood food)
        {
         //check for null food  
         //actually consume it
       }
  }

Edit

I think using generics is not suited in this case, because what if an Animal can play with a Toy, hunt a specific Animal and so on. You can have a number of methods with arugments that implement an abstraction, it's awkward to use generics every time there is a method with an argument that uses polymorhpism.

Thirza answered 21/3, 2012 at 14:59 Comment(3)
What language is this? And will this cause a compile time error when you try to feed grass to a lion?Pod
that's C#, probably works with java too (not sure about the syntax). No compile error, unless Grass doesn't inherit Food. But that's why you're using polymorphism, to program against an abstraction (Food can be an interface).Thirza
@Thirza Exactly Mike, Your edit explains why the generics route can get very messy. It seems like a good option at first but doesn't usually turn out very well. I'm thinking it is often best to sacrifice the compile time checking and instead put the type checks in the code as you have in your answer example.Entourage
P
1

Ok, could that be what you want? Sometimes, when you can't articulate what you want to do, what you want is actually a mixin.

template<typename base>
class Eater: public base {

template<typename T>
Eat(T (extends Food) food) { // you can do that extends thing in C++ but I won't bother
    // register what's eaten, or do whatever
    base::Eat(food);
}

}

class Lion {
    Eat(LionFood food) {
        cout<<"Hmm delicious!";
    }
}

int main() {
    Eater<Lion> lion;
    lion.eat(LionFood());
    return 0;
}

This'll give you a nice compiler error if you try to feed grass to the lion.

Pod answered 21/3, 2012 at 15:16 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.