In mature applications, domain models tend to be stable, but business requirements constantly evolve. New needs arise: reports, validations, exports, calculations, or additional logs.
Visitor Pattern elegantly solves this problem by allowing adding new operations over a structure of objects without modifying existing classes.
It is a pattern from the Behavioral category and is extremely useful when:
-
you have a complex object structure (e.g., AST, documents, UI elements, domain models),
-
you want to avoid modifying existing classes,
-
the operations applied over objects change more often than their structure.
Problem
Let's assume we have an application that works with different types of documents:
-
Invoice
-
Contract
Initially, we want to:
-
display information
-
calculate values
-
export data
If we add methods directly into each class (Print(), Export(), Validate()), we quickly end up with:
-
overloaded classes
-
violation of Open/Closed Principle
-
hard-to-maintain code
Solution offered by Visitor Pattern
Visitor Pattern separates:
-
the object structure (the models)
-
the operations applied on them
👉 Objects accept a visitor
👉 The visitor knows what to do for each object type
Pattern structure
Main components:
-
Element – common interface for visited objects
-
ConcreteElement – actual implementations
-
Visitor – interface for operations
-
ConcreteVisitor – concrete implementations of operations
-
Object Structure – collection of elements
Concrete example in C#
1️⃣ Interface IVisitable
public interface IVisitable
{
void Accept(IVisitor visitor);
}
2️⃣ Interface IVisitor
public interface IVisitor
{
void VisitInvoice(Invoice invoice);
void VisitContract(Contract contract);
}
3️⃣ Concrete elements
public class Invoice : IVisitable
{
public string Number { get; set; }
public decimal Total { get; set; }
public void Accept(IVisitor visitor)
{
visitor.VisitInvoice(this);
}
}
public class Contract : IVisitable
{
public string Client { get; set; }
public DateTime StartDate { get; set; }
public void Accept(IVisitor visitor)
{
visitor.VisitContract(this);
}
}
4️⃣ Concrete visitor – data display
public class PrintVisitor : IVisitor
{
public void VisitInvoice(Invoice invoice)
{
Console.WriteLine($"Invoice {invoice.Number}, Total: {invoice.Total} RON");
}
public void VisitContract(Contract contract)
{
Console.WriteLine($"Contract with {contract.Client}, Start: {contract.StartDate:d}");
}
}
5️⃣ Concrete visitor – data export
public class ExportVisitor : IVisitor
{
public void VisitInvoice(Invoice invoice)
{
Console.WriteLine($"Exporting invoice {invoice.Number}...");
}
public void VisitContract(Contract contract)
{
Console.WriteLine($"Exporting contract for {contract.Client}...");
}
}
6️⃣ Usage
var documents = new List<IVisitable>
{
new Invoice { Number = "INV-001", Total = 1200 },
new Contract { Client = "ACME SRL", StartDate = DateTime.Today }
};
var printVisitor = new PrintVisitor();
var exportVisitor = new ExportVisitor();
foreach (var doc in documents)
{
doc.Accept(printVisitor);
doc.Accept(exportVisitor);
}
Advantages
✅ Respects Open/Closed Principle
✅ Operations are centralized and easy to extend
✅ Cleaner code for models
✅ Ideal for reports, validations, exports, audit
Disadvantages
⚠️ Difficult to extend the structure (adding a new element requires modifying all visitors)
⚠️ More complex pattern, hard to understand at first
⚠️ Not ideal for simple structures
When to use Visitor Pattern
✔ Stable object structure
✔ Operations that change frequently
✔ Need for clear separation between data and logic
✔ Working with complex hierarchies (e.g., interpreters, document processing, DDD)
Conclusion
Visitor Pattern is a powerful tool when you want to extend application behavior without "touching" existing models.
Used correctly, it can transform rigid code into extremely flexible code — exactly the kind of pattern that shows its value in large and long-term projects.