The Visitor Pattern – part 6: specialized 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).

Specialized visitors

Implementation of the Visitor pattern often tends to be create wide interfaces with a huge number of methods. This happens because normally there is a big number of visitable elements already on start of the implementation and this number will get increased during the project lifetime. Generally, you should avoid wide interfaces as they are often complex and difficult to use. As the Visitor pattern creates one kind of methods only, the complexity of the interface stays constant no matter whether you have ten or hundred interface methods. But on implementation of the Visitor you will feel the pain if you have to implement such a huge number of methods. For example, let’s say you want to implement a couple of queries which should work with a group of elements only. Even if you are interested in this element group only, you have to implement all other interface methods too. This may result in a huge number of empty method implementations.

But no fear, there is an easy way out of this situation. Instead to follow a “one fits it all” approach, you may implement specialized visitors. But before we think about this kind of implementation I want to mention another solution which is often mentioned in this context but which comes with a big disadvantage. To avoid the need to implement empty methods you could define empty visitor interfaces. Instead of this explicit interface methods, you will create an implicit method definition e.g. within a design document. Within your elements you will now be able to implement this implicit method or not. If you implement it you can use type cast to check whether the visitor supports this method and is able to receive the element instance. This kind of implementation removes the disadvantage of the empty methods but comes with the bid disadvantage of some kind of implicit interface definition somewhere in your project documentation. And of course, this is a good source for implementation errors. Therefore, I don’t recommend this kind of implementation.

Instead I would prefer specialized visitors. That means I want to create several visitors which operate on a group of elements only. Let’s think about the following element structure: you have a shop with several articles and within these articles you have some soup groups like food.

  • Article
    • Book
    • Food
      • Cheese
      • Sausage

Within this little tree structure, we can find three leaves: Book, Cheese and Sausage. These elements are good candidates for the visitable interface. Furthermore, we have two nodes –  Article and Food – which are good candidates for the visitor interface.

The following source code shows a possible implementation of the two different Visitors. At first we define the interfaces for the Article Visitor and the Food Visitor and the according visitable elements.

class IArticleVisitor
{
public:
  virtual void InstanceReceiver(Book* book) = 0;
  virtual void InstanceReceiver(Cheese* radio) = 0;
  virtual void InstanceReceiver(Sausage* cheese) = 0;
};

class IFoodVisitor
{
public:
  virtual void InstanceReceiver(Cheese* book) = 0;
  virtual void InstanceReceiver(Sausage* radio) = 0;  
};

class IVisitableArticle
{
public:
  virtual void GetInstance(IArticleVisitor* visitor) = 0;
};

class IVisitableFood
{
public:
  virtual void GetInstance(IFoodVisitor* visitor) = 0;
};

Based on these interfaces we can implement the date elements.

class Article : IVisitableArticle
{
public:
  int mPrice;

  virtual void GetInstance(IArticleVisitor* visitor) {};
};

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

  std::string mTitle;

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

class Food : public Article, IVisitableFood
{
public:
  virtual void GetInstance(IFoodVisitor* visitor) {};
};

class Cheese : public Food
{
public:
  Cheese(std::string manufacturer, std::string name)
    : mManufacturer(manufacturer), mName(name){};

  std::string mManufacturer;
  std::string mName;

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

class Sausage : public Food
{
public:
  Sausage(std::string brandName)
    : mBrandName(brandName){};

  std::string mBrandName;

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

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

The implementation of the Visitors is done according to the examples we have seen in the previous articles. Therefore, the following code shows a template only and you can have a look into the previous articles for more information about implementation of the data containers, the Enumerator Visitors and the Query Visitors.

class MyFoodQuery : public IFoodVisitor
{
public:

  void ExecuteQuery(std::vector& foods)
  {    
  }

  void InstanceReceiver(Cheese* cheese)
  {
    // ...   
  }

  void InstanceReceiver(Sausage* sausage)
  {
    // ...
  }
  
private:
  std::vector mDescriptions;
};

Inherit Interfaces

Within the above example implementation of the interfaces you may see a possibility for a code optimization. The underlying elements which are covered by the Visitors exist in a base-derived relationship. If we have this kind of element hierarchy, the Visitor interfaces can also inherit from each other. So, we can use the “BaseVisitor” as base class of the “ArticleVisitor” to create a visitor interface hierarchy.

Interface Concept

During software design phase, you may want to decide about the structure of your Visitors. Should you create one Visitor for each group of elements or should you combine some of them or is it better to create one Visitor with all elements? Of course, this question is use case specific and depends on your application. Following I want to give you some common guidelines which may help you during software design.

Beside the “empty methods” the Visitor pattern comes with the downside of a bad maintainability in case the data elements change. If you must introduce new data elements, you must change all existing Visitors too. You can weaken both disadvantages if you create specialized interfaces because the data structure changes will affect a part of you Visitors only. As conclusion: if you have fix data elements and data structures you could use a small number of wide and general Visitor interfaces and if you have variable structures with a high possibility of changes, you should use specialized Visitor interfaces.

During software design phase, you can define the Visitor interfaces by using a top-down or bottom-up approach. The top-down approach starts at use case level. You should analyze your use cases and think about the concrete queries needed in these cases. At next you can think about the data elements needed in these queries. Based on the knowledge about the queries and their data elements, you can look for similarities and differences and create according use case specific Visitor interfaces.

If you don’t know the use cases and queries yet, or if you already know that there will be a lot more use cases in future, you can use the bottom-up approach. This method is based on the data. You should analyze the data structure and create Visitor interfaces which are suitable for software extensions and maintenances. As we already know, this can be solved with specialized Visitor interfaces. You should analyze the structure of your data. Like in the example above, your data normally has a tree-like inheritance architecture. Each tree node is a possible candidate for a Visitor interface. Of course, if you have a lot of nodes you may not create the same number of Visitors. Instead you should find the most important ones. Furthermore, you should create an architecture of inherited Visitors equal to the data structure. This will allow an easy integration of additional Visitors for nodes without Visitor in future.

Assessment

Often, the “one fits it all” approach results in a rigid software design which isn’t suitable in case of extensions or code maintenance. Therefore, you should create specialized Visitor patterns. The challenge is to find the right balance between too less wide interfaces and too much small interfaces. The top-down or bottom-up approach may help you during software design phase to define the interfaces according to the use cases and/or the data structure.

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.

Advertisements
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