The Visitor Pattern – part 7: reusable enumerator and query visitors

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).

 

Reusable visitors

Within the examples so far, we created enumerator and query visitors. This separation of concerns leads to clean, easy to understand and maintainable code. The query visitors inherit from the enumerator visitors and can use their traversing features. This will allow to implement different queries based on one enumerator. But so far it is not possible to use one query based on different enumerators. As C++ supports multiple inheritance it would be possible to inherit from several enumerators. But this will result in a complex and difficult interface of the query visitor because depending on the data structure we must use an individual subset of the interface only.

It would be better to have independent components and create a loose coupling depending on the actual use case. Therefore, we should remove the inheritance which creates a very strong coupling and use composition instead.

If we use composition, we will implement independent enumerators and queries. The enumerators will be implemented as concreate classes and no longer as abstract base classes. Furthermore, the enumerators will support an enumeration interface. The queries will no longer be coupled with a concreate data structure. Instead they use the enumeration interface to access the elements, independent of the concreate structure.

Let’s implement an according example. We will use the example application of the previous articles with the order history data structure. Additional we create a new data structure which represents the article stock. To keep it simple we will use one article only. Please keep in mind that the Visitor pattern isn’t the best choice in this kind of use cases, as the dispatching aspect disappears. I removed the dispatching aspect only to focus on the composition software design.

A query should be created which lists all book titles. This query should be used together with each of the two data structures. Therefore, we want to create independent components and create a use case specific loose coupling only.

At first, we define the interfaces. Beside the well-known default Visitor interfaces, we add an additional interface for the enumerator visitor.

class IVisitable
{
public:
  virtual void GetInstance(IVisitor* visitor) = 0;
};

class IVisitor
{
public:
  virtual void InstanceReceiver(Book* book) = 0;
};

class IVisitorEnumerator : public IVisitor
{
public:
  virtual void EnumerateAll() = 0;
};

 

At next we add the date element and two data structures. The stock is a simple list and the order history a tree-like structure.

//-------------------------------------
// Element
//-------------------------------------

class Book : IVisitable
{
public:
  Book(std::string title) : mTitle(title) {};

  std::string mTitle;

  void GetInstance(IVisitor* visitor)
  {
    visitor->InstanceReceiver(this);
  }
};

//-------------------------------------
// container 1
//-------------------------------------

class StockItem
{
public:
  StockItem(Book book, int count) : mBook(book), mCount(count) {};

  Book mBook;
  int mCount;
};

class Stock
{
public:
  std::vector mStockItems;
};

//-------------------------------------
// container 2
//-------------------------------------

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;
};

 

Based on the enumerator visitor interface we will now be able to implement the enumerators for the two data structures. Within the constructor we will pass the query visitor and the data structure. Of course, this is a kind of implicit design definition and you may use another way to set the dependencies between the instances. For example, instead of the generic enumerator interface you can create two individual interfaces and specify these methods.

class OrderHistoryEnumerator : public IVisitorEnumerator
{
public:
  OrderHistoryEnumerator(IVisitor& elementsReceiver, OrdersHistory& ordersHistory) 
    : mElementsReceiver(elementsReceiver), mOrdersHistory(ordersHistory) {};

  void EnumerateAll()
  {
    for (auto& dailyOrders : mOrdersHistory.mDailyOrders)
    {
      std::for_each(dailyOrders.mOrders.begin(), dailyOrders.mOrders.end(),
        [&](Order& order){order.mBook.GetInstance(this); });
    }
  }

  void InstanceReceiver(Book* book)
  {
    mElementsReceiver.InstanceReceiver(book);
  }

private:
  IVisitor& mElementsReceiver;
  OrdersHistory& mOrdersHistory;
};


class StockEnumerator : public IVisitorEnumerator
{
public:
  StockEnumerator(IVisitor& elementsReceiver, Stock& stock) 
    : mElementsReceiver(elementsReceiver), mStock(stock) {};

