Visitor Design Pattern

Apparently Amazon asks this question.

Learned in CS247 - Software Engineering Principles.

What problem does Visitor pattern solve?

“How do we write a method that depends on two polymorphic types?

Visitor is a behavioural design pattern that lets you separate algorithms from the objects on which they operate. Combining overloading with overriding.

AFTER we’ve established the visitor pattern, we can add more visitors simply by adding more subclasses under the visitor. This is what doesn’t require changes to the public interface.

Whereas, if you didn’t use visitor, then to get more functionality out of your class with the accept method (the AST hierarchy in the course notes) you could only do this by adding a pure virtual method to all classes in the AST hierarchy.

Resource: https://refactoring.guru/design-patterns/visitor

Example: Lecture 20 of CS247 Lectures

We might want different behaviours for each combination of these concrete classes.

class Weapon {
	public:
		virtual void strike(Enemy& e) = 0;
};

We can override this in Rock and Stick, but we don’t know which enemy we’re hitting. Same problem exists in reverse if we implement it inside of Enemy.

  • Notice that dynamic casting can partially fix this issue, but not completely

Solution: Visitor Pattern - combine overloading and overriding.

class Enemy {
	public:
		virtual void beStruckBy(weapon& w) = 0;
		...
};
 
class Monster: public Enemy {
	public:
		void beStruckBy(Weapon& w) {
			w.strike(*this);
		}
};
 
class Turtle: public Enemy {
	public:
		void beStruckBy(Weapon& w) {
			w.strike(*this);
		}
};
 
class Weapon {
	public:
		virtual void strike(Monster& m) = 0;
		virtual void strike(Turtle& t) = 0;
};
 
class Rock: public Weapon {
	public:
		void strike(Monster& m) override {
			cout << "Rock vs. Monster" << endl;
		}
		void strike(Turtle& t) override {
			cout << "Rock vs. Tutrtle" << endl;
		}
}; // Stick looks similar
 
 
Enemy* e = ...;
Weapon* w = ...;
e->beStruckBy(*w);
  1. beStruckBy is a virtual function →\rightarrow→ either calling Monster::beStruckBy or Turtle::beStruckBy (runtime)
  2. Call w.strike on the Weapon&. Strike is virtual →\rightarrow→ Rock::strike or Stick::strike(runtime)
  3. Are we calling Rock::strike(Turtle&) or Rock::strike(Monster&) ? We know the type of this. If this is a Turtle*, then use the Turtle version. If this is a Monster* , then use the Monster version (compile time)

Example Compiler Design (Ross)

Another use of Visitor Pattern

Another use of the visitor pattern is add behavior and functionality to classes, without changing the classes themselves.

Consider the following compiler design example.

How to Implement

Steps

  1. Declare the visitor interface with a set of “visiting” methods, one per each concrete element class that exists in the program.
  2. Declare the element interface. If you’re working with an existing element class hierarchy, add the abstract “acceptance” method to the base class of the hierarchy. This method should accept a visitor object as an argument.
  3. Implement the acceptance methods in all concrete element classes. These methods must simply redirect the call to a visiting method on the incoming visitor object which matches the class of the current element.
  4. The element classes should only work with visitors via the visitor interface. Visitors, however, must be aware of all concrete element classes, referenced as parameter types of the visiting methods.
  5. For each behavior that can’t be implemented inside the element hierarchy, create a new concrete visitor class and implement all of the visiting methods. You might encounter a situation where the visitor will need access to some private members of the element class. In this case, you can either make these fields or methods public, violating the element’s encapsulation, or nest the visitor class in the element class. The latter is only possible if you’re lucky to work with a programming language that supports nested classes.
  6. The client must create visitor objects and pass them into elements via “acceptance” methods.