Visitor Pattern Part 2 – the enum based visitor

In my last post I showed the Visitor design pattern in its fully object oriented implementation. In the post before that I wrote about moving from enums to class hierarchies. I explicitly mentioned the visitor pattern there to be overkill if the visited objects do not differ too much from another. A simpler alternative can be an enum based visitor.

Looking back at the examples in the two posts, we see that we have a bunch of data structures that have been made classes only because of the visitor pattern. Luckily C++ is not an object oriented language, so we don’t have to squeeze everything in a class hierarchy. In other words: the pattern can be simplified.

The enum based visitor pattern

In our example, we can reverse the enum-to-class-hiearchy refactoring. We can make the code of the Expression classes and their ExpressionVisitor simpler by moving from the set of classes back to enums.

I will refactor last week’s example step by step. Until the end of the week I will upload the code and the refactoring history to the GitHub repository for this blog.

As a first step add an enum to the Expression base class. While we are at it, let’s make some trivial simplifications by converting every Expression class to structs with public access. Also remove the getters.

struct Expression {
  enum ExpressionType {
    ADD,
    MULTIPLY,
    NUMBER
  };
  
  ExpressionType expressionType;
  
  Expression(ExpressionType eType) : expressionType(eType) {}
  //...
};

struct BinaryExpression : Expression {
  ExpressionPtr lhs;
  ExpressionPtr rhs;

  BinaryExpression(ExpressionPtr left, ExpressionPtr right, ExpressionType eType) 
    : Expression(eType), lhs(move(left)), rhs(move(right))
  { assert(lhs && rhs); }
};

struct AddExpression : BinaryExpression {
  using BinaryExpression::BinaryExpression;
  void accept(ExpressionVisitor& visitor) override { /* ... */ }
};

struct NumberExpression : Expression {
  double number;
  NumberExpression(double d) : Expression(NUMBER), number(d) {}
  void accept(ExpressionVisitor& visitor) override { /* ... */ }
};

You may ask why we have not altered the AddExpression and MultiplyExpression to pass the correct enums to the base class. You will see soon, first let’s get rid of the accept methods.

To do so, we have to supply the visitor with another means to know which visit-method to call. That means is already in place, it is the new enum.

Instead of the Expression classes accepting a visitor, the visitor now has to visit the expression base class actively and do the dispatch that had been done by the virtual dispatch on the accept method.

class ExpressionVisitor {
public:
  void visit(Expression& expr) {
    switch (expr.expressionType) {
      case Expression::ADD:
        visitAdd(static_cast<AddExpression&>(expr));
        break;
      case Expression::MULTIPLY:
        visitMultiply(static_cast<MultiplyExpression&>(expr));
        break;
      case Expression::NUMBER:
        visitNumber(static_cast<NumberExpression&>(expr));
        break;
    }
  }
  //...
};

If we look closely now, we see that suddenly the expression class hierarchy does no longer depend on the ExpressionVisitor, because the accept method depending on that class has gone. Decoupling dependencies is a good thing!

Another look shows, that MultiplyExpression and AddExpression are now identical. There is no point in keeping them, since they are also distinguishable by the enum.

class ExpressionVisitor {
public:
  void visit(Expression& expr) {
    switch (expr.expressionType) {
      case Expression::ADD:
        visitAdd(static_cast<BinaryExpression&>(expr));
        break;
      case Expression::MULTIPLY:
        visitMultiply(static_cast<BinaryExpression&>(expr));
        break;
      case Expression::NUMBER:
        visitNumber(static_cast<NumberExpression&>(expr));
        break;
    }
  }
  virtual void visitAdd(BinaryExpression&) = 0;
  virtual void visitMultiply(BinaryExpression&) = 0;
  virtual void visitNumber(NumberExpression&) = 0;
  //...
};

You can find the complete code on my GitHub repository, including the refactoring history for the visitor from OOP to enum based.

Tradeoffs

The bad side

By moving from the class hierarchy to enums we introduced a few things that may look like flaws or code smells. The first one is the need to static_cast down to the concrete Expression subtype.

Strictly speaking, we have code duplication in the way that we have to express the mapping from a specific enumerator to its class in two places: calling the Expression base constructor with the right enumerator and casting back to the concrete type depending on the enumerator in the visitor.

This duplication may be overcome by some kind of template meta programming, but this would make the code more complicated again, so I’ll leave it as it is for now.

The other smelly part is that the enumerators are leaked outside the Expression class hierarchy. You have to know and use them e.g. to create what once was an AddExpression or an MultiplyExpression. This could be fixed by getting those classes back and thereby encapsulating the BinaryExpression constructor call.

The good side

The big plus is that we broke the dependency on the ExpressionVisitor base class. In fact, we do not even have to derive all visitors from ExpressionVisitor any more. We could for example add another basic visitor class that is not interested in the differences between the binary operators:

class ADifferentExpressionVisitor {
public:
  void visit(Expression& expr) {
    switch (expr.expressionType) {
      case Expression::ADD:
      case Expression::MULTIPLY:
        visitBinaryy(static_cast<BinaryExpression&>(expr));
        break;
      case Expression::NUMBER:
        visitNumber(static_cast<NumberExpression&>(expr));
        break;
    }
  }
  virtual void visitBinary(BinaryExpression&) = 0;
  virtual void visitNumber(NumberExpression&) = 0;
  //...
};

Conclusion

As with many patterns, there are different implementations of the visitor pattern. In this case we sacrificed a bit of code beauty on the visitor side to get a simpler implementation on the side of the visited data structure. As a byproduct we decoupled the data from the visitor class and also got more freedom for visitor implementations.

Previous Post
Next Post

Leave a Reply

Your email address will not be published. Required fields are marked *