This article is one part of a series about the Visitor pattern. Please read the previous articles to get an overview about the pattern and to get the needed basics to understand this article.
The example implementation of this article is focused on the Visitor pattern. To keep the example as short as possible I intentional ignored some mandatory programming guidelines. Therefore, please keep in mind that the implementation should show the idea of the Visitor pattern but it ignores things like const correctness, private members or RAII (resource acquisition is initialization).
Dispatcher visitor
As mentioned within the first article of this series, one task of the Visitor pattern is to access the specific element type instances and provides these instance to a receiver. This dispatching is done by using double dispatch.
Within this article we want to look at a possible implementation of this dispatching task. Within the example, the data container stores the elements without to know their concrete types. We have three different element types, all based on a common base class. Like in the previous example we start by defining the Visitor interfaces.
class Book; class Radio; class Cheese; class IVisitor { public: virtual void InstanceReceiver(Book* book) = 0; virtual void InstanceReceiver(Radio* radio) = 0; virtual void InstanceReceiver(Cheese* cheese) = 0; }; class IVisitable { public: virtual void GetInstance(IVisitor* visitor) = 0; };
At next we implement the element classes.
class Article : IVisitable { public: int mPrice; virtual void GetInstance(IVisitor* visitor) {}; }; class Book : public Article { public: Book(std::string title) : mTitle(title) {}; std::string mTitle; void GetInstance(IVisitor* visitor) { visitor->InstanceReceiver(this); } }; class Radio : public Article { public: Radio(std::string manufacturer, std::string model) : mManufacturer(manufacturer), mModel(model){}; std::string mManufacturer; std::string mModel; void GetInstance(IVisitor* visitor) { visitor->InstanceReceiver(this); } }; class Cheese : public Article { public: Cheese(std::string manufacturer, std::string name) : mManufacturer(manufacturer), mName(name){}; std::string mManufacturer; std::string mName; void GetInstance(IVisitor* visitor) { visitor->InstanceReceiver(this); } };
This time we will not implement a data container class. We use a standard vector instead. The abstract base Visitor to enumerate all elements is accordingly simple.
class ElementsEnumerator : IVisitor { public: void EnumerateAll(std::vector<Article>& articles) { std::for_each(articles.begin(), articles.end(), [&](Article* article){article->GetInstance(this); }); } };
Based on the enumerator we would implement a query Visitor. This query should get all article descriptions and therefore it needs to analyze the type specific properties. This will be done in the dispatcher functions of the Visitor.
class ArticleDescriptionsQuery : public ElementsEnumerator { public: std::vector ExecuteQuery(std::vector<Article>& articles) { EnumerateAll(articles); return mDescriptions; } void InstanceReceiver(Book* book) { mDescriptions.push_back("Book: " + book->mTitle); } void InstanceReceiver(Radio* radio) { mDescriptions.push_back("Radio: " + radio->mManufacturer + ", " + radio->mModel); } void InstanceReceiver(Cheese* cheese) { mDescriptions.push_back("Cheese: " + cheese->mManufacturer + ", " + cheese->mName); } private: std::vector mDescriptions; };
We could now use this query within a little test application.
int _tmain(int argc, _TCHAR* argv[]) { // prepare data std::vector<Article> articles; articles.push_back(new Book("My book")); articles.push_back(new Radio("My manufacturer", "My model")); articles.push_back(new Cheese("My manufacturer", "My name")); // execute query std::vector descriptions; descriptions = ArticleDescriptionsQuery().ExecuteQuery(articles); std::for_each(descriptions.begin(), descriptions.end(), [](std::string description){std::cout << description << std::endl;; }); // delete data std::for_each(articles.begin(), articles.end(), [](Article* article){delete article; }); return 0; }
Solution without Visitor pattern
The implemented example shows a solution for a standard programming use case: we want to enumerate over elements which are stored as base class pointers and have to access their concrete type. Of course, this can be implemented by using some other techniques too. A popular solution is casting. Therefore, I want to spend some time and compare the Visitor pattern with common casting.
At first, we want to think about the advantages of casting. These advantages result in the fact that we can omit the complex Visitor pattern. So, we eliminate the issues which came with this complex pattern.
But on the other hand, the casting solution – as simple as it is – will add some issues too. At first, there are some technical disadvantages. A dynamic cast costs performance. And it gets around the strict type checking done by the compiler and therefore the chance for errors gets increased. At second there is a software design issue which may result in misunderstanding and errors. I want to call this design issue a “lying interface”. The component offers an interface which is used by a client. This explicit contract is expanded by some implicit agreements which are not part of the contract. So, hopefully both partners keep these agreements, otherwise we could result in undefined behavior of the application. So, what do I mean with “lying interfaces”? The component promises to deliver elements of base type and the client will use these elements. But within a hidden agreement the component promises to deliver derived type object instances. And the client does expect these derived types. So, the interface does not reflect the real contract between the component and the client. If it is an interface between two strong coupled objects within a component this is a minor issue because in this case the technical point of view matters. But if it is an interface which is used by another component this may result in big issues and errors because in this case the logical point of view independent of technical details matters. In such external interfaces, you may offer some type specific query methods instead of one method returning the whole element container with mixed types.
Assessment
The Visitor is used as dispatcher between the data container which manages elements as pointers of base class and the processing component which wants to execute functionality depending on the concrete types of the elements. The Visitor pattern allows an efficient implementation of this use case. But the pattern comes with the downside of the complexity and some other pattern specific disadvantages. Depending on the individual use case it may be over-engineering to use the Visitor pattern if the dispatching functionality is used only. In such cases you may use casting in component internal interfaces, or type specific query methods in external interfaces.
Outlook
Within the next article of this series I want to combine the two single use cases seen so far: the enumerator visitor and the dispatcher visitor.