  void EnumerateAll()
  {
    std::for_each(mStock.mStockItems.begin(), mStock.mStockItems.end(),
      [&](StockItem& item){item.mBook.GetInstance(this); });
  }

  void InstanceReceiver(Book* book)
  {
    mElementsReceiver.InstanceReceiver(book);
  }

private:
  IVisitor& mElementsReceiver;
  Stock& mStock;
};

 

The query itself is very easy to implement. You pass the enumerator visitor and execute enumeration and you implement the instance receiver methods. Like before you may use implicit design rules or you may define a query visitor interface to define the “ExecuteQuery” method.

class BookTitlesQuery : public IVisitor
{
public:

  std::vector ExecuteQuery(IVisitorEnumerator& enumerator)
  {
    mTitles.clear();
    enumerator.EnumerateAll();
    return mTitles;
  }

  void InstanceReceiver(Book* book)
  {
    mTitles.push_back(book->mTitle);
  }

private:
  std::vector mTitles;
};

 

As mentioned before, this implementation approach allows a loose and use case specific coupling of the components. Within a test application we can therefore create the data structures, the enumerators and the queries and we use composition to create the connection between the components. As you can see, this connection is created within the context of the test method only. Such a loose coupling will allow an easy maintenance and extension of the source code. It will be easy to add new queries based on the existing enumerators as well as add new data structures and enumerators and use them within existing queries.

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");

  Stock stock;
  stock.mStockItems.push_back(StockItem(book1, 20));
  stock.mStockItems.push_back(StockItem(book2, 30));
  stock.mStockItems.push_back(StockItem(book3, 10));
  stock.mStockItems.push_back(StockItem(book4, 50));
  stock.mStockItems.push_back(StockItem(book5, 10));

  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;

  BookTitlesQuery bookTitlesQuery;

  StockEnumerator stockEnumerator(bookTitlesQuery, stock);
  OrderHistoryEnumerator historyEnumerator(bookTitlesQuery, ordersHistory);

  titles = bookTitlesQuery.ExecuteQuery(stockEnumerator);
  
  std::cout << std::endl;
  std::cout << "---Stock---" << std::endl;

  std::for_each(titles.begin(), titles.end(),
    [](std::string title){std::cout << title << std::endl;; });

  titles = bookTitlesQuery.ExecuteQuery(historyEnumerator);

  std::cout << std::endl;
  std::cout << "---Order History---" << std::endl;

  std::for_each(titles.begin(), titles.end(),
    [](std::string title){std::cout << title << std::endl;; });

  return 0;
}

 

Assessment

The enumerator and query Visitors together form a unit which is needed to solve a use case. Instead of implementing fix units we are now able to implement independent components and connect them depending on the use cases. This advantage of maintainable and extensible code comes with a minor disadvantage. As you can see within the example we have to define and implement some additional interfaces. But this additional work is negligible. Furthermore, the enumerator visitors must implement all visitor methods even if they are used only, to pass on the element instance to the query visitor.

I would recommend using composition over inheritance as the advantages go far beyond the disadvantages. Of course, of you have a very fixed use case with data structure specific queries only, you could use the inheritance concept. If you don’t use the flexibility of independent enumerators and queries, then you don’t need to implement such a flexibility.

 

Outlook

Within the next articles 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 and we will finish the article series with a summary of the whole topic.

Werbeanzeigen
Dieser Beitrag wurde unter C++ veröffentlicht. Setze ein Lesezeichen auf den Permalink.

Kommentar verfassen

Trage deine Daten unten ein oder klicke ein Icon um dich einzuloggen:

WordPress.com-Logo

Du kommentierst mit Deinem WordPress.com-Konto. Abmelden /  Ändern )

Google Foto

Du kommentierst mit Deinem Google-Konto. Abmelden /  Ändern )

Twitter-Bild

Du kommentierst mit Deinem Twitter-Konto. Abmelden /  Ändern )

Facebook-Foto

Du kommentierst mit Deinem Facebook-Konto. Abmelden /  Ändern )

Verbinde mit %s