The Pain of Scattered Business Logic

Picture this: You're building a clinic management system. You have three appointment types: general consultations, lab tests, and surgeries. Each is modelled as a clean class. Your first feature request is straightforward: calculate the billing fee for each appointment. You add a calculateFee() method to each class. Done.

Then the insurance team needs claim codes per appointment type. You add generateClaimCode() to each class. Then admin wants appointment summaries for records. You add generateSummary(). The compliance team needs discharge notes. Another method on every class.

Six months in, your clean appointment classes look like this:

public class Surgery {
    private String patientName;
    private String procedureName;
    private int durationMinutes;

    public double calculateFee() { /* billing logic */ }
    public String generateClaimCode() { /* insurance code */ }
    public String generateSummary() { /* appointment summary */ }
    public String generateDischargeNote() { /* discharge logic */ }
    // ...more unrelated methods keep piling up
}

Every new operation means modifying every appointment class. Your classes have stopped being models of clinical appointments. They've become dumping grounds for every feature the business ever asked for. Adding a new operation is risky: you touch every class, risk introducing bugs, and trigger a full regression cycle.

The Visitor pattern solves this by separating operations from the object structure they work on. New operations become new visitor classes, and you never touch the appointment classes again.

Understanding the Visitor Pattern

The Visitor pattern lets you define a new operation without changing the classes of the elements it operates on. You create a visitor object that encapsulates the operation, and each element in the structure "accepts" the visitor and delegates the operation back to it.

Think of it like a clinic administrator processing different appointment types: the administrator (visitor) knows how to handle a general consultation, a lab test, and a surgery differently. Each appointment doesn't need to know how it gets processed; it just hands itself to the administrator. When the insurance team sends their own claims processor, the appointments don't change, only the processor does.

This pattern promotes Open/Closed Principle, Single Responsibility, and Separation of Concerns by keeping operations in dedicated visitor classes rather than scattered across the element hierarchy.

Visitor Pattern Components

Core Components

  • Element Interface: Declares an accept(visitor) method that each concrete element must implement

  • Visitor Interface: Declares a visit() method overload for each concrete element type

  • Concrete Elements: The object structure (GeneralConsultation, LabTest, Surgery). Each calls visitor.visit(this) in its accept method

  • Concrete Visitors: Implement the operation for every element type (BillingVisitor, InsuranceClaimVisitor)

Complete Java Implementation

Let's build a clinic management system that demonstrates the Visitor pattern's power in adding new operations without touching appointment classes.

Element Interface

public interface Appointment {
    void accept(AppointmentVisitor visitor);
    String getPatientName();
}

Visitor Interface

public interface AppointmentVisitor {
    void visit(GeneralConsultation consultation);
    void visit(LabTest labTest);
    void visit(Surgery surgery);
}

Concrete Elements

public class GeneralConsultation implements Appointment {
    private String patientName;
    private String doctorName;

    public GeneralConsultation(String patientName, String doctorName) {
        this.patientName = patientName;
        this.doctorName = doctorName;
    }

    public String getDoctorName() { return doctorName; }

    @Override public String getPatientName() { return patientName; }
    @Override public void accept(AppointmentVisitor visitor) { visitor.visit(this); }
}

public class LabTest implements Appointment {
    private String patientName;
    private String testType;

    public LabTest(String patientName, String testType) {
        this.patientName = patientName;
        this.testType = testType;
    }

    public String getTestType() { return testType; }

    @Override public String getPatientName() { return patientName; }
    @Override public void accept(AppointmentVisitor visitor) { visitor.visit(this); }
}

public class Surgery implements Appointment {
    private String patientName;
    private String procedureName;

    public Surgery(String patientName, String procedureName) {
        this.patientName = patientName;
        this.procedureName = procedureName;
    }

    public String getProcedureName() { return procedureName; }

    @Override public String getPatientName() { return patientName; }
    @Override public void accept(AppointmentVisitor visitor) { visitor.visit(this); }
}

Concrete Visitors

public class BillingVisitor implements AppointmentVisitor {
    private double totalBill;

    @Override
    public void visit(GeneralConsultation consultation) {
        double fee = 80.00;
        totalBill += fee;
        System.out.println(consultation.getPatientName() + " [Consultation] $" + fee + " (Dr. " + consultation.getDoctorName() + ")");
    }

    @Override
    public void visit(LabTest labTest) {
        double fee = 45.00;
        totalBill += fee;
        System.out.println(labTest.getPatientName() + " [Lab Test] $" + fee + " (" + labTest.getTestType() + ")");
    }

    @Override
    public void visit(Surgery surgery) {
        double fee = 500.00;
        totalBill += fee;
        System.out.println(surgery.getPatientName() + " [Surgery] $" + fee + " (" + surgery.getProcedureName() + ")");
    }

    public double getTotalBill() { return totalBill; }
}
public class InsuranceClaimVisitor implements AppointmentVisitor {

