Appearance
Visitor Pattern
The Visitor pattern allows you to define new operations on a structure of objects without changing the classes of the elements on which it operates. It separates an algorithm from the object structure, enabling the addition of new operations through new visitor classes.
The design of the Visitor pattern can be complex. In the original design by the Gang of Four (GoF), it employs a callback mechanism to simulate what is known as "double dispatch," since Java only supports single dispatch based on polymorphism. This forced simulation increases the complexity of the code.
To illustrate a simplified version of the Visitor pattern, let’s say we want to recursively traverse a directory structure to find all .java
files. A typical recursive implementation might look like this:
java
void scan(File dir, List<File> collector) {
for (File file : dir.listFiles()) {
if (file.isFile() && file.getName().endsWith(".java")) {
collector.add(file);
} else if (file.isDirectory()) {
// Recursive call:
scan(file, collector);
}
}
}
The problem with this code is that the scanning logic and the logic for processing .java
files are mixed together. If we later want to add functionality to clean up .class
files, we would have to repeat the scanning logic.
The Visitor pattern separates the data structure (in this case, a tree structure of folders and files) from the operations on it (finding files). If we want to add a new operation (like cleaning up .class
files), we only need to add a new visitor without changing the existing logic.
Implementation Steps
- Define the Visitor Interface: This interface declares the operations that can be performed on the elements.
java
public interface Visitor {
// Visit directory:
void visitDir(File dir);
// Visit file:
void visitFile(File file);
}
- Define the Data Structure: Create a class to hold the file structure.
java
public class FileStructure {
// Root directory:
private File path;
public FileStructure(File path) {
this.path = path;
}
}
- Implement the Handle Method: Add a method in
FileStructure
to accept a visitor.
java
public class FileStructure {
...
public void handle(Visitor visitor) {
scan(this.path, visitor);
}
private void scan(File file, Visitor visitor) {
if (file.isDirectory()) {
// Let the visitor process the directory:
visitor.visitDir(file);
for (File sub : file.listFiles()) {
// Recursively process subdirectories:
scan(sub, visitor);
}
} else if (file.isFile()) {
// Let the visitor process the file:
visitor.visitFile(file);
}
}
}
Implementing Visitors
Now we can implement visitors for different operations. For example, to find .java
files:
java
public class JavaFileVisitor implements Visitor {
public void visitDir(File dir) {
System.out.println("Visit dir: " + dir);
}
public void visitFile(File file) {
if (file.getName().endsWith(".java")) {
System.out.println("Found java file: " + file);
}
}
}
For cleaning up .class
files, we can create another visitor:
java
public class ClassFileCleanerVisitor implements Visitor {
public void visitDir(File dir) {
// Do nothing for directories.
}
public void visitFile(File file) {
if (file.getName().endsWith(".class")) {
System.out.println("Will clean class file: " + file);
}
}
}
Usage
To use the visitors, we can do the following:
java
FileStructure fs = new FileStructure(new File("."));
fs.handle(new JavaFileVisitor());
This prints out the directories and .java
files found in the current directory.
Similarly, to clean up .class
files, we would do:
java
fs.handle(new ClassFileCleanerVisitor());
Summary of the Visitor Pattern
The core idea of the Visitor pattern is to abstract the operations on a complex data structure without altering the structure itself. Operations are handled through the visitor, allowing for new operations to be added simply by creating a new visitor class.
Java's standard library already provides an implementation of the Visitor pattern with Files.walkFileTree()
:
java
import java.io.*;
import java.nio.file.*;
import java.nio.file.attribute.*;
public class Main {
public static void main(String[] args) throws IOException {
Files.walkFileTree(Paths.get("."), new MyFileVisitor());
}
}
// Implementing a FileVisitor:
class MyFileVisitor extends SimpleFileVisitor<Path> {
// Handle directory:
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
System.out.println("pre visit dir: " + dir);
// Returning CONTINUE indicates to continue visiting:
return FileVisitResult.CONTINUE;
}
// Handle file:
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
System.out.println("visit file: " + file);
// Returning CONTINUE indicates to continue visiting:
return FileVisitResult.CONTINUE;
}
}
The walkFileTree()
method allows visitors to return FileVisitResult.CONTINUE
to continue or FileVisitResult.TERMINATE
to stop visiting.
Similarly, SAX processing of XML is also a form of the Visitor pattern, where a SAX handler acts as the visitor to handle various nodes of the XML structure.
Practice
Try implementing the Visitor pattern to recursively traverse a folder structure.
Conclusion
The Visitor pattern abstracts operations that apply to a set of complex objects and allows for the addition of new operations without modifying the existing object structure.