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).
Enumerator visitor
Within the previous article we learned that one purpose of the Visitor pattern is to enumerate over the elements of a complex data structure and to apply an algorithm an each of the elements. We also learned that these two concerns – enumeration and algorithm – should be separated into different classes.
Now we want to implement the example application, introduced within the previous article, by using the Visitor pattern. As we focus on the enumeration use case I called this article “enumerator visitor” but of course, the algorithm is implemented as visitor too.
Within the example we want to manage one article only: a book. But we will have a data container with a complex structure. This data container is used to store the order history for this book. Based on this order history we want to write two queries. On query should show all ordered books in chronological sequence. If a book was sold server times then it will be shown several times by the query too. As we also want to know which books of our collection were sold, we create a second query which should show them. In this case we need something like a distinct search within our order history.
Let’s start the implementation by defining the Visitor pattern interfaces.
class IVisitor { public: virtual void InstanceReceiver(Book* book) = 0; }; class IVisitable { public: virtual void GetInstance(IVisitor* visitor) = 0; };
At next we add the element and implement the “IVisitable” interface. Furthermore, we add the data container. The order history is a collection of daily orders. Each daily order contains the sold books of one day.
//------------------------------------- // Element //------------------------------------- class Book : IVisitable { public: Book(std::string title) : mTitle(title) {}; std::string mTitle; void GetInstance(IVisitor* visitor) { visitor->InstanceReceiver(this); } }; //------------------------------------- // Container //------------------------------------- class Order { public: Order(Book book, int count) : mBook(book), mCount(count) {}; Book mBook; int mCount; }; class DailyOrders { public: std::vector mOrders; std::string mDate; }; class OrdersHistory { public: std::vector mDailyOrders; };
Now it is time to implement the Visitors. As a base functionality, we need the enumeration of all elements. Thereafter the queries can be implemented based on these enumerations. So, let’s create an abstract base class for the enumerator does implement the traversing and is of type “IVisitor”. The visitor methods must be implemented by the queries. Therefore, the enumerator class is abstract and will not implement these methods. Instead the queries will derive from the enumerator. So, they inherit the traversing implementation and will add the visitor implementation.
//------------------------------------- // Enumerator //------------------------------------- class ElementsEnumerator : IVisitor { public: void EnumerateAll(OrdersHistory& ordersHistory) { for(auto& dailyOrders : ordersHistory.mDailyOrders) { std::for_each(dailyOrders.mOrders.begin(), dailyOrders.mOrders.end(), [&](Order& order){order.mBook.GetInstance(this); }); } } }; //------------------------------------- // queries //------------------------------------- class OrderedBooksHistoryQuery : public ElementsEnumerator { public: std::vector ExecuteQuery(OrdersHistory& ordersHistory) { EnumerateAll(ordersHistory); return mTitles; } void InstanceReceiver(Book* book) { mTitles.push_back(book->mTitle); } private: std::vector mTitles; };
This will solve our first use case: show complete history. The second use case can be implemented in nearly the same way. We just have to add a check whether an element was already enumerated. To keep it simple we use the book title for this check.
//------------------------------------- // Enumerator //------------------------------------- class DistinctElementsEnumerator : IVisitor { public: void EnumerateAll(OrdersHistory& ordersHistory) { mAlreadyEnumerated.clear(); for (auto& dailyOrders : ordersHistory.mDailyOrders) { for (auto& order : dailyOrders.mOrders) { auto iterator = std::find(mAlreadyEnumerated.begin(), mAlreadyEnumerated.end(), order.mBook.mTitle); if (iterator == mAlreadyEnumerated.end()) { mAlreadyEnumerated.push_back(order.mBook.mTitle); order.mBook.GetInstance(this); } } } } private: std::vector mAlreadyEnumerated; }; //------------------------------------- // queries //------------------------------------- class OrderedBooksQuery : public DistinctElementsEnumerator { public: std::vector ExecuteQuery(OrdersHistory& ordersHistory) { EnumerateAll(ordersHistory); return mTitles; } void InstanceReceiver(Book* book) { mTitles.push_back(book->mTitle); } private: std::vector mTitles; };
The following example application shows how to use the queries to analyze the data container.
int _tmain(int argc, _TCHAR* argv[]) { // prepare data Book book1("My book 1"); Book book2("My book 2"); Book book3("My book 3"); Book book4("My book 4"); Book book5("My book 5"); DailyOrders dailyOrders1; DailyOrders dailyOrders2; dailyOrders1.mDate = "20180101"; dailyOrders1.mOrders.push_back(Order(book1, 3)); dailyOrders1.mOrders.push_back(Order(book2, 4)); dailyOrders1.mOrders.push_back(Order(book4, 2)); dailyOrders2.mDate = "20180102"; dailyOrders2.mOrders.push_back(Order(book2, 3)); dailyOrders2.mOrders.push_back(Order(book4, 2)); dailyOrders2.mOrders.push_back(Order(book5, 2)); OrdersHistory ordersHistory; ordersHistory.mDailyOrders.push_back(dailyOrders1); ordersHistory.mDailyOrders.push_back(dailyOrders2); // execute queries std::vector titles; titles = OrderedBooksHistoryQuery().ExecuteQuery(ordersHistory); std::cout << "---Complete Order History---" << std::endl; std::for_each(titles.begin(), titles.end(), [](std::string title){std::cout << title << std::endl;; }); titles = OrderedBooksQuery().ExecuteQuery(ordersHistory); std::cout << std::endl; std::cout << "---Ordered Books---" << std::endl; std::for_each(titles.begin(), titles.end(), [](std::string title){std::cout << title << std::endl;; }); return 0; }
Assessment
The example shows some nice features of the Visitor pattern. We have a clear separation of the different concerns. New kinds of enumerations which may contain sorting or filter functionality can be added without changing the data container. Similarly, it is possible to add new algorithms.
But I think the implementation has two downsides too. We have implemented two algorithms but they are equal. They just inherit from a different enumerator. It would be nice if we have to implement the algorithm just once and can use it with any enumerator. Within a further article I want to show some other implementation techniques of the pattern which solve this issue.
The second issue of the implementation is not a technical one. It is a question of software design. Within this example the Visitor pattern is used to enumerator the elements. Beside this enumeration aspect it adds no further benefit. If we just want to enumerate items, we could use the Iterator pattern. Of course, the focus of this article was to show one aspect of the Visitor pattern only. Therefore, with respect to this aim the implementation is fine. I just mentioned this objection to keep in mind that the use of the Visitor pattern may be over-engineering in some use cases.
Outlook
Within the next article of this series I want to show the second and in my opinion the most important aspect of the Visitor pattern: the type specific dispatching of the container Elements.