Hello, dear habravchane!
I want to share with you my experience of using the visitor design pattern and its interesting modification, which I called the upcast visitor. Unfortunately, it is not easy to come up with a simple short example and describe how everything works, this article may also seem difficult for beginners, however I will try to simplify the task as much as possible. Code examples are given in C ++ and are required to be read. Without understanding the code, it will be difficult to grasp the essence of the article.
Prehistory
Imagine that we are designing a 2D game in which fruit falls from a tree, hitting the branches along the way. The goal of the game is to catch all the fruits by moving the basket under the tree.
Build the following class diagram:

Following the principles of OOP, we declare a virtual drawing function in the base class and override it in all derivatives (except for the class Fruit).

For example, this is the code responsible for drawing an apple:
void Apple::draw() { Graphics& graphics = GetGraphicsFromSomeMagicalPlace(); Texture& texture = GetAppleTextureFromEvenMoreMagicalPlace(); graphics.draw(texture, x(), y(), angle()); }
Our main drawing method, which is called in the game loop, looks like this:
void Game::draw(std::vector<Object*>& allObjects) { std::vector<Object*>::iterator it = allObjects.begin(); std::vector<Object*>::iterator end = allObjects.end(); while(it != end) { (*it)->draw(); ++it; } }
Woo! Polymorphism is cool, we think and continue to write the game. We want to portray the fall realistically, so we decide to use the
Box2D library. Some classes from the diagram need to have a pointer to an object from Box2D, but not all. For example, the Tree class does not need such functionality; its task is only to draw itself and store information about the amount of fruit. Therefore, we will not put this pointer into the base Object class, but create an intermediate class Box2DObject, after which our diagram will look like this (only the upper part of the diagram is shown):

The code responsible for creating b2Body objects will be in the constructor of each derived class, and the code responsible for deleting will be in the destructor. Take for example the Branch class:
Branch::Branch() { b2BodyDef bodyDef; bodyDef.type = b2_staticBody; b2Body* body = GetBox2DWorldFromSomewhrere().CreateBody(&bodyDef); b2PolygonShape shape; shape.SetAsBox(BRANCH_WIDTH, BRANCH_HEIGHT); b2FixtureDef fixtureDef; fixtureDef.shape = &shape; fixtureDef.friction = BRANCH_FRICTION; fixtureDef.restitution = BRANCH_RESTITUTION; body->CreateFixture(&fixtureDef); m_box2dBody = body; body->SetUserData(this); } Branch::~Branch() { GetBox2DWorldFromSomewhrere().DestroyBody(m_box2dBody); }
Cool again, we think. Although the concept of “virtuality” is not applicable for designers, it is still possible to draw an analogy with drawing - each class creates objects from Box2D in its own way.
We notice that the deletion code will be the same for all classes derived from Box2DObject, so it is logical to move it to the Box2DObject destructor. Like this:
Box2DObject::~Box2DObject() { GetBox2DWorldFromSomewhrere().DestroyBody(m_box2dBody); }
Also, the code for creating an orange and an apple is no different, therefore we transfer it to the constructor of the base class Fruit:
Fruit::Fruit() { b2BodyDef bodyDef; bodyDef.type = b2_dynamicBody; b2CircleShape shape; shape.m_radius = FRUIT_RADIUS; b2Body* body = GetBox2DWorldFromSomewhrere().CreateBody(&bodyDef); b2FixtureDef fixtureDef; fixtureDef.shape = &shape; fixtureDef.density = FRUIT_DENSITY; fixtureDef.friction = FRUIT_FRICTION; fixtureDef.restitution = FRUIT_RESTITUTION; body->CreateFixture(&fixtureDef); m_box2DBody = body; body->SetUserData(this); }
Here, now everything seems to work, but there are several problems:
- The code responsible for drawing and working with the Box2D library is randomly scattered across all classes.
- Interfaces for working with graphics and Box2D need to be “dragged” through a chain of constructors of hierarchy and saved in base classes (otherwise how to access them)?
- With a large number of classes in the hierarchy, the code can be compiled much longer, because the header files for working with graphics and Box2D are included in each cpp file.
- When adding a new functionality will have to modify all classes in the hierarchy.
')
Visitor pattern
How to avoid these problems?
Of course, you need to use the visitor design pattern! If you are familiar with it, you probably already guessed that we will have visitor'y for drawing and for Box2D. If you are not familiar - now you will understand everything, this is a very useful pattern, which I love to use.
We need four new classes: Visitor, Painter, Creator and Destroyer:

All methods for working with graphics will go to Painter, and for working with Box2D - to Creator and Destroyer. We also need to add the purely virtual accept method to the base Object class and implement it in all the leaves of the hierarchy.
class Object { public: virtual void accept(Visitor& visitor) = 0;
The hierarchy after this will be:

The implementation of the accept method is the same in absolutely all leaves and consists of one line.
class Tree : public Object { public: virtual void accept(Visitor& visitor) { visitor.visit(*this);
When I first became acquainted with the visitor pattern, this line of code carried my brain and I did not understand anything. A few months later I took the
book of the gang of four and was able to master this pattern. The point is that when the this pointer is dereferenced, the necessary function is selected from the Visitor interface (overloading by the type of parameter works). In other words, when this string is called from the Tree class, we get into void visit (Tree & tree), when called from Basket - we get into void visit (Basket & basket). And in every visitor, we are already doing what we want with the object. For example, take drawing:
void Painter::visit(Apple& apple) { m_graphics.draw(m_appleTexture, apple.x(), apple.y(), apple.angle()); } void Painter::visit(Tree& tree) { m_graphics.draw(m_treeTexture, tree.x(), tree.y()); }
Depending on the type of parameter, we draw an apple or a tree. The same principle in the Creator class:
void Creator::visit(Basket& basket) {
Our general drawing method, which is called from the game loop, will be:
void Game::draw(std::vector<Object*>& allObjects) { Painter& painter = GetPainter(); std::vector<Object*>::iterator it = allObjects.begin(); std::vector<Object*>::iterator end = allObjects.end(); while(it != end) { (*it)->accept(painter); ++it; } }
When creating an object, we will need to visit it with a Creator, and when deleting, with a Destroyer. I think it's better than putting code in a constructor and destructor.
So, we solved all the problems that we have. Now:
- The code responsible for drawing, is in one file. The other two files contain the code for working with the Box2D library.
- Access to interfaces for working with graphics and Box2D is needed only from the classes Painter, Creator and Destroyer.
- The code is compiled quickly, because the header files for working with graphics and Box2D are included only in painter.cpp, creator.cpp and destroyer.cpp.
- When adding a new functionality will have to modify only Visitor'y. The hierarchy classes remain the same.
UpcastVisitor Interlayer
Here we could finish and enjoy the work done, but we notice with horror that in several places we have duplicated code. In the Destroyer class, all methods have the same appearance, for example, the same code is used for an apple and an orange:
void Destroyer::visit(Apple& apple) { m_box2dWorld.DestroyBody(apple.b2Body()); } void Destroyer::visit(Orange& orange) { m_box2dWorld.DestroyBody(orange.b2Body()); }
Attendance of all classes inherited from Box2DObject is implemented in Destroyer in the same way. You have to bring duplicate code into the additional method visitBox2DObject and call it from everywhere:
void Destroyer::visitBox2DObject(Box2DObject& object) { m_box2dWorld.DestroyBody(object.b2Body()); } void Destroyer::visit(Apple& apple) { visitBox2DObject(apple); } void Destroyer::visit(Orange& orange) { visitBox2DObject(orange); }
And do not want to do this. It would be desirable that instead of a heap of overloaded methods in the Destroyer class, which contain the same code, there was only one with the “correct” type of parameter - Box2DObject. And so that it works for Apple, Orange, Basket, etc., for example:
class Destroyer : public Visitor { public: void visit(Box2DObject& object) { m_box2dWorld.DestroyBody(object.b2Body()); }
Take the risk and add this method to the base class:
class Visitor { public: void visit(Box2DObject& object);
Unfortunately, it will never be called, because in the Box2DObject class there is no implementation of the accept method. And even if it were, it would be overloaded in derived classes and the result would be the same - the visit method with the parameter type Box2DObject would never be called. Then the idea of ​​a visitor-a layer between the base class Visitor and the classes Creator and Destroyer came to my mind. I called this layer UpcastVisitor. This is how the hierarchy of our visitor changes:

Now the most important thing is the code:
void UpcastVisitor::visit(Object& object) { } void UpcastVisitor::visit(Box2DObject& object) { visit(static_cast<Object&>(object)); } void UpcastVisitor::visit(Fruit& fruit) { visit(static_cast<Box2DObject&>(fruit)); } void UpcastVisitor::visit(Apple& apple) { visit(static_cast<Fruit&>(apple)); } void UpcastVisitor::visit(Orange& orange) { visit(static_cast<Fruit&>(orange)); }
If an Apple object is sent to UpcastVisitor, it leads it to the Fruit type, then to the Box2DObject type and at the very end to the Object type. By the same principle, each visited class in UpcastVisitor goes through the chain of hierarchy to the most basic one. Now we inherit the class Destroyer from UpcastVisitor and rejoice! If UpcastVisitor is implemented correctly, then we need to overload only one method.
class Destroyer : public UpcastVisitor { public: virtual void visit(Box2DObject& object) { m_box2dWorld.DestroyBody(object.b2Body()); }
UpcastVisitor will also come in handy for you in the Creator class, because the Box2D entity creation code is the same for apple and orange:
class Creator : public UpcastVisitor { public: virtual void visit(Fruit& fruit) {
Perfectly. Very easy life. One small drawback - you need to keep track of the UpcastVisitor class, which is not so difficult. Unfortunately, you can never completely get rid of shortcomings in your program, you can only choose and leave the smallest of them.
If you already use the visitor pattern, and you have a similar situation, you can safely create the UpcastVisitor class. It's very simple, you just need to add methods to visit all intermediate types in the base class Visitor, as well as monitor and update UpcastVisitor as new types appear in the hierarchy. Well, inherit from it as needed.
Thanks for attention! I hope this article will be useful to someone!
Update :
7vies suggests that in UpcastVisitor you can get rid of static_cast as follows:
void UpcastVisitor::visit(Box2DObject& object) { Object& o = object; visit(o); }
Thanks for the helpful advice!