Level: Intermediate
Table of Contents
- 2.1 The Visitor Pattern explained
- 2.2 Creation of the Visitor blueprint
- 2.3 Implementation of a Concrete Visitor
- 2.4 Visitor Pattern Test
- 2.5 Adding an Object Structure
- 2.6 Use cases and applications
- 3.1 Starter project
- 3.2 Final Draft (Final)
1. Course Overview
Module 1 — Course Overview (1m 57s)
Welcome to the C# Design Patterns: Visitor training. This course is a starting point in the world of Visitor Design Pattern in C#. No prior experience with design patterns is required to get started.
Prerequisites
- Knowledge of C# language
- Familiarity with Visual Studio (Mac or Windows)
- If a beginner, the C# Fundamentals course from the Pluralsight library is recommended
Topics covered
- Define a Visitor blueprint (Visitor interface)
- Create Concrete Visitor classes (concrete visitor classes)
- Test the pattern in practice
- Use an Object Structure
- Understand real-world examples and applications of the pattern
Educational objective
At the end of this course, you will be able to identify and analyze practical use cases for the Visitor Design Pattern and integrate it into your own C# projects.
2. Implementation of the Visitor Pattern
Module 2 — Implementing the Visitor Pattern (25m 37s)
This module covers the theory and practical application of the Visitor Pattern, from blueprint definition to advanced use cases.
2.1 The Visitor Pattern explained
Pattern categorization
Design patterns are classified into three categories, as defined by the book Gang of Four (Elements of Reusable Object-Oriented Software):
| Category | Description |
|---|---|
| Creative | Patterns related to object creation |
| Structural | Patterns related to the structure of classes and objects |
| Behavioral | Patterns linked to communication between objects |
The Visitor Pattern belongs to the Behavioral category because it focuses on communication between objects.
Official definition
The Visitor Pattern allows you to represent an operation to be performed on the elements of an object structure. It allows you to define new operations without changing the classes of the elements it operates on.
In other words: The Visitor Pattern allows you to add behaviors to existing class hierarchies without modifying the underlying classes.
Key Features
- Behaviors specific to each class: the behavior can be different for each class when a single visitor is used.
- Miscellaneous hierarchy: The existing class hierarchy can be diverse and not connected by inheritance, as long as each class is marked as visitable.
- Separation of concerns: Operation logic is extracted from element classes and grouped into visitors.
Pattern components (conceptual UML diagram)
The pattern has several essential components:
+------------------+ +--------------------+
| «interface» | | «interface» |
| IVisitor | | IVisitableElement |
+------------------+ +--------------------+
| + VisitBook() | | + Accept(IVisitor) |
| + VisitVinyl() | +--------------------+
| + Print() | △
+------------------+ |
△ +---------+---------+
| | |
+--------+--------+ +------+-------+ +-------+------+
| DiscountVisitor | | Book | | Vinyl |
+-----------------+ +--------------+ +--------------+
| + VisitBook() | | + Accept() | | + Accept() |
| + VisitVinyl() | +--------------+ +--------------+
| + Print() |
| + Reset() |
+-----------------+
+--------+--------+
| SalesVisitor |
+-----------------+
| + VisitBook() |
| + VisitVinyl() |
| + Print() |
+-----------------+
Component details:
- IVisitor (interface): contains a
Visitmethod for each class in the project. The concrete classes implementing this interface provide the desired behavior. - IVisitableElement (interface): contains a single
Acceptmethod, which takes a visitor as a parameter. - Concrete Elements (Book, Vinyl): implement
IVisitableElement. TheAcceptmethod delegates the call to the visitor by passingthisas an argument. - Concrete Visitors (DiscountVisitor, SalesVisitor): implement
IVisitorwith logic specific to each item type. - Object Structure (ObjectStructure): manages the collection of visitable elements and orchestrates the application of visitors.
Pattern execution flow
Client
│
│ ApplyVisitor(discountVisitor)
▼
ObjectStructure
│
│ foreach item → item.Accept(visitor)
▼
Book.Accept(visitor) Vinyl.Accept(visitor)
│ │
│ visitor.VisitBook(this) │ visitor.VisitVinyl(this)
▼ ▼
DiscountVisitor.VisitBook() DiscountVisitor.VisitVinyl()
2.2 Creation of the Visitor blueprint
Presentation of the initial project
The starter project (M02_Starter) contains a simple class structure for a Barnes & Noble-style retail store:
- Item.cs: base class with properties
ID,Price, and methodGetDiscount - Book and Vinyl: subclasses inheriting from
Item - Program.cs: entry point with an array of predefined items
The goal is to use the Visitor Pattern to calculate item discounts and number of items sold without adding logic to existing classes.
Creation of fundamental interfaces
The first step is to create a new Visitor.cs file containing the two fundamental interfaces of the pattern:
IVisitor — the interface that all concrete visitors must implement:
public interface IVisitor
{
void VisitBook(Book book);
void VisitVinyl(Vinyl vinyl);
void Print();
}
IVisitableElement — the interface that all visitable elements must implement:
public interface IVisitableElement
{
void Accept(IVisitor visitor);
}
Important note: These two interfaces can also be abstract classes if you are more comfortable with that approach. The course uses interfaces for clarity and flexibility.
Make classes visitable
The Book and Vinyl classes must implement IVisitableElement by adding the Accept method. It is this method which establishes the link between the element and the visitor:
public class Book : Item, IVisitableElement
{
public Book(int id, double price) : base(id, price) { }
public void Accept(IVisitor visitor)
{
visitor.VisitBook(this); // délègue au visitor en passant "this"
}
}
public class Vinyl : Item, IVisitableElement
{
public Vinyl(int id, double price) : base(id, price) { }
public void Accept(IVisitor visitor)
{
visitor.VisitVinyl(this); // délègue au visitor en passant "this"
}
}
Key concept — Double Dispatch: The Visitor Pattern exploits the principle of double dispatch. When
item.Accept(visitor)is called, the actual type ofitemdetermines whichAcceptmethod is executed, which in turn calls the correct method on the visitor. It is this mechanism that allows dynamic dispatching to the correct logic.
2.3 Implementing a Concrete Visitor
The DiscountVisitor — Discount calculation
The first functionality to add is the calculation and display of discounts depending on the item type. The discount logic is:
- Books: 10% discount if the price is less than $20, otherwise full price
- Vinyl: 15% discount systematically
public class DiscountVisitor : IVisitor
{
private double _savings;
public void VisitBook(Book book)
{
var discount = 0.0;
if (book.Price < 20.00)
{
discount = book.GetDiscount(0.10);
Console.WriteLine($"DISCOUNTED: Book #{book.ID} is now ${Math.Round(book.Price - discount, 2)}");
}
else
{
Console.WriteLine($"FULL PRICE: Book #{book.ID} is ${book.Price}");
}
_savings += discount;
}
public void VisitVinyl(Vinyl vinyl)
{
var discount = vinyl.GetDiscount(0.15);
Console.WriteLine($"SUPER SAVINGS: Vinyl #{vinyl.ID} is now ${Math.Round(vinyl.Price - discount, 2)}");
_savings += discount;
}
public void Print()
{
Console.WriteLine($"\nYou saved a total of ${Math.Round(_savings, 2)} on today's order!");
}
public void Reset()
{
_savings = 0.0;
}
}
Important points about this visitor:
- The private field
_savingsaccumulates state across all visits — this is an important feature of concrete visitors. - The
Reset()method allows you to reset the state of the visitor for new use. - Each
Visitmethod receives the typed concrete object (not an interface), which gives access to all specific properties. - Using
Math.Roundon 2 decimal places avoids uncontrolled decimal numbers in the display.
The SalesVisitor — Sales counting
Once the structure is in place, adding a second visitor is very simple. The SalesVisitor counts the number of items sold by type:
public class SalesVisitor : IVisitor
{
private int BookCount = 0;
private int VinylCount = 0;
public void VisitBook(Book book)
{
BookCount++;
}
public void VisitVinyl(Vinyl vinyl)
{
VinylCount++;
}
public void Print()
{
Console.WriteLine($"Books sold: {BookCount} \nVinyl sold: {VinylCount}");
Console.WriteLine($"The store sold {BookCount + VinylCount} units today!");
}
}
Key observation: The ease of adding the
SalesVisitorperfectly illustrates one of the great advantages of the Visitor Pattern — we can add new functionalities without touching existing classes (Book,Vinyl,Item).
2.4 Testing the Visitor Pattern
Initial test program (without ObjectStructure)
Firstly, we can test directly from Program.cs by manually iterating over the list of items:
List<IVisitableElement> items = new List<IVisitableElement>
{
new Book(12345, 11.99),
new Book(78910, 22.79),
new Vinyl(11198, 17.99),
new Book(63254, 9.79)
};
var discountVisitor = new DiscountVisitor();
foreach (var item in items)
{
item.Accept(discountVisitor);
}
discountVisitor.Print();
Expected console result:
DISCOUNTED: Book #12345 is now $10.79
FULL PRICE: Book #78910 is $22.79
SUPER SAVINGS: Vinyl #11198 is now $15.29
DISCOUNTED: Book #63254 is now $8.81
You saved a total of $3.88 on today's order!
Analysis of results:
Book #12345(11.99$) → price < 20$ → discount 10% → final price 10.79$Book #78910(22.79$) → price ≥ 20$ → full price 22.79$Vinyl #11198(17.99$) → systematic 15% discount → final price 15.29$Book #63254(9.79$) → price < 20$ → discount 10% → final price 8.81$
Note: Notice the important change in the list declaration: we go from
List<Object>(in the Starter) toList<IVisitableElement>(in the Final). This is necessary to be able to callitem.Accept(visitor)polymorphically.
2.5 Adding an Object Structure
Role and usefulness of the Object Structure
The Object Structure is the main control panel of the Visitor Pattern. It allows you to:
- Store an iterable collection of visitable items
- Edit this collection (adding, removing items)
- Iterate over elements and call
Acceptcentrally
Although not always present in every implementation of the pattern, its use is strongly recommended as an additional level of abstraction.
Implementing the ObjectStructure
public class ObjectStructure
{
private List<IVisitableElement> _cart;
public ObjectStructure(List<IVisitableElement> items)
{
_cart = items;
}
public void RemoveItem(IVisitableElement item)
{
_cart.Remove(item);
}
public void ApplyVisitor(IVisitor visitor)
{
Console.WriteLine("\n------- Visitor Breakdown -------");
foreach (var item in _cart)
item.Accept(visitor);
visitor.Print();
}
}
Details of responsibilities:
| Method | Responsibility |
|---|---|
ObjectStructure(List<IVisitableElement>) | Constructor: initializes the internal list with the elements provided |
RemoveItem(IVisitableElement) | Wraps the removal of an item from the private list |
ApplyVisitor(IVisitor) | Iterates over all elements and calls Accept on each, then calls Print |
Why encapsulate the list?
The _cart list is private to prevent accidental modifications from the outside. The RemoveItem (and potentially AddItem) methods expose controlled access to this list.
Complete program with ObjectStructure
class Program
{
static void Main(string[] args)
{
List<IVisitableElement> items = new List<IVisitableElement>
{
new Book(12345, 11.99),
new Book(78910, 22.79),
new Vinyl(11198, 17.99),
new Book(63254, 9.79)
};
var cart = new ObjectStructure(items);
var discountVisitor = new DiscountVisitor();
var salesVisitor = new SalesVisitor();
// Premier passage : calcul des rabais
cart.ApplyVisitor(discountVisitor);
// Second passage : comptage des ventes
cart.ApplyVisitor(salesVisitor);
// Réinitialisation du visitor et suppression d'un item
discountVisitor.Reset();
cart.RemoveItem(items[2]); // on retire le Vinyl
// Troisième passage : recalcul des rabais sans le Vinyl
cart.ApplyVisitor(discountVisitor);
}
}
Expected console result:
------- Visitor Breakdown -------
DISCOUNTED: Book #12345 is now $10.79
FULL PRICE: Book #78910 is $22.79
SUPER SAVINGS: Vinyl #11198 is now $15.29
DISCOUNTED: Book #63254 is now $8.81
You saved a total of $3.88 on today's order!
------- Visitor Breakdown -------
Books sold: 3
Vinyl sold: 1
The store sold 4 units today!
------- Visitor Breakdown -------
DISCOUNTED: Book #12345 is now $10.79
FULL PRICE: Book #78910 is $22.79
DISCOUNTED: Book #63254 is now $8.81
You saved a total of $1.18 on today's order!
Analysis of the third pass:
- After
discountVisitor.Reset(), the_savingscounter is reset to 0 - After
cart.RemoveItem(items[2]), the Vinyl is removed from the cart - The third
ApplyVisitortherefore only processes the remaining 3 books - Total savings go from $3.88 to $1.18 (without the $2.70 Vinyl rebate)
2.6 Use cases and applications
When to use the Visitor Pattern?
The Visitor Pattern is particularly suitable in the following situations:
-
Class variety: When a project contains a variety of classes, potentially with different interfaces and inheritance structures, that require behavior specific to each class.
-
Unrelated behaviors: when different, unrelated behaviors need to be applied without polluting existing classes.
-
Stable hierarchy: when the existing class structure is unlikely to change, but new behaviors should be able to be added at will.
-
State accumulation: when it is necessary to collect related behaviors and accumulate state across different classes in a complex hierarchy.
Advantages of the pattern
| Advantage | Explanation |
|---|---|
| Extensibility | Add new behaviors without modifying existing classes |
| Separation of Concerns | The operations logic is grouped in visitors |
| Specific behavior per class | Each class can have a different behavior with the same visitor |
| State accumulation | Visitors can maintain a state across multiple visits |
| Compatibility with other patterns | Works well with Composite Pattern and Interpreter Pattern |
Disadvantages and limitations
| Disadvantage | Explanation |
|---|---|
| Cost of modifying the hierarchy | Each change in the class hierarchy requires an update of IVisitor and of all concrete visitors |
| Potential encapsulation break | The pattern often needs access to the internal state of elements to work |
| Coupling with concrete types | The IVisitor interface is coupled to concrete types (one Visit method per type) |
General rule: If your class hierarchy changes often, the Visitor Pattern can cause a lot of maintenance. In this case, other approaches might be more suitable.
Relations with other patterns
- Composite Pattern: the Visitor Pattern can work with a class structure already using the Composite Pattern, making it easier to traverse object trees.
- Interpreter Pattern: The Visitor Pattern can be used to perform interpretation as part of the Interpreter Pattern.
3. Project Structure — Code Files
3.1 Starter project
The starting project (M02_Starter) contains only the base class hierarchy, without any implementation of the Visitor Pattern.
Item.cs — Base classes
using System;
using System.Collections.Generic;
using System.Text;
namespace Visitor_Pattern
{
public class Item
{
public int ID;
public double Price;
public Item(int id, double price)
{
this.ID = id;
this.Price = price;
}
public double GetDiscount(double percentage)
{
return Math.Round(Price * percentage, 2);
}
}
public class Book : Item
{
public Book(int id, double price) : base(id, price) { }
}
public class Vinyl : Item
{
public Vinyl(int id, double price) : base(id, price) { }
}
}
Observations:
Itemis the base class with two public properties:ID(int) andPrice(double)- The
GetDiscount(double percentage)method calculates the discount amount from a percentage and returns the result rounded to 2 decimal places BookandVinylinherit fromItemand just pass parameters to parent constructor viabase(id, price)
Program.cs — Initial entry point
using System;
using System.Collections.Generic;
namespace Visitor_Pattern
{
class Program
{
static void Main(string[] args)
{
List<Object> items = new List<Object>
{
new Book(12345, 11.99),
new Book(78910, 22.79),
new Vinyl(11198, 17.99),
new Book(63254, 9.79)
};
}
}
}
Note: The list is of type List<Object> in the Starter. It will be changed to List<IVisitableElement> during the implementation of the pattern.
3.2 Final Draft (Final)
The final project (M02_Final) contains the complete implementation of the Visitor Pattern with all files.
Item.cs — Classes made visitable
using System;
using System.Collections.Generic;
using System.Text;
namespace Visitor_Pattern
{
public class Item
{
public int ID;
public double Price;
public Item(int id, double price)
{
this.ID = id;
this.Price = price;
}
public double GetDiscount(double percentage)
{
return Math.Round(Price * percentage, 2);
}
}
public class Book : Item, IVisitableElement
{
public Book(int id, double price) : base(id, price) { }
public void Accept(IVisitor visitor)
{
visitor.VisitBook(this);
}
}
public class Vinyl : Item, IVisitableElement
{
public Vinyl(int id, double price) : base(id, price) { }
public void Accept(IVisitor visitor)
{
visitor.VisitVinyl(this);
}
}
}
Changes compared to Starter:
Booknow implementsIVisitableElementwith theAcceptmethod which callsvisitor.VisitBook(this)Vinylnow implementsIVisitableElementwith theAcceptmethod which callsvisitor.VisitVinyl(this)- The
Itemclass itself is not modified — this is a fundamental principle of the Visitor Pattern
Visitor.cs — Concrete interfaces and visitors
using System;
using System.Collections.Generic;
using System.Text;
namespace Visitor_Pattern
{
// Interface fondamentale 1 : le Visitor
public interface IVisitor
{
void VisitBook(Book book);
void VisitVinyl(Vinyl vinyl);
void Print();
}
// Interface fondamentale 2 : l'Element visitable
public interface IVisitableElement
{
void Accept(IVisitor visitor);
}
// Concrete Visitor 1 : calcul des rabais
public class DiscountVisitor : IVisitor
{
private double _savings;
public void VisitBook(Book book)
{
var discount = 0.0;
if (book.Price < 20.00)
{
discount = book.GetDiscount(0.10);
Console.WriteLine($"DISCOUNTED: Book #{book.ID} is now ${Math.Round(book.Price - discount, 2)}");
}
else
{
Console.WriteLine($"FULL PRICE: Book #{book.ID} is ${book.Price}");
}
_savings += discount;
}
public void VisitVinyl(Vinyl vinyl)
{
var discount = vinyl.GetDiscount(0.15);
Console.WriteLine($"SUPER SAVINGS: Vinyl #{vinyl.ID} is now ${Math.Round(vinyl.Price - discount, 2)}");
_savings += discount;
}
public void Print()
{
Console.WriteLine($"\nYou saved a total of ${Math.Round(_savings, 2)} on today's order!");
}
public void Reset()
{
_savings = 0.0;
}
}
// Concrete Visitor 2 : comptage des ventes
public class SalesVisitor : IVisitor
{
private int BookCount = 0;
private int VinylCount = 0;
public void VisitBook(Book book)
{
BookCount++;
}
public void VisitVinyl(Vinyl vinyl)
{
VinylCount++;
}
public void Print()
{
Console.WriteLine($"Books sold: {BookCount} \nVinyl sold: {VinylCount}");
Console.WriteLine($"The store sold {BookCount + VinylCount} units today!");
}
}
}
ObjectStructure.cs — Object structure
using System;
using System.Collections.Generic;
using System.Text;
namespace Visitor_Pattern
{
public class ObjectStructure
{
private List<IVisitableElement> _cart;
public ObjectStructure(List<IVisitableElement> items)
{
_cart = items;
}
public void RemoveItem(IVisitableElement item)
{
_cart.Remove(item);
}
public void ApplyVisitor(IVisitor visitor)
{
Console.WriteLine("\n------- Visitor Breakdown -------");
foreach (var item in _cart)
item.Accept(visitor);
visitor.Print();
}
}
}
Program.cs — Final entry point
using System;
using System.Collections.Generic;
namespace Visitor_Pattern
{
class Program
{
static void Main(string[] args)
{
List<IVisitableElement> items = new List<IVisitableElement>
{
new Book(12345, 11.99),
new Book(78910, 22.79),
new Vinyl(11198, 17.99),
new Book(63254, 9.79)
};
var cart = new ObjectStructure(items);
var discountVisitor = new DiscountVisitor();
var salesVisitor = new SalesVisitor();
cart.ApplyVisitor(discountVisitor);
cart.ApplyVisitor(salesVisitor);
discountVisitor.Reset();
cart.RemoveItem(items[2]);
cart.ApplyVisitor(discountVisitor);
}
}
}
4. Visitor Pattern UML Diagram
Here is the complete representation of the Visitor Pattern architecture as implemented in this course:
┌─────────────────────────────────────────────────┐
│ «interface» IVisitor │
├─────────────────────────────────────────────────┤
│ + VisitBook(book: Book): void │
│ + VisitVinyl(vinyl: Vinyl): void │
│ + Print(): void │
└───────────────────┬─────────────────────────────┘
│ implements
┌─────────────────────────┴──────────────────────────┐
│ │
┌─────────────┴────────────────┐ ┌────────────────────┴──────────┐
│ DiscountVisitor │ │ SalesVisitor │
├──────────────────────────────┤ ├───────────────────────────────┤
│ - _savings: double │ │ - BookCount: int │
├──────────────────────────────┤ │ - VinylCount: int │
│ + VisitBook(book): void │ ├───────────────────────────────┤
│ + VisitVinyl(vinyl): void │ │ + VisitBook(book): void │
│ + Print(): void │ │ + VisitVinyl(vinyl): void │
│ + Reset(): void │ │ + Print(): void │
└──────────────────────────────┘ └───────────────────────────────┘
┌─────────────────────────────────────────────────┐
│ «interface» IVisitableElement │
├─────────────────────────────────────────────────┤
│ + Accept(visitor: IVisitor): void │
└───────────────────┬─────────────────────────────┘
│ implements
┌─────────────────────────┴──────────────────────────┐
│ │
┌─────────────┴──────────┐ ┌──────────────┴──────────┐
│ Book │ │ Vinyl │
├────────────────────────┤ ├─────────────────────────┤
│ (hérite de Item) │ │ (hérite de Item) │
├────────────────────────┤ ├─────────────────────────┤
│ + Accept(visitor): void│ │ + Accept(visitor): void │
└────────────────────────┘ └─────────────────────────┘
│ hérite de │ hérite de
└─────────────────────┬────────────────────────────┘
│
┌───────────────┴──────────────────┐
│ Item │
├──────────────────────────────────┤
│ + ID: int │
│ + Price: double │
├──────────────────────────────────┤
│ + Item(id, price) │
│ + GetDiscount(pct): double │
└──────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ ObjectStructure │
├──────────────────────────────────────────────┤
│ - _cart: List<IVisitableElement> │
├──────────────────────────────────────────────┤
│ + ObjectStructure(items) │
│ + RemoveItem(item): void │
│ + ApplyVisitor(visitor): void │
└──────────────────────────────────────────────┘
5. Summary and key points
What we learned
| Concept | Description |
|---|---|
| Visitor Pattern | Behavioral pattern for adding behaviors to class hierarchies without modifying them |
| IVisitor | Interface defining a Visit method for each type of concrete element |
| IVisitableElement | Interface defining the Accept method which accepts a visitor |
| Concrete Visitor | Class implementing IVisitor with specific logic for each type |
| Double Dispatch | Mechanism at the heart of the pattern: Accept delegates to Visit by passing this |
| Object Structure | Manager of the collection of visitable elements and orchestrator of visitors |
| State accumulation | Visitors can maintain a state (_savings, BookCount) across visits |
Implementation Checklist
To implement the Visitor Pattern in a C# project:
- Create the
IVisitorinterface with aVisitmethod for each element type - Create the
IVisitableElementinterface with theAccept(IVisitor)method - Implement
IVisitableElementin each element class - In each
Acceptmethod, call the correspondingVisitmethod on the visitor - Create one or more concrete visitors implementing
IVisitor - (Optional) Create an
ObjectStructureto manage the collection of elements - From the calling code, instantiate the visitors and call
AcceptorApplyVisitor
Pattern selection criteria
Use the Visitor Pattern when:
- You have a diverse class hierarchy that requires type-specific behaviors
- You want to add new operations without modifying existing classes
- Class hierarchy is stable and unlikely to change
- You need to accumulate state or gather information across different classes
Avoid the Visitor Pattern when:
- Class hierarchy changes frequently (high maintenance cost)
- Strict encapsulation is critical (pattern may need to access internal states)
- The hierarchy is simple with few types (over-engineering)
Relations with other patterns
- Composite Pattern: the Visitor Pattern integrates naturally with the Composite Pattern to traverse tree structures
- Interpreter Pattern: the Visitor Pattern can be used to perform interpretation in the context of the Interpreter Pattern
- Iterator Pattern: the Object Structure can use an Iterator to traverse the elements
Search Terms
c-sharp · design · patterns · visitor · testing · architecture · c# · .net · development · pattern · classes · initial · object · objectstructure · concrete · creation · diagram · entry · interfaces · item.cs · point · program · program.cs · relations