TechWayFit
● Design Patterns in Practice

Visitor Pattern – Add Behavior Without Changing Classes

Tech Buddy July 6, 2025 7 min read
Visitor Pattern – Add Behavior Without Changing Classes

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.

Comments

Loading comments…

Leave a comment