flow.
"Flow is a condition of deep, nearly meditative involvement." - Tom DeMarco

Providing Multiple Views to the Same Collection

Tuesday, February 19, 2008 1:58 PM

How many times have we seen the infamous piece of code below...

            foreach (IAnimal animal in zoo.Animals)

            {

                Elephant elephant = animal as Elephant;

                if (elephant != null)

                    elephant.SwingTrunk();

 

            }

Not exactly terse is it?  We've seen a few different ideas of how to deal with this common problem, some of which I've liked better than others.  But here's a much simpler one that occurred to me a few nights ago...what if we just surfaced different iterators from the same collection?  The beauty of the iterator pattern is that is shields the client from how the underlying collection is implemented.  Normally this is to isolate clients from changes to how the collection is implemented, but there's nothing to say that we can't use the pattern to isolate the client from the source of the collection.


 Let's assume that we have a collection Animals which contains objects of type IAnimal.  Now let's assume that this collection contains instances of both Elephants and Zebras, since they both implement IAnimal.  Let's take a look at how this property would likely be implemented...

        public IEnumerable Animals

        {

            get { return _animals; }

        }

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.


kick it on DotNetKicks.com

Feedback

# re: Providing Multiple Views to the Same Collection

Maybe i'm missing something but have you considered the visitor pattern? You can pass in your visitor and iterate over the collection visiting each item. New types are catered for by new visitors. 2/19/2008 3:22 PM | BenL

# re: Providing Multiple Views to the Same Collection

What about this?

public IEnumerable FindBySpecieSameAs(IAnimal animal)
{
foreach (IAnimal _animal in _animals)
{
if (_animal.GetType() == animal.GetType())
yield return _animal;
}
}

It even comes with a test! ;)

[Test]
public void can_get_animals_by_specie()
{
Zoo zoo = new Zoo();
zoo.AddAnimal(new Zebra());
zoo.AddAnimal(new Elephant());
zoo.AddAnimal(new Elephant());
Assert.AreEqual(2, Zoo.EnumeratorCount(zoo.FindBySpecieSameAs(new Elephant())));
}
2/19/2008 3:52 PM | alberto

# re: Providing Multiple Views to the Same Collection

I just realized typeof doesn't play well with inheritance, this one does:

public IEnumerable FindBySpecieSameAs<T>() where T : IAnimal
{
foreach (IAnimal _animal in _animals)
{
if (_animal is T)
yield return _animal;
}
}

[Test]
public void can_get_animals_by_subspecie()
{
Zoo zoo = new Zoo();
zoo.AddAnimal(new Zebra());
zoo.AddAnimal(new Elephant());
zoo.AddAnimal(new AfricanElephant());
Assert.AreEqual(2, Zoo.EnumeratorCount(zoo.FindBySpecieSameAs<Elephant>()));
}
2/19/2008 6:21 PM | alberto

 re: Providing Multiple Views to the Same Collection

Most of the solutions proposed above are already available in the .net framework. As part of the Linq framework, a lot of extension methods are added to the IEnumerable interface. So you can do things like:

foreach(Elephant elephant in zoo.Animals.OfType<Elephant>())

You can even filter on other things:

foreach(IAnimal oldAnimal in zoo.Animals.Where(animal => animal.Age > 10)

There are a lot of cool things in C# 3.0! 2/25/2008 1:47 AM | Ebbing

# re: Providing Multiple Views to the Same Collection

That's an awesome idea! I hadn't thought about that.

I'm starting to warm up more and more to extension methods...

jeremy 2/27/2008 9:04 PM | Jeremy

Post a comment





 

Please add 6 and 7 and type the answer here: