How many times have we seen the infamous piece of code below...
Pretty straight forward, eh? But how would we retrieve only the instances of Elephant from the group? Normally we would have to use code similar to above, however, by leaning on our old friend the yield keyword we can create a much more elegant solution. What if we depended on the Elephants property to hand pick the elephants for us? What would this code look like?
public IEnumerable Elephants
{
get
{
foreach (IAnimal animal in _animals)
{
Elephant elephant = animal as Elephant;
if (elephant != null)
yield return elephant;
}
}
}
Note that from a logic perspective this is very similar to how our first example code worked. The only different is that we are relying on the iterator to return the elements we require instead of pushing this logic off to the client. With this new code in place, our client code simplifies greatly to this...
foreach (Elephant elephant in zoo.Elephants)
elephant.SwingTrunk();
Much cleaner, huh? It is, but I can spot one glaring issue with this already...we're in dire violation of the Open/Closed Principle. This Zoo works great as long as we only ever want to house Elephants and Zebras. But what happens when someone wants to see a Monkey? Don't you think you'll want to see a Monkey someday? What about a Rhino? What about a Giraffe? We can always add these animals into the Zoo but if we want to retrieve them in the same clean manner as Elephants and Zebras then we'll have to go back into the Zoo class and add this functionality back in. There's no easy way to add it without modifying the existing class, which would potentially break legacy code (OK, so adding features to a class usually doesn't break legacy code...but you get the point). What are some ways around this? Well, I couldn't find a clean way to do it with generics since you would have to introduce the generic type variable when you instantiated the class. You could always derive your own Zoo from this one (MonkeyRhinoGiraffeZoo?) and add that functionality in, but that's not really ideal either.
So, is all hope lost? Not at all, as with any feature, you'll just have to use your best judgement when to apply it. I recently used this technique with different implementations of an interface called ITestResult. As you can guess, there will only ever be two types of ITestResults...PassingTestResult and FailingTestResult. In this case, I'm probably safe in assuming that I can get away with this. In the Zoo scenario, and others where you could potentially add later implementations of the base type of your collection, you may want to approach this technique with caution.
You can download the code and associated tests here.