Visitor Pattern – Add Behavior Without Changing Classes

Learn how the Visitor Pattern helps you add new operations to existing object structures without modifying their classes. Includes real-world examples, UML, and C#/Python code.

The Visitor Pattern lets you define new operations on objects without changing their classes. It's perfect for when you have an object structure and want to perform varying operations across its elements.

🔧 Problem It Solves

Imagine you have a set of different elements (like shapes, document parts, or nodes in a tree) and you want to perform various operations on them—such as rendering, exporting, validation, or analytics. If you add these operations directly to the element classes, the code becomes cluttered and violates the Single Responsibility Principle.

The Visitor Pattern solves this by letting you define new operations in separate visitor classes, keeping element classes clean and focused. Each visitor can implement a different operation, and you can add new visitors without modifying the element classes. This is especially useful when:

  • You need to perform unrelated operations across a set of objects with different types.
  • The object structure is stable, but operations on them change or grow frequently.
  • You want to follow the Open/Closed Principle—open for extension, closed for modification.

For example, in a graphics editor, you might have shapes like Circle and Rectangle. Instead of adding export, print, and area calculation methods to each shape, you create visitors for each operation. This way, adding a new operation is as simple as creating a new visitor.

🧱 Structure & Participants

  • Visitor: Declares visit methods for each type of element.
  • ConcreteVisitor: Implements the operations for each type.
  • Element: Accepts a visitor object.
  • ConcreteElement: Implements accept method.
  • Client: Runs the operation through visitors.

🧭 UML Diagram (Mermaid)


classDiagram
    class Visitor {
        +visitElementA(a: ElementA)
        +visitElementB(b: ElementB)
    }
    class ConcreteVisitor
    class Element {
        +accept(v: Visitor)
    }
    class ElementA
    class ElementB

    Visitor <|-- ConcreteVisitor
    Element <|-- ElementA
    Element <|-- ElementB
    ElementA --> Visitor : accept(visitor)
    ElementB --> Visitor : accept(visitor)

💻 Code Examples

// C# Example
  interface IIncomeSource {
    void Accept(IIncomeVisitor visitor);
}

class SalaryIncome : IIncomeSource {
    public double GrossIncome { get; set; } = 80000;
    public void Accept(IIncomeVisitor visitor) => visitor.Visit(this);
}

class RentalIncome : IIncomeSource {
    public double MonthlyRent { get; set; } = 2500;
    public int MonthsRented { get; set; } = 10;
    public void Accept(IIncomeVisitor visitor) => visitor.Visit(this);
}

class CapitalGains : IIncomeSource {
    public double GainAmount { get; set; } = 15000;
    public bool LongTerm { get; set; } = true;
    public void Accept(IIncomeVisitor visitor) => visitor.Visit(this);
}
interface IIncomeVisitor {
    void Visit(SalaryIncome salary);
    void Visit(RentalIncome rental);
    void Visit(CapitalGains gains);
}

class TaxCalculatorVisitor : IIncomeVisitor {
    public void Visit(SalaryIncome salary) =>
        Console.WriteLine($"Salary Tax: ${salary.GrossIncome * 0.20}");

    public void Visit(RentalIncome rental) {
        double income = rental.MonthlyRent * rental.MonthsRented;
        Console.WriteLine($"Rental Tax: ${income * 0.15}");
    }

    public void Visit(CapitalGains gains) {
        double rate = gains.LongTerm ? 0.10 : 0.25;
        Console.WriteLine($"Capital Gains Tax: ${gains.GainAmount * rate}");
    }
}

class AuditVisitor : IIncomeVisitor {
    public void Visit(SalaryIncome salary) =>
        Console.WriteLine(salary.GrossIncome > 100000
            ? "High-income salary: audit triggered"
            : "Salary within normal range");

    public void Visit(RentalIncome rental) =>
        Console.WriteLine(rental.MonthsRented < 6
            ? "Low rental activity: audit triggered"
            : "Rental records are stable");

    public void Visit(CapitalGains gains) =>
        Console.WriteLine(gains.GainAmount > 50000
            ? "Large capital gain: audit triggered"
            : "Capital gains clear");
}
class Program {
    static void Main() {
        var incomes = new List {
            new SalaryIncome(),
            new RentalIncome(),
            new CapitalGains()
        };

        IIncomeVisitor taxVisitor = new TaxCalculatorVisitor();
        IIncomeVisitor auditVisitor = new AuditVisitor();

        Console.WriteLine("--- Tax Calculations ---");
        incomes.ForEach(i => i.Accept(taxVisitor));

        Console.WriteLine("\n--- Audit Report ---");
        incomes.ForEach(i => i.Accept(auditVisitor));
    }
}
//Output
///--- Tax Calculations ---
//Salary Tax: $16000
//Rental Tax: $3750
//Capital Gains Tax: $1500

//--- Audit Report ---
//Salary within normal range
//Rental records are stable
//Capital gains clear
///
  
# Python Example
  class IncomeSource:
    def accept(self, visitor):
        raise NotImplementedError()

