08 Jan 2025

Visitor Pattern

Overview

The Visitor Pattern is a behavioral design pattern that allows you to add new operations to a group of objects (elements) without modifying their classes. This is achieved by separating the operation from the objects it operates on, encapsulating it in a separate visitor object.

The pattern promotes open/closed principle: objects remain open for extension through visitors while being closed for modification.

Key Participants

  1. Visitor

  • Defines an interface for visiting each type of element in the structure.

  • Declares methods for each concrete element type (e.g., visitConcreteElementA, visitConcreteElementB).

  1. ConcreteVisitor

  • Implements the operations for each concrete element.

  • Encapsulates specific behavior that is applied to the elements.

  1. Element

  • Defines an interface for accepting a visitor.

  • Typically has an accept(Visitor visitor) method.

  1. ConcreteElement

  • Implements the Element interface and defines the accept method.

  • Calls the appropriate method on the visitor, passing itself as an argument.

  1. ObjectStructure

  • Holds a collection of elements and provides a way to iterate through them for visiting.

Implementation in Code

Example: File System with Visitors for Size Calculation and File Type Display

// Visitor Interface
interface Visitor {
    void visitFile(File file);
    void visitFolder(Folder folder);
}

// Concrete Visitor: Calculate Total Size
class SizeCalculator implements Visitor {
    private int totalSize = 0;

    @Override
    public void visitFile(File file) {
        totalSize += file.getSize();
    }

    @Override
    public void visitFolder(Folder folder) {
        // No size directly for folders in this example
    }

    public int getTotalSize() {
        return totalSize;
    }
}

// Concrete Visitor: Display File Type
class FileTypeDisplayer implements Visitor {
    @Override
    public void visitFile(File file) {
        System.out.println("File: " + file.getName() + " [" + file.getType() + "]");
    }

    @Override
    public void visitFolder(Folder folder) {
        System.out.println("Folder: " + folder.getName());
    }
}

// Element Interface
interface Element {
    void accept(Visitor visitor);
}

// Concrete Element: File
class File implements Element {
    private String name;
    private String type;
    private int size;

    public File(String name, String type, int size) {
        this.name = name;
        this.type = type;
        this.size = size;
    }

    public String getName() {
        return name;
    }

    public String getType() {
        return type;
    }

    public int getSize() {
        return size;
    }

    @Override
    public void accept(Visitor visitor) {
        visitor.visitFile(this);
    }
}

// Concrete Element: Folder
class Folder implements Element {
    private String name;
    private List<Element> elements = new ArrayList<>();

    public Folder(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void addElement(Element element) {
        elements.add(element);
    }

    @Override
    public void accept(Visitor visitor) {
        visitor.visitFolder(this);
        for (Element element : elements) {
            element.accept(visitor);
        }
    }
}

// Client
public class VisitorPatternDemo {
    public static void main(String[] args) {
        // Create elements
        File file1 = new File("Document", "txt", 120);
        File file2 = new File("Presentation", "ppt", 300);
        Folder folder = new Folder("Work");
        folder.addElement(file1);
        folder.addElement(file2);

        // Visitor for size calculation
        SizeCalculator sizeCalculator = new SizeCalculator();
        folder.accept(sizeCalculator);
        System.out.println("Total Size: " + sizeCalculator.getTotalSize() + " KB");

        // Visitor for file type display
        FileTypeDisplayer fileTypeDisplayer = new FileTypeDisplayer();
        folder.accept(fileTypeDisplayer);
    }
}

Key Methods

  1. accept(Visitor visitor)

  • Allows the visitor to perform an operation on the element.

  • Calls the visitor’s corresponding visit method and passes itself as an argument.

  1. visit(Element)

  • Encapsulates the operation performed on an element.

Advantages

  1. Open/Closed Principle: New operations can be added without modifying the existing element classes.

  1. Separation of Concerns: Different behaviors are encapsulated in separate visitors, simplifying element classes.

  1. Flexibility: A visitor can operate on a group of related objects without changing their structure.

Disadvantages

  1. Tight Coupling: Adding new element types requires modifying all visitors.

  1. Complexity: The pattern introduces additional classes for visitors and can make the code harder to follow.

  1. Not Always Applicable: It works best when the element hierarchy is stable.

Real-World Examples

  1. Compilers: Visitors can traverse abstract syntax trees (AST) to perform operations like type checking or code generation.

  1. Document Processing: A visitor can apply different operations, such as exporting, printing, or spell-checking, to parts of a document.

  1. UI Frameworks: A visitor can traverse and operate on UI components like buttons, text boxes, or labels.

When to Use the Visitor Pattern?

  • When you want to perform multiple operations on objects without modifying their classes.

  • When the object structure is stable, but new operations are frequently added.

  • When you want to separate algorithms from the objects on which they operate.

← Back to Library