Home Developer Blogs Tony's Blog Keeping Interfaces Clean - Iterators
Keeping Interfaces Clean - Iterators PDF Print E-mail
Developer Blogs
Written by Tony Richards   
Wednesday, 11 February 2009 21:21

In Object Oriented Design, one of the goals is to hide our implementation, so that if you make a modification to the implementation, nothing else needs to be recompiled.

One of the problem areas with this philosophy is how to expose collections.  You cannot directly expose an iterator as that causes several problems.

What happens if the container goes out of scope before the iterator?  What happens if for some reason you need to change the underlying container type, such as from a std::list to a std::vector.  Or worse, what happens if your collection gets to large and you need to re-write it so as to have the collection be records in a database.

Using generic programming, one could choose to write some templates that will help solve the problem, and I think that's a fine approach if you want to use generics.

I use generics quite a bit in my implementation because they reduce the amount of coding required to do certain types of things, especially while using STL or Boost.

But that is my implementation. If I can keep my design consistently Object Oriented and avoid generics in my public interfaces then I will, even if it means a little extra effort. 

This follows the MIT approach of Worse is better, where I'm choosing consistency over simplicity while still maintaining completeness and correctness.

Contrast this approach to the one in Thomas Becker's "On the Tension Between Object-Oriented and Generic Programming in C++" article where he solves the problem using generics.

A pure Object Oriented approach would use a callback of some sort, generally using the visitor design pattern.

Using the example given by Thomas:

class number_cruncher
{
public:
typedef std::vector::const_iterator const_iterator;
const_iterator begin() const;
const_iterator end() const;
[...]
};

would be changed using my coding conventions and object oriented design to this:

class NumberCruncher
{
public:
struct I_NumberVisitor;
void getNumbers(I_NumberVisitor& _visitor) const;
  struct I_NumberVisitor
{
virtual void begin() = 0;
virtual void visit(const Number_type& _number) = 0;
virtual void end() = 0;
};
};

Now, when you want to get (or otherwise operate on) all of the numbers in the number cruncher, your client code implements NumberCruncher::I_NumberVisitor and calls getNumbers() passing an instance of your visitor's implementation. This calling convention is slightly different from the visit() / accept() method normally found in the visitor design pattern, but I think my example with getNumbers() / visit() is clearer.

For example:

void printNumbers(NumberCruncher& _numberCruncher)
{
struct NumberPrinter
: public NumberCruncher::I_NumberVisitor
{
virtual void begin()
{
std::cout << "Dumping numbers: " << std::endl;
}
virtual void end()
{
std::cout << "Done." << std::endl;
}
virtual void visit(const Number_type& _number)
{
std::cout << _number << " ";
}
} visitor;

_numberCruncher.getNumbers(visitor);
}

Who is right?  We both are correct (but make sure you read and understand the whole of his article).

The key is to remain consistent.

If you are designing applications using Object Oriented Design, stick with it.  If you are using Generic Programming, stick with that.

The only part of his article with which I disagree is where he called Object Oriented "old", whereas I prefer calling it "mature" and we should simply chalk that disagreement up as opinion and not fact... for both of us.  Tongue out

Respond to this article...

Last Updated ( Wednesday, 11 February 2009 22:51 )