June 11, 2009

Hail to the Visitor Pattern

In my opinion the Visitor Pattern is one of the often underestimated patterns from the GOF book. For instance in Head First Design Patterns it just gets it share in the orphaned further patterns section. But applied correctly, the Visitor is a powerful tool helping you to dispatch complex type systems without violating the Open Closed Principle. Interested? Then continue reading!

Let's fetch up those of you, who are not familiar with the Visitor at all. This pattern enables injecting methods into types. 'Why the hell, do I want to do this?' you might ask. Well, consider you have some micky mouse type hierarchy of geometry related classes:

And you want to add functionality to these classes, like drawing. You could add a purely virtual method draw() to the super class and provide an implementation in all of the sub classes, since all of our subclasses are drawn differently, of course. Yes, indeed you could do this. But what if such a method would introduce dependencies you damn want to avoid with your plain classes? To stay in our example, let's assume we want draw our Geometries with QT. Hence we would implement QT depending code in draw(). This would make our classes dependent of QT. But what if we don't want this? Because we also want to allow drawing it in AWT? what now? Add drawMeWithAWT() as well? And then add new dependencies? I don't know about you, but surely I don't want this! Therefore, what else can we do.

Ah, I've got an idea. There must have been a reason, why god (or at least James Gosling) has invented instanceof in Java:

public void drawGeometry(Geometry g){
    
    if(g instanceof Polygon){
        drawPolygon((Polygon) g);
    }else if(g instanceof Circle){
        drawCircle((Circle) g);
    }else if(g instanceof Line){
        drawLine((Line) g);
    }else {
        throw new RuntimeException("Unknown type");
    }
}
Ok, let's look at the code closely. Does your brain hurt and do your eyes bleed already? No? Look again! They should! Please say loud to yourself: 'This is bad code!'. Why? Because it's the prototype example of violating the Open Closed Principle: You have some generalization (here Geometry) and make assumptions about existing sub types. The overall idea of generalization is well, generalize the fact that there are sub types. But why is it so awful evil? Because it is not maintainable. What happens if you start adding new sub types to Geometry? You would have to extend every single dispatching block and add a new else if for your new type. And code which relies on this idiom does not stop with a single block of such if else nonsense. No. It will spread through the whole code. And I would bet a serious amount of money (if I would have a serious amount of money) that you miss at least one place where you have to add your new sub type.

The rescue to this problem is polymorphism. But we've already been at this point. So we don't want polymorphism in the type hierarchy itself like our draw() method. But what now? Well, please step aside for the Visitor pattern. It lets you write specialized handling for each type, but prevents you from implementing the type dispatching on your own. How? Have a look:

The trick is SubElement's implementation of accept:

 
public void accept(Visitor visitor){ 
    visitor.visit(this); 
} 
This is the magic polymorphic dispatching code! Let's see it in action:
Element elem = new SubElement();
elem.accept(new ConcreteVisitor());
Ok, not much action. But all we need for the explanation. We treat our SubElement as a general Element. Calling the polymorphic accept actually on SubElement will take care that the visit(SubElement) is finally executed. Hence we did our dispatching, but without any if or even else. That's the power of polymorphism!

Now let's go back to our Geometry world and see a more complex example:

Here we have our DrawVisitor, who is responsible for drawing. The class hierarchy just depends on Visitor and therefore knows nothing about our concrete DrawVisitor. This goal has been reached. But, wait, we get even more, for free. People usually advice you to use the Visitor on a stable class hierarchy, hence on such, which does not frequently add new classes. Of course, this is correct. Let's assume our classes would belong to a framework and therefore we would not know anything about the client. if we constantly add new classes to the model and change the interface people could become quite angry with us.

But (and that's a very huge but in my opinion) if you change your model, add new classes and add the respective visit(NewClass) methods to the Visitor then you get a compile time check whether you have taken care of handling the new classes in all of your visitor's implementations. And having problems reported by the compiler is one of the major advantages of a static type system, in my honest opinion.

You might have noticed that Visitor visits all concrete classes, but not the abstract super class. This is the necessary otherwise the whole dispatching mechanism does not work. But on the other hand this introduces some challenges on more complex type systems, where we have several levels of abstraction. Even here we can use the visitor and build some handy tools with it to savely navigate such hierarchies, but I think this might be an issue for another post.

Summarizing what we've learned today: The Visitor pattern enables us to dispatch type hierarchies without violating the Open Closed principle by injecting methods in classes and relying on good old runtime polymorphism.

Or do you have a complete different opinion on this topic?

3 comments:

alstar said...

Great article! I like the style of those figures.

Unknown said...

schade, Du bloggst nicht mehr über Design Patterns. Aber kannst Du mir verraten, womit diese netten UML Diagramme gemacht wurden? Der Comicstil sieht ganz nett aus.

Stuart said...

The visitor pattern is certainly a lot better than an instanceof chain, but it does bother me that it's cyclic (e.g. Visitor depends on Circle depends on Visitor). From a dependencies point-of-view, it's quite messy.