    @Override
    public void visit(GeneralConsultation consultation) {
        System.out.println(consultation.getPatientName() + " Code: GP-001 Type: General Practice");
    }

    @Override
    public void visit(LabTest labTest) {
        System.out.println(labTest.getPatientName() + " Code: LT-042 Type: Diagnostic Lab");
    }

    @Override
    public void visit(Surgery surgery) {
        System.out.println(surgery.getPatientName() + " Code: SG-301 Type: Surgical Procedure");
    }
}

Client

public class ClinicBillingDemo {
    public static void main(String[] args) {
        System.out.println("=== Clinic Management System ===\n");

        List<Appointment> appointments = Arrays.asList(
            new GeneralConsultation("Alice Johnson", "Smith"),
            new LabTest("Bob Williams", "Blood Panel"),
            new Surgery("Carol Davis", "Appendectomy"),
            new GeneralConsultation("David Brown", "Patel")
        );

        System.out.println("--- Insurance Claims ---");
        InsuranceClaimVisitor claims = new InsuranceClaimVisitor();
        for (Appointment appointment : appointments) {
            appointment.accept(claims);
        }

        System.out.println("\n--- Patient Bills ---");
        BillingVisitor billing = new BillingVisitor();
        for (Appointment appointment : appointments) {
            appointment.accept(billing);
        }
        System.out.println("Total: $" + billing.getTotalBill());
    }
}

Expected Output:

=== Clinic Management System ===

--- Insurance Claims ---
Alice Johnson Code: GP-001 Type: General Practice
Bob Williams Code: LT-042 Type: Diagnostic Lab
Carol Davis Code: SG-301 Type: Surgical Procedure
David Brown Code: GP-001 Type: General Practice

--- Patient Bills ---
Alice Johnson [Consultation] $80.0 (Dr. Smith)
Bob Williams [Lab Test] $45.0 (Blood Panel)
Carol Davis [Surgery] $500.0 (Appendectomy)
David Brown [Consultation] $80.0 (Dr. Patel)
Total: $705.0

🚀 Get the Complete Implementation

The full code with additional visitors (risk analysis, compliance audit) is available in our Design Patterns Repository.

# Clone and run the complete demo
git clone https://github.com/shift-elevate/design-patterns.git
cd design-patterns
mvn test -Dtest=VisitorPatternTest

Real World Examples

The Visitor pattern is extensively used in real-world systems where stable structures need evolving operations:

1. Compiler Abstract Syntax Trees

Compilers build an AST (Abstract Syntax Tree) representing the parsed source code. Different compiler phases (type checking, optimisation, code generation, linting) are implemented as separate visitors that traverse the same tree. The AST node classes remain untouched while each new compiler phase is a new visitor, making compiler toolchains highly extensible.

2. Document Export

Applications that model documents as a tree of elements (paragraphs, headings, tables, images) use the Visitor pattern for export operations. Each export format (PDF, HTML, plain text) is implemented as a separate visitor that knows how to render every element type, without any element class needing to know about export formats.

3. Static Code Analysis

Static analysis tools analyse source code by visiting AST nodes. Each analysis rule (detect null dereference, find unused variables, enforce naming conventions) is an independent visitor. Adding a new rule means adding a new visitor, and the AST structure and existing rules are never modified.

When to Use the Visitor Pattern

Understanding when to apply the Visitor pattern is crucial for making the right architectural decisions. Here's when it shines and when alternatives might be better:

Ideal Scenarios:

  • You have a stable object structure but need to frequently add new operations.

  • You want to keep unrelated operations out of the element classes.

  • A class hierarchy contains many distinct types and you need type-specific behaviour across multiple operations.

  • You need to accumulate state across elements during traversal (like totalling tax across all assets).

Skip It When:

  • The element hierarchy changes frequently, as adding a new element type requires updating every visitor.

  • The object structure is simple and has only a handful of operations that rarely change.

  • Encapsulation of element internals is critical, as visitors often need access to internal fields.

  • The double-dispatch mechanism adds more complexity than the problem warrants.

Next Steps: Apply the Visitor Pattern in Your Project

Ready to implement the Visitor pattern in your own projects? Here's a structured approach to get you started:

  1. Identify Stable Structures: Find class hierarchies that rarely gain new types but frequently gain new operations.

  2. Define the Visitor Interface: Declare one method per concrete element type so each visitor knows how to handle every element.

  3. Add Accept to Elements: Each element implements a method that hands itself to the visitor, keeping all operation logic out of the element.

  4. Create Concrete Visitors: Implement each operation as a dedicated visitor class with its own state.

  5. Drive from the Client: Iterate the element collection and let each element accept the visitor in turn.

The Visitor pattern transforms bloated element classes into clean, focused models by relocating operations to dedicated visitor classes that can evolve independently. Your element classes model your domain; your visitor classes model what you do with it.

Found this helpful? Share it with a colleague whose domain classes are drowning in unrelated operations. Got questions? We'd love to hear from you at [email protected]

Reply

Avatar

or to participate

Keep Reading