Visitor Pattern in Java and Python
This article exists on YouTube
The entire content of this article is also available on YouTube. It's your choice: if you prefer video content, you can watch it on YouTube. If you prefer reading, feel free to continue here.
What is a Visitor Pattern?
The Visitor Pattern is a programming pattern used in software engineering and is part of the group of behavioral patterns. It may be used in any object-oriented programming language, such as Java, Python, C++, and so on. What it does is to separate algorithms from the objects on which they operate.
The primary advantage of the Visitor Pattern lies in its ability to facilitate the addition of new operations to existing object structures without requiring modifications to those structures themselves. This capability proves especially valuable in scenarios where there is a predetermined set of classes, yet the necessity for incorporating new operations arises frequently.
So what would real-world scenarios in which the Visitor Pattern is used be like?
In a document processing system, different types of documents (such as XML, JSON, HTML) can be considered as different elements. A visitor class can be created to process these documents. For example, a RenderVisitor can be used to render documents in a GUI, a ValidationVisitor can be used to validate the structure of the documents, and so on. The Visitor Pattern allows these operations to be added and modified independently from the document classes.
In a GUI system, different components (like Button, Checkbox, TextField) can be considered as different elements. A visitor class can be created to perform operations on these components. For example, a DrawVisitor can be used to draw the components on the screen, an EnableVisitor can be used to enable or disable the components, and so on. The Visitor Pattern allows these operations to be added and modified independently from the component classes.
In a database system, different types fo databases (such as SQL, NoSQL, Graph) can be considered as different elements. A visitor class can be created to perform operations on these databases. For example, a BackupVisitor can be used to backup the databases, a MigrationVisitor can be used to migrate data from one database to another, and so on. The Visitor Pattern allows these operations to be added and modified independently from the database classes.
In a network system, different types of protocols (such as HTTP, FTP, SMTP) can be considered as different elements. A visitor class can be created to process these protocols. For example, a LoggingVisitor can be used to log the network traffic, a SecurityVisitor can be used to check for security vulnerabilities in the network traffic, and so on. The Visitor Pattern allows these operations to be added and modified independently from the protocol classes.
In all these cases, the Visitor Pattetrn provides a way to seperate the operations from the objects on which they operate, making the system more modular and easier to extend and maintain. It also promotes the Single Responsibility Principle, as each visitor class has a single responsibility. However, it can make the code more complex and harder to understand, especially for developers who are not familiar with this pattern. Therefore, it should be used in accordance with the general team knowledge.
What is the structure of a Visitor Pattern?
-
Visitor
This interface defines the contract for how visitors interact with elements. It declares methods for visiting each concrete element type (i.e. visit(ConcreteElementA) or visit(ConcreteElementB)). These methods serve as entry points for concrete visitors to perform specific operations on the corresponding elements. -
ConcreteVisitor Classes
These classes implement the Visitor interface. Each concrete visitor provides its own implementations for the visit methods defined in the interface. The behavior within these mehtods determines what actions are taken when a specific element is visited. -
Element
This interface defines the contract for the concrete elements. It declares a method accept(visitor visitor) which allows elements to delegate the operation to the provided visitor. -
ConcreteElement Classes
These classes represent specific types of elements in the object structure. They inherit from the Element class and implement the accept method.
Example Codes
There are a number of programming languages where the Visitor Pattern can prove useful. Every language that supports the construct of classes can make use of this pattern.
All the codes within this article can also be found on the GitHub repository programming-patterns and in there within the directory Visitor Pattern.
Java Example
The following example is a Java application putting the Visitor Pattern into action. All the following files need to be placed into the same folder so that the code within the files can work properly, because the files also reference each other. Being the smallest possible code example, this little project consists of seven Java files:
- Visitor.java
- ConcreteVisitorA.java
- ConcreteVisitorB.java
- Element.java
- ConcreteElementA.java
- ConcreteElementB.java
- Main.java
public interface Visitor {
void visit(ConcreteElementA element);
void visit(ConcreteElementB element);
}
public class ConcreteVisitorA implements Visitor {
@Override
public void visit(ConcreteElementA element) {
System.out.println("ConcreteVisitorA visits ConcreteElementA: " + element.operationA());
}
@Override
public void visit(ConcreteElementB element) {
System.out.println("ConcreteVisitorA visits ConcreteElementB: " + element.operationB());
}
}
public class ConcreteVisitorB implements Visitor {
@Override
public void visit(ConcreteElementA element) {
System.out.println("ConcreteVisitorB visits ConcreteElementA: " + element.operationA());
}
@Override
public void visit(ConcreteElementB element) {
System.out.println("ConcreteVisitorB visits ConcreteElementB: " + element.operationB());
}
}
public interface Element {
void accept(Visitor visitor);
}
public class ConcreteElementA implements Element {
@Override
public void accept(Visitor visitor) {
visitor.visit(this);
}
public String operationA() {
return "ConcreteElementA operation";
}
}
public class ConcreteElementB implements Element {
@Override
public void accept(Visitor visitor) {
visitor.visit(this);
}
public String operationB() {
return "ConcreteElementB operation";
}
}
And that is it already. The main construct of the Visitor Pattern consists of these six Java files. The only thing that remains is to apply their logic within the executing main class. The main(...) method creates an instance of each concrete class. After that both elements are brought to accept each of the visitors. This leads to a series of command line interface prints.
public class Main {
public static void main(String[] args) {
System.out.println("Visitor Pattern in Java");
System.out.println("-----------------------");
System.out.println();
Element elementA = new ConcreteElementA();
Element elementB = new ConcreteElementB();
Visitor visitorA = new ConcreteVisitorA();
Visitor visitorB = new ConcreteVisitorB();
elementA.accept(visitorA);
elementA.accept(visitorB);
elementB.accept(visitorA);
elementB.accept(visitorB);
}
}
There also needs to be a Manifest.txt file within the same folder. This is a metadata file used primarily in JAR files. It contains information about the JAR file itself and its contents. It is placed within thie META-INF folder within the JAR archive file structure.
Main-Class: Main
To compile this code into an executable Java application which would be a JAR file, a couple of commands need to be executed from within the same directory. The commands are found within the build file.
@echo off
javac *.java
jar cvfm VisitorDemo.jar Manifest.txt *.class
#!/bin/bash
javac *.java
jar cvfm VisitorDemo.jar Manifest.txt *.class
Now that the compilation has taken place, there are new files within the same directory. Every JAVA file has its equivalent CLASS file, and there is also a file called VisitorDemo.jar. This is the Java application that needs to be executed. The command for that is within the run file.
@echo off
java -jar VisitorDemo.jar
#!/bin/bash
java -jar VisitorDemo.jar
Testing the Java Example
Now that all the files are ready and within their correct location they can be put to test. First we need to compile the Java code and then we can execute it to see what it will print in the Windows Command Prompt or in the MacOS / Linux Terminal.
Python Example
The following example is a Python application putting the Visitor Pattern into action. All the following files need to be placed into the same folder so that the code within the files can work properly, because the files also reference each other. As this is the smallest possible example, this little project consists of seven Python files:
- visitor.py
- concrete_visitor_a.py
- concrete_visitor_b.py
- element.py
- concrete_element_a.py
- concrete_element_b.py
- main.py
Python does not support the overloading of methods. The result of that is that we need to change the names of the visit-methods a little bit.
from abc import ABC, abstractmethod
class Visitor(ABC):
@abstractmethod
def visit_concrete_element_a(self, element_a):
pass
@abstractmethod
def visit_concrete_element_b(self, element_b):
pass
from visitor import Visitor
class ConcreteVisitorA(Visitor):
def visit_concrete_element_a(self, element_a):
print("ConcreteVisitorA visits ConcreteElementA:", element_a.operation_a())
def visit_concrete_element_b(self, element_b):
print("ConcreteVisitorA visits ConcreteElementB:", element_b.operation_b())
from visitor import Visitor
class ConcreteVisitorB(Visitor):
def visit_concrete_element_a(self, element_a):
print("ConcreteVisitorB visits ConcreteElementA:", element_a.operation_a())
def visit_concrete_element_b(self, element_b):
print("ConcreteVisitorB visits ConcreteElementB:", element_b.operation_b())
import re
class Element:
def accept(self, visitor):
snake_case_string = re.sub(r"(?<!^)(?=[A-Z])", "_", type(self).__name__).lower()
method_name = f"visit_{snake_case_string}"
visitor_method = getattr(visitor, method_name)
visitor_method(self)
from element import Element
class ConcreteElementA(Element):
def operation_a(self):
return "ConcreteElementA operation"
from element import Element
class ConcreteElementB(Element):
def operation_b(self):
return "ConcreteElementB operation"
from concrete_element_a import ConcreteElementA
from concrete_element_b import ConcreteElementB
from concrete_visitor_a import ConcreteVisitorA
from concrete_visitor_b import ConcreteVisitorB
def main():
print("Visitor Pattern in Python")
print("-------------------------")
print()
elementA = ConcreteElementA()
elementB = ConcreteElementB()
visitorA = ConcreteVisitorA()
visitorB = ConcreteVisitorB()
elementA.accept(visitorA)
elementA.accept(visitorB)
elementB.accept(visitorA)
elementB.accept(visitorB)
if __name__ == "__main__":
main()
There is no need to compile anything in Python. The code is executed directly using the python command under Windows and the python3 command under MacOS / Linux.
Testing the Python Example
Now that all the files are ready and within their correct location they can be put to test. Python does not need to be compiled before being run. Therefore we can execute it right away to see what it will print in the Windows Command Prompt or in the MacOS / Linux Terminal.
More Information
So, where do I have my information from? In general, there are lots of places on the internet that offer great information. But that is just one source of information. Another source would be books. Those have the advantage that usually several people work on the same book before it is released, raising the chance for distribution of good and proven information.
Head First - Design Patterns
Building Extensible and Maintainable Object-Oriented Software
A book that has proven to be a valuable asset within my personal library is Head First - Design Patterns by the publisher O'Reilly. It has a fun approach to this topic, having recurring characters, such as the Guru, the Developer, the Skeptical Developer, Joe, and many more.
Java Design Patterns
A Hands-On Experience with Real-World Examples
Another highly valuable book is Java Design Patterns: A Hands-On Experience with Real-World Examples by the publisher Apress. It offers a conservative and rather scientific approach to the subject. It also offers code examples in Java, although any object-oriented programming language applies to the design patterns shown here.