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
, andCapitalGains
implement the interface and defineaccept
.- Visitors like
TaxCalculatorVisitor
andAuditVisitor
implementIIncomeVisitor
and contain logic for each income class. - Each income instance
delegates
the processing to the visitor using itsaccept
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.