class SalaryIncome(IncomeSource):
    def __init__(self, gross_income=80000):
        self.gross_income = gross_income

    def accept(self, visitor):
        visitor.visit_salary(self)

class RentalIncome(IncomeSource):
    def __init__(self, monthly_rent=2500, months_rented=10):
        self.monthly_rent = monthly_rent
        self.months_rented = months_rented

    def accept(self, visitor):
        visitor.visit_rental(self)

class CapitalGains(IncomeSource):
    def __init__(self, gain_amount=15000, long_term=True):
        self.gain_amount = gain_amount
        self.long_term = long_term

    def accept(self, visitor):
        visitor.visit_gains(self)
class IncomeVisitor:
    def visit_salary(self, salary): pass
    def visit_rental(self, rental): pass
    def visit_gains(self, gains): pass

class TaxCalculatorVisitor(IncomeVisitor):
    def visit_salary(self, salary):
        print(f"Salary Tax: ${salary.gross_income * 0.20:.2f}")

    def visit_rental(self, rental):
        income = rental.monthly_rent * rental.months_rented
        print(f"Rental Tax: ${income * 0.15:.2f}")

    def visit_gains(self, gains):
        rate = 0.10 if gains.long_term else 0.25
        print(f"Capital Gains Tax: ${gains.gain_amount * rate:.2f}")

class AuditVisitor(IncomeVisitor):
    def visit_salary(self, salary):
        print("High-income salary: audit triggered" if salary.gross_income > 100000 else "Salary within normal range")

    def visit_rental(self, rental):
        print("Low rental activity: audit triggered" if rental.months_rented < 6 else "Rental records are stable")

    def visit_gains(self, gains):
        print("Large capital gain: audit triggered" if gains.gain_amount > 50000 else "Capital gains clear")
if __name__ == "__main__":
    incomes = [
        SalaryIncome(),
        RentalIncome(),
        CapitalGains()
    ]

    tax_visitor = TaxCalculatorVisitor()
    audit_visitor = AuditVisitor()

    print("--- Tax Calculations ---")
    for income in incomes:
        income.accept(tax_visitor)

    print("\n--- Audit Report ---")
    for income in incomes:
        income.accept(audit_visitor)


# Output
#--- Tax Calculations ---
#Salary Tax: $16000
#Rental Tax: $3750
#Capital Gains Tax: $1500

#--- Audit Report ---
#Salary within normal range
#Rental records are stable
#Capital gains clear
#
  

📚 Understnad the example

classDiagram class IIncomeSource { +accept(visitor: IIncomeVisitor) } class SalaryIncome { +gross_income +accept(visitor: IIncomeVisitor) } class RentalIncome { +monthly_rent +months_rented +accept(visitor: IIncomeVisitor) } class CapitalGains { +gain_amount +long_term +accept(visitor: IIncomeVisitor) } IIncomeSource <|-- SalaryIncome IIncomeSource <|-- RentalIncome IIncomeSource <|-- CapitalGains class IIncomeVisitor { +visit_salary(salary: SalaryIncome) +visit_rental(rental: RentalIncome) +visit_gains(gains: CapitalGains) } class TaxCalculatorVisitor { +visit_salary(SalaryIncome) +visit_rental(RentalIncome) +visit_gains(CapitalGains) } class AuditVisitor { +visit_salary(SalaryIncome) +visit_rental(RentalIncome) +visit_gains(CapitalGains) } IIncomeVisitor <|-- TaxCalculatorVisitor IIncomeVisitor <|-- AuditVisitor SalaryIncome --> IIncomeVisitor : Accepts RentalIncome --> IIncomeVisitor : Accepts CapitalGains --> IIncomeVisitor : Accepts

🧠 What This Shows

  • IIncomeSource is the interface for all income types.
  • SalaryIncome, RentalIncome, and CapitalGains implement the interface and define accept.
  • Visitors like TaxCalculatorVisitor and AuditVisitor implement IIncomeVisitor and contain logic for each income class.
  • Each income instance delegates the processing to the visitor using its accept method.

Want to add a new FraudDetectionVisitor or extend this to support more income types like FreelanceIncome or InvestmentDividends? I can show how the diagram evolves too!

🌐 Real-World Use Cases

  • Compilers: Walking syntax trees for code generation or optimization
  • Exporters: Convert document structures to PDF, CSV, HTML
  • UI Toolkits: Apply operations like rendering or event binding

✅ Pros and ❌ Cons

Pros Cons
Easy to add new operations
Example: Add a PrintVisitor to print all elements without changing ElementA or ElementB.
Hard to add new element types
Example: Adding ElementC means updating every visitor with a new visit_element_c method.
Separation of concerns
Example: Business logic stays in visitors, keeping element classes focused and simple.
Requires double dispatch setup
Example: Each element must implement accept(visitor) and call the correct visitor method.

🧠 Quiz

🔚 Conclusion

The Visitor Pattern is a powerful behavioral pattern for extending behavior without altering existing class structures. Use it when operations are frequently added, but the object structure is stable.