Tuesday, January 25, 2011

Producer, Consumer, Downcaster

Producer/Consumer relationships occur all over the places in code. In most contexts, separating the responsibility of producing something from the various ways that something can be consumed is an invaluable way to simplify a design. However, as we'll see, consumer code can encounter some unfortunate pitfalls when it tries to react to what gets produced.

Commonly the things getting produced inherit from the same interface. Often the class hierarchy looks something like this:

class IProducedThing
{
public:
    virtual void Foo() = 0;
  ...
};

class CConcreteProducedThing1 : public IProducedThing
{
public:
    virtual void Foo();
   ...
};

class CConcreteProducedThing2 : public IProducedThing
{
public:
    virtual void Foo();
   ...
};

IProducedThing* produce()
{
     // In some cases build a CConcreteThing1, in other cases a CConcreteThing2
     if (...)
    {
         return new CConcreteProducedThing1();
    }
    else
    {
         return new CConcreteProducedThing2();
     }
}

Unfortunately this can have the side effect of forcing the consumer to figure out exactly what was just produced. This probably involves a dynamic cast at the consumer to react appropriately to the consumed event:

void consumer()
{
      IProducedThing* thingie = produce();
      thingie->Foo();
      // Is this a CConcreteProducedThing1 or CConcreteProducedThing2?
      if (dynamic_cast<CConcreteProducedThing1>(thingie) != NULL)
      {
           // do something for a produced thing 1
      }
      else if (dynamic_cast<CConcreteProducedThing2>(thingie) != NULL)
      {
           // do something for a produced thing 2
      }
}

The most egregious reason to have this dynamic_cast would be if the "do something" operation is something best implemented as a method in each of the produced thingies. Something that should be pure virtual in the interface. 

Nevertheless, the consumer will need to do things that are best left separate from the produced class hierarchy. For example, the producer may be producing events while the consumer logs those events into a very specific format. Or the consumer itself may need to change its internal state based on what was produced. None of these things should or could be implemented as methods in our producer's class hierarchy.

Still even in these cases, dynamic_cast, casting in general, should give us the heebie-jeebies. We are circumventing what is otherwise a strongly typed language. By casting, we're forcing a conversion the compiler otherwise would see as an error. We tell the compiler to trust us. In strongly-typed languages, we want a persnickety compiler to complain to us as much as possible. We even hope that if we write our code thoughtfully, the compiler's errors can translate directly to prevented bugs. In short we want a compiler that doesn't trust us.

If ignoring compiler errors might mean ignoring bugs, there's got to be a better way. And there is. The producer just needs to be nice enough to implement the Visitor Pattern and we're all set. Here's how it works.

First we need to define a visitor base class:

class IProducedThingVisitor
{
  
};

In our visitor, we need to explicitly list every concrete implementation of IProducedThing, giving each its own virtual method called "visit":

class IProducedThingVisitor
{
public:
        void visit(CConcreteProducedThing1& thing1);
        void visit(CConcreteProducedThing2& thing2);
};

Then we'll need to require that each class that inherits from IProducedThing will accept a visitor

class IProducedThing
{
public:
       virtual void accept(IProducedThingVisitor& visitor) = 0;
  ...
};

class CConcreteProducedThing1 : public IProducedThing
{
       virtual void accept(IProducedThingVisitor& visitor)
       {
               visitor.visit(*this);
       }
   ...
};

class CConcreteProducedThing2 : public IProducedThing
{
       virtual void accept(IProducedThingVisitor& visitor)
       {
               visitor.visit(*this);
       }
   ...
};

What happens here? A class in consumer code inherits from IProducedThingVisitor. For each concrete IProducedThing, the consumers visitor creates a visit method to perform a specific task for that specific concrete type. For example, the consumers visitor may log each produced thing in a different log format. The consumer makes use of the visitor interface by creating its concrete visitor and passes an instance of this visitor into the produced item's accept as follows:

void consumer()
{
      IProducedThing* producedThingie = produce();
      CMyConcreteVisitor* visitor = new CMyConcreteVisitor();
      producedThingie->accept(visitor);
}

On the last line, the consumer ends up invoking either CConcreteProducedThing1::accept or CConcreteProducedThing2::accept. Once in those methods, the type of the this pointer corresponds to the type of the class we are in. This's type is CConcreteProducedThing1 in CConcreteProducedThing1::accept() and CConcreteProducedThing2 in CConcreteProducedThing2::accept() respectively. With this information he compiler can use the type of the this pointer to call the visitor's correct visit method. In this way we can have one set of operations for CConcreteProducedThing1 and another for CConcreteProducedThing2. Viola! no dynamic_cast required.

Problem solved? This solves the problem as presented, but there's still some open questions and issues that you'd have to consider before using the visitor pattern. 
  • What if the producer is in a separate, 3rd-party library and they didn't implement the visitor pattern?
  • What if you want to implement your own concrete implementations of the IProducedThing interface but the visitor base class is in 3rd party code out of your control, can you get producer code to call the visit method for your class?
  • What if you want data *returned* from the visit operations? What if some operations compute data while others don't?
Still, I'll think you'll agree this is an invaluable addition to any developers toolkit.