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).
Extended enumerator visitor
Within the previous articles we have seen the enumerator Visitor. This kind of Visitor is responsible to traverse a data structure and enumerate all elements. It is used by the query Visitors or algorithm Visitors.
As we have seen, this separation of concerns results in a clean code with reusable Visitors. But there may be one issue we didn’t consider so far. Data structures may not contain the elements only. The may contain additional information too. This additional information could be needed by the query Visitors but at the moment our implementation will enumerator the elements only and does not provide the additional information.
Let’s have a look at the example we used so far. We have defined the Visitor interface and created some element objects.
//------------------------------------- // forward declaration //------------------------------------- class Book; class Radio; class Cheese; //------------------------------------- // interfaces //------------------------------------- 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; }; //------------------------------------- // Elements //------------------------------------- class Article : IVisitable { public: int mPrice; virtual void GetInstance(IVisitor* visitor) {}; }; class Book : public Article { public: Book(std::string title, int price) : mTitle(title) { mPrice = price; }; std::string mTitle; void GetInstance(IVisitor* visitor) { visitor->InstanceReceiver(this); } }; class Radio : public Article { public: Radio(std::string manufacturer, std::string model, int price) : mManufacturer(manufacturer), mModel(model) { mPrice = price; }; 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, int price, int importDuty) : mManufacturer(manufacturer), mName(name), mImportDuty(importDuty) { mPrice = price; }; std::string mManufacturer; std::string mName; int mImportDuty; void GetInstance(IVisitor* visitor) { visitor->InstanceReceiver(this); } };
As data container, we have a tree like structure which contains the history of ordered articles.
class Order { public: Order(Article* article, int count) : mArticle(article), mCount(count) {}; Article* mArticle; int mCount; }; class DailyOrders { public: std::vector mOrders; std::string mDate; }; class OrdersHistory { public: std::vector mDailyOrders; };
As you can see, this data container stores the elements and it contains additional information like the count and data of orders. A standard use case based on this data may be to calculate the total turnover. In this case we need the count-information stored within the order element. An easy solution is to create an extended enumerator Visitor. I call it “extended” because the enumerator will traverse over the elements and additional it will provide extended information about the data structure which stores the elements.
class ElementsEnumerator : IVisitor { public: void EnumerateAll(OrdersHistory& ordersHistory) { for (auto& dailyOrders : ordersHistory.mDailyOrders) { for (auto& order : dailyOrders.mOrders) { mActualOrder = ℴ order.mArticle->GetInstance(this); } } } int ActualOrderCount() { return mActualOrder->mCount; }; private: Order* mActualOrder; };
This extended enumerator Visitor is quite simple. It is based on the standard enumerator Visitor we used so far and adds a property only. So, we can reuse or extend existing code. The extended enumerator Visitor can be used within the query to calculate the total turnover.
class TurnoverQuery : public ElementsEnumerator { public: int ExecuteQuery(OrdersHistory& ordersHistory) { mTurnover = 0; EnumerateAll(ordersHistory); return mTurnover; } void InstanceReceiver(Book* book) { mTurnover += book->mPrice * ActualOrderCount(); } void InstanceReceiver(Radio* radio) { mTurnover += radio->mPrice * ActualOrderCount(); } void InstanceReceiver(Cheese* cheese) { mTurnover += (cheese->mPrice + cheese->mImportDuty) * ActualOrderCount(); } private: int mTurnover; };
And again, we have the same advantage. The query Visitors can be implemented in the same way as we already seen in the other examples. We do not reinvent the wheel or create a new concept. We use the existing concept and extend it with the new features.
The following code shows the example application which creates some data elements and executes the query.
int _tmain(int argc, _TCHAR* argv[]) { // prepare data Book* book = new Book("My book", 5); Radio* radio = new Radio("My manufacturer", "My model", 30); Cheese* cheese = new Cheese("My manufacturer", "My name", 5, 2); DailyOrders dailyOrders1; DailyOrders dailyOrders2; dailyOrders1.mDate = "20180101"; dailyOrders1.mOrders.push_back(Order(book, 3)); dailyOrders1.mOrders.push_back(Order(radio, 4)); dailyOrders1.mOrders.push_back(Order(cheese, 2)); dailyOrders2.mDate = "20180102"; dailyOrders2.mOrders.push_back(Order(book, 3)); dailyOrders2.mOrders.push_back(Order(cheese, 2)); OrdersHistory ordersHistory; ordersHistory.mDailyOrders.push_back(dailyOrders1); ordersHistory.mDailyOrders.push_back(dailyOrders2); // execute queries int turnover = TurnoverQuery().ExecuteQuery(ordersHistory); std::cout << "turnover: " << turnover << std::endl; // delete data delete book; delete radio; delete cheese; return 0; }
Assessment
As mentioned before, the implementation has the big advantage of matching with the base implementation of the pattern. These kinds of extended enumerators are equal to the simple enumerators. This increases the readability of the source code and does not add new complexity. Furthermore, it allows to extend existing source code without impact to the queries or algorithms which use the existing enumerator.
The downside of the concept is the separation of the data accesses. The query must get the needed data over two different interfaces and principles. The main data, stored within the data element, will be forwarded to the query by the double dispatch mechanism. The additional data will be determined by accessing the according properties via single dispatch. This will result in a more complex data query mechanism.
An alternative would be to make the needed structure elements visitable too. In this case the standard dispatching could be used. But this will increase the complexity of the whole data access mechanism because new we have a system of dependent elements. So far, the elements are independent of each other. If we make a structure element visitable which contains visitable elements, we have a hierarchical structure of inner and outer visitable objects. Writing enumerator Visitors and query Visitors will now become more difficult as we always have to think about whether we want to visit the inner element, the outer element or both. This will become very complex in systems with a deep element hierarchy. Beside the complexity of the system, such a kind of implementation will inevitable result in specialized enumerators and queries.
Outlook
Within the next articles we will think about the reusability of the different Visitors and remove the currently existing strict relationship between the enumerator and query Visitors. Furthermore, we will think about use cases which are often used as examples for the Visitor pattern but which could be implemented easier by using other patterns. This should help to avoid a misuse of the pattern, resulting in over-engineering. And finally, we will finish the article series with a summary of the whole topic.