Hello, everyone. Today, I want to discuss a pattern you might have encountered, even though it's not very common. This pattern is rarely used, but let's take a closer look at it. We'll explore when it should be applied and examine its advantages and disadvantages.
What's the Visitor pattern?
The main point of this pattern is that it allows you to separate an algorithm from the object structure. In other words, if you have a complex class hierarchy and want to perform different operations without modifying these classes, the Visitor pattern is ideal.
For example, consider having employees of two types: full-time and contractors. You may want to calculate taxes for each group, generate reports, or compute bonuses. Instead of adding new methods to every employee class, you can use the Visitor pattern to implement these operations externally.
Scenario
Let's consider a real-world scenario: we want to display a file tree and show the size of each file. There are two concrete types of items: files and folders, both of which are considered file elements. Additionally, we want to generate a file report. For this example, let's look at the bin executable folder.
Ordinary approach
How would you complete this task? You would first create a base type for the hierarchy, which should be an abstract class. This class represents the general structure of a file, regardless of whether it is a file or a folder. Each type should have a Name property, a method to get the Size, and a Report method.
public abstract class FileSystemElement(string name)
{
protected string Name { get; } = string.IsNullOrEmpty(name) ? "root" : name;
public abstract long GetSize();
public abstract void PrintReport(int depth = 0);
}
Next, we should implement the folder type. Starting from the root folder, we retrieve the sizes of its child files and print the folder's name. The GetSize() method calculates the total size of the root folder, while the PrintReport() method displays the folder name and manages spacing to reflect the tree hierarchy.
public class DirectoryElement(string name) : FileSystemElement(name)
{
private List<FileSystemElement> Children { get; } = [];
public void Add(FileSystemElement element)
{
Children.Add(element);
}
public override long GetSize()
{
long total = 0;
foreach (var child in Children)
total += child.GetSize();
return total;
}
public override void PrintReport(int depth = 0)
{
Console.WriteLine($"{new string(' ', depth * 2)}📁 {Name}");
foreach (var child in Children)
child.PrintReport(depth + 1);
}
}
Currently, we display only folders. We need to add the file type. Here, we also set the file size and display all relevant information.
public class FileElement(string name, long size) : FileSystemElement(name)
{
private long Size { get; } = size;
public override long GetSize() => Size;
public override void PrintReport(int depth = 0)
{
Console.WriteLine($"{new string(' ', depth * 2)}📄 {Name} ({Size / 1024.0:F2} KB)");
}
}
How do we use it? First, we obtain the target path, then build the tree hierarchy, print the entire tree, and display the total size.
public void OrdinaryApproach()
{
string targetPath = Path.Combine(AppContext.BaseDirectory);
Console.WriteLine($"Building file tree from: {targetPath}\n");
var root = BuildDirectoryTreeForOrdinary(targetPath);
Console.WriteLine("=== File tree ===");
root.PrintReport();
Console.WriteLine($"\nTotal size: {root.GetSize() / 1024.0:F2} KB");
}
private static Ordinary.DirectoryElement BuildDirectoryTreeForOrdinary(string path)
{
var directory = new Ordinary.DirectoryElement(Path.GetFileName(path));
foreach (var file in Directory.GetFiles(path))
{
var fileInfo = new FileInfo(file);
directory.Add(new Ordinary.FileElement(fileInfo.Name, fileInfo.Length));
}
foreach (var dir in Directory.GetDirectories(path))
{
directory.Add(BuildDirectoryTreeForOrdinary(dir));
}
return directory;
}
After launching, you should see a result that looks something like this:
=== File tree ===
📁 root
📄 Gee.External.Capstone.dll (282,50 KB)
📄 Microsoft.DotNet.PlatformAbstractions.dll (22,88 KB)
📄 System.Management.dll (69,38 KB)
📄 VisitorPattern.runtimeconfig.json (0,32 KB)
📄 Microsoft.Extensions.Logging.dll (30,99 KB)
📄 BenchmarkDotNet.dll (1175,50 KB)
📄 JetBrains.FormatRipper.dll (85,00 KB)
📄 Microsoft.Extensions.Logging.Abstractions.dll (46,52 KB)
📄 BenchmarkDotNet.Annotations.dll (28,00 KB)
📄 Microsoft.Diagnostics.FastSerialization.dll (73,43 KB)
📄 Microsoft.CodeAnalysis.CSharp.dll (5794,62 KB)
📄 JetBrains.Profiler.Api.dll (18,00 KB)
📄 JetBrains.HabitatDetector.dll (59,50 KB)
📄 Iced.dll (1824,00 KB)
📄 Microsoft.Extensions.Configuration.dll (24,99 KB)
📄 Perfolizer.dll (193,50 KB)
📄 JetBrains.Profiler.SelfApi.dll (55,50 KB)
📄 VisitorPattern.deps.json (24,26 KB)
📄 VisitorPattern (106,16 KB)
📄 Microsoft.Extensions.Primitives.dll (34,99 KB)
📄 Microsoft.Diagnostics.NETCore.Client.dll (126,87 KB)
📄 TraceReloggerLib.dll (22,67 KB)
📄 Microsoft.Diagnostics.Runtime.dll (531,89 KB)
📄 Microsoft.Extensions.Configuration.Abstractions.dll (19,99 KB)
📄 Microsoft.Extensions.Options.dll (39,49 KB)
📄 VisitorPattern.pdb (15,33 KB)
📄 Microsoft.Extensions.Configuration.Binder.dll (24,49 KB)
📄 Microsoft.CodeAnalysis.dll (2620,62 KB)
📄 BenchmarkDotNet.Diagnostics.dotTrace.dll (19,50 KB)
📄 Settings.pdb (13,14 KB)
📄 Microsoft.Extensions.DependencyInjection.Abstractions.dll (35,99 KB)
📄 VisitorPattern.dll (9,50 KB)
📄 Microsoft.Diagnostics.Tracing.TraceEvent.dll (3088,44 KB)
📄 System.CodeDom.dll (178,88 KB)
📄 Settings.dll (4,50 KB)
📄 CommandLine.dll (220,00 KB)
📄 Microsoft.Bcl.AsyncInterfaces.dll (14,57 KB)
📄 Dia2Lib.dll (57,64 KB)
📁 zh-Hans
📄 Microsoft.CodeAnalysis.resources.dll (40,11 KB)
📄 Microsoft.CodeAnalysis.CSharp.resources.dll (327,15 KB)
📁 zh-Hant
📄 Microsoft.CodeAnalysis.resources.dll (41,12 KB)
📄 Microsoft.CodeAnalysis.CSharp.resources.dll (327,15 KB)
📁 pl
📄 Microsoft.CodeAnalysis.resources.dll (45,62 KB)
📄 Microsoft.CodeAnalysis.CSharp.resources.dll (387,65 KB)
📁 ja
📄 Microsoft.CodeAnalysis.resources.dll (48,65 KB)
📄 Microsoft.CodeAnalysis.CSharp.resources.dll (420,12 KB)
📁 it
📄 Microsoft.CodeAnalysis.resources.dll (45,62 KB)
📄 Microsoft.CodeAnalysis.CSharp.resources.dll (381,65 KB)
📁 cs
📄 Microsoft.CodeAnalysis.resources.dll (43,62 KB)
📄 Microsoft.CodeAnalysis.CSharp.resources.dll (360,62 KB)
📁 amd64
📄 msdia140.dll (1756,43 KB)
📄 KernelTraceControl.dll (260,45 KB)
📁 runtimes
📁 osx-x64
📁 native
📄 libcapstone.dylib (4576,90 KB)
📁 linux-arm
📁 native
📄 libcapstone.so (4956,35 KB)
📁 linux-arm64
📁 native
📄 libcapstone.so (7129,93 KB)
📁 win
📁 lib
📁 netcoreapp2.0
📄 System.Management.dll (287,88 KB)
📁 linux-x86
📁 native
📄 libcapstone.so (5081,67 KB)
📁 win-x86
📁 native
📄 capstone.dll (4714,00 KB)
📁 osx-arm64
📁 native
📄 libcapstone.dylib (4533,16 KB)
📁 win-x64
📁 native
📄 capstone.dll (5465,50 KB)
📁 linux-x64
📁 native
📄 libcapstone.so (6713,03 KB)
📁 win-arm64
📁 native
📄 capstone.dll (5028,00 KB)
📁 ru
📄 Microsoft.CodeAnalysis.resources.dll (54,62 KB)
📄 Microsoft.CodeAnalysis.CSharp.resources.dll (507,65 KB)
📁 x86
📄 msdia140.dll (1467,92 KB)
📄 KernelTraceControl.dll (181,45 KB)
📄 KernelTraceControl.Win61.dll (156,20 KB)
📁 pt-BR
📄 Microsoft.CodeAnalysis.resources.dll (44,65 KB)
📄 Microsoft.CodeAnalysis.CSharp.resources.dll (369,65 KB)
📁 de
📄 Microsoft.CodeAnalysis.resources.dll (45,62 KB)
📄 Microsoft.CodeAnalysis.CSharp.resources.dll (384,15 KB)
📁 ko
📄 Microsoft.CodeAnalysis.resources.dll (45,62 KB)
📄 Microsoft.CodeAnalysis.CSharp.resources.dll (387,12 KB)
📁 fr
📄 Microsoft.CodeAnalysis.resources.dll (45,62 KB)
📄 Microsoft.CodeAnalysis.CSharp.resources.dll (384,15 KB)
📁 es
📄 Microsoft.CodeAnalysis.resources.dll (45,12 KB)
📄 Microsoft.CodeAnalysis.CSharp.resources.dll (375,65 KB)
📁 arm64
📄 msdia140.dll (3412,89 KB)
📄 KernelTraceControl.dll (252,45 KB)
📁 tr
📄 Microsoft.CodeAnalysis.resources.dll (44,12 KB)
📄 Microsoft.CodeAnalysis.CSharp.resources.dll (366,65 KB)
📁 BenchmarkDotNet.Artifacts
📄 VisitorPattern.Program-20251029-141418.log (14350,01 KB)
📄 VisitorPattern.Program-20251029-154029.log (10672,75 KB)
📄 VisitorPattern.Program-20251029-140100.log (14349,62 KB)
📄 VisitorPattern.Program-20251029-141341.log (14349,80 KB)
📄 VisitorPattern.Program-20251029-140513.log (14349,81 KB)
📄 VisitorPattern.Program-20251029-141256.log (21517,92 KB)
📄 VisitorPattern.Program-20251029-153910.log (10672,97 KB)
📄 VisitorPattern.Program-20251029-153953.log (10672,72 KB)
📄 VisitorPattern.Program-20251029-141211.log (10765,70 KB)
📁 results
📄 VisitorPattern.Program-report.html (1,18 KB)
Total size: 200239,73 KB
It's a very simple implementation, but there's a significant problem: it's difficult to scale. If you want to add new actions, you have to modify each class. Imagine having many more types or needing to handle each extension type individually—it would be a nightmare to add new actions in such a setup.
Visitor approach
Here is a completely different approach. However, you should also include the base type, which is associated with the Visitor. This type only has a Name and does not contain any actions.
public interface IFileSystemElement
{
string Name { get; }
void Accept(IFileSystemVisitor visitor);
}
Only the Visitor knows the concrete types. It includes all types, but not their corresponding actions. Instead, it simply associates types with individual concrete visitors.
public interface IFileSystemVisitor
{
void Visit(FileElement file);
void Visit(DirectoryElement directory);
}
As you can see, the types themselves no longer perform any calculations; instead, they delegate this responsibility to the concrete visitors. Additionally, you may notice that DirectoryElement no longer calculates Size, unlike in the traditional approach.
public class DirectoryElement(string name) : IFileSystemElement
{
public string Name { get; } = string.IsNullOrEmpty(name) ? "root" : name;
public List<IFileSystemElement> Children { get; } = [];
public void Add(IFileSystemElement element)
{
Children.Add(element);
}
public void Accept(IFileSystemVisitor visitor)
{
visitor.Visit(this);
}
}
public class FileElement(string name, long size) : IFileSystemElement
{
public string Name { get; } = name;
public long Size { get; } = size;
public void Accept(IFileSystemVisitor visitor)
{
visitor.Visit(this);
}
}
Now, let's add a visitor that will handle size calculation.
public class SizeCalculatorVisitor : IFileSystemVisitor
{
public long TotalSize { get; private set; }
public void Visit(FileElement file)
{
TotalSize += file.Size;
}
public void Visit(DirectoryElement directory)
{
foreach (var child in directory.Children)
{
child.Accept(this);
}
}
}
The visitor focuses primarily on reporting. Unlike the traditional method, where each type managed its own reports, the visitor handles reports for both types.
public class ReportVisitor : IFileSystemVisitor
{
private int _depth = 0;
public void Visit(FileElement file)
{
Console.WriteLine($"{new string(' ', _depth * 2)}📄 {file.Name} ({file.Size / 1024.0:F2} KB)");
}
public void Visit(DirectoryElement directory)
{
Console.WriteLine($"{new string(' ', _depth * 2)}📁 {directory.Name}");
_depth++;
foreach (var child in directory.Children)
{
child.Accept(this);
}
_depth--;
}
}
How can you use it? This approach is slightly different. First, obtain the target path and build the tree hierarchy as before. Then, create the report visitor and apply it to the folder type. Next, create and use the size visitor in a similar way. Finally, display the total size.
public void VisitorApproach()
{
string targetPath = Path.Combine(AppContext.BaseDirectory);
Console.WriteLine($"Building file tree from: {targetPath}\n");
var root = BuildDirectoryTreeForVisitor(targetPath);
var reportVisitor = new Visitor.ReportVisitor();
Console.WriteLine("=== File tree ===");
root.Accept(reportVisitor);
var sizeVisitor = new Visitor.SizeCalculatorVisitor();
root.Accept(sizeVisitor);
Console.WriteLine($"\nTotal size: {sizeVisitor.TotalSize / 1024.0:F2} KB");
}
private static Visitor.DirectoryElement BuildDirectoryTreeForVisitor(string path)
{
var directory = new Visitor.DirectoryElement(Path.GetFileName(path));
foreach (var file in Directory.GetFiles(path))
{
var info = new FileInfo(file);
directory.Add(new Visitor.FileElement(info.Name, info.Length));
}
foreach (var dir in Directory.GetDirectories(path))
{
directory.Add(BuildDirectoryTreeForVisitor(dir));
}
return directory;
}
The result should be identical.
=== File tree ===
📁 root
📄 Gee.External.Capstone.dll (282,50 KB)
📄 Microsoft.DotNet.PlatformAbstractions.dll (22,88 KB)
📄 System.Management.dll (69,38 KB)
📄 VisitorPattern.runtimeconfig.json (0,32 KB)
📄 Microsoft.Extensions.Logging.dll (30,99 KB)
📄 BenchmarkDotNet.dll (1175,50 KB)
📄 JetBrains.FormatRipper.dll (85,00 KB)
📄 Microsoft.Extensions.Logging.Abstractions.dll (46,52 KB)
📄 BenchmarkDotNet.Annotations.dll (28,00 KB)
📄 Microsoft.Diagnostics.FastSerialization.dll (73,43 KB)
📄 Microsoft.CodeAnalysis.CSharp.dll (5794,62 KB)
📄 JetBrains.Profiler.Api.dll (18,00 KB)
📄 JetBrains.HabitatDetector.dll (59,50 KB)
📄 Iced.dll (1824,00 KB)
📄 Microsoft.Extensions.Configuration.dll (24,99 KB)
📄 Perfolizer.dll (193,50 KB)
📄 JetBrains.Profiler.SelfApi.dll (55,50 KB)
📄 VisitorPattern.deps.json (24,26 KB)
📄 VisitorPattern (106,16 KB)
📄 Microsoft.Extensions.Primitives.dll (34,99 KB)
📄 Microsoft.Diagnostics.NETCore.Client.dll (126,87 KB)
📄 TraceReloggerLib.dll (22,67 KB)
📄 Microsoft.Diagnostics.Runtime.dll (531,89 KB)
📄 Microsoft.Extensions.Configuration.Abstractions.dll (19,99 KB)
📄 Microsoft.Extensions.Options.dll (39,49 KB)
📄 VisitorPattern.pdb (15,33 KB)
📄 Microsoft.Extensions.Configuration.Binder.dll (24,49 KB)
📄 Microsoft.CodeAnalysis.dll (2620,62 KB)
📄 BenchmarkDotNet.Diagnostics.dotTrace.dll (19,50 KB)
📄 Settings.pdb (13,14 KB)
📄 Microsoft.Extensions.DependencyInjection.Abstractions.dll (35,99 KB)
📄 VisitorPattern.dll (9,50 KB)
📄 Microsoft.Diagnostics.Tracing.TraceEvent.dll (3088,44 KB)
📄 System.CodeDom.dll (178,88 KB)
📄 Settings.dll (4,50 KB)
📄 CommandLine.dll (220,00 KB)
📄 Microsoft.Bcl.AsyncInterfaces.dll (14,57 KB)
📄 Dia2Lib.dll (57,64 KB)
📁 zh-Hans
📄 Microsoft.CodeAnalysis.resources.dll (40,11 KB)
📄 Microsoft.CodeAnalysis.CSharp.resources.dll (327,15 KB)
📁 zh-Hant
📄 Microsoft.CodeAnalysis.resources.dll (41,12 KB)
📄 Microsoft.CodeAnalysis.CSharp.resources.dll (327,15 KB)
📁 pl
📄 Microsoft.CodeAnalysis.resources.dll (45,62 KB)
📄 Microsoft.CodeAnalysis.CSharp.resources.dll (387,65 KB)
📁 ja
📄 Microsoft.CodeAnalysis.resources.dll (48,65 KB)
📄 Microsoft.CodeAnalysis.CSharp.resources.dll (420,12 KB)
📁 it
📄 Microsoft.CodeAnalysis.resources.dll (45,62 KB)
📄 Microsoft.CodeAnalysis.CSharp.resources.dll (381,65 KB)
📁 cs
📄 Microsoft.CodeAnalysis.resources.dll (43,62 KB)
📄 Microsoft.CodeAnalysis.CSharp.resources.dll (360,62 KB)
📁 amd64
📄 msdia140.dll (1756,43 KB)
📄 KernelTraceControl.dll (260,45 KB)
📁 runtimes
📁 osx-x64
📁 native
📄 libcapstone.dylib (4576,90 KB)
📁 linux-arm
📁 native
📄 libcapstone.so (4956,35 KB)
📁 linux-arm64
📁 native
📄 libcapstone.so (7129,93 KB)
📁 win
📁 lib
📁 netcoreapp2.0
📄 System.Management.dll (287,88 KB)
📁 linux-x86
📁 native
📄 libcapstone.so (5081,67 KB)
📁 win-x86
📁 native
📄 capstone.dll (4714,00 KB)
📁 osx-arm64
📁 native
📄 libcapstone.dylib (4533,16 KB)
📁 win-x64
📁 native
📄 capstone.dll (5465,50 KB)
📁 linux-x64
📁 native
📄 libcapstone.so (6713,03 KB)
📁 win-arm64
📁 native
📄 capstone.dll (5028,00 KB)
📁 ru
📄 Microsoft.CodeAnalysis.resources.dll (54,62 KB)
📄 Microsoft.CodeAnalysis.CSharp.resources.dll (507,65 KB)
📁 x86
📄 msdia140.dll (1467,92 KB)
📄 KernelTraceControl.dll (181,45 KB)
📄 KernelTraceControl.Win61.dll (156,20 KB)
📁 pt-BR
📄 Microsoft.CodeAnalysis.resources.dll (44,65 KB)
📄 Microsoft.CodeAnalysis.CSharp.resources.dll (369,65 KB)
📁 de
📄 Microsoft.CodeAnalysis.resources.dll (45,62 KB)
📄 Microsoft.CodeAnalysis.CSharp.resources.dll (384,15 KB)
📁 ko
📄 Microsoft.CodeAnalysis.resources.dll (45,62 KB)
📄 Microsoft.CodeAnalysis.CSharp.resources.dll (387,12 KB)
📁 fr
📄 Microsoft.CodeAnalysis.resources.dll (45,62 KB)
📄 Microsoft.CodeAnalysis.CSharp.resources.dll (384,15 KB)
📁 es
📄 Microsoft.CodeAnalysis.resources.dll (45,12 KB)
📄 Microsoft.CodeAnalysis.CSharp.resources.dll (375,65 KB)
📁 arm64
📄 msdia140.dll (3412,89 KB)
📄 KernelTraceControl.dll (252,45 KB)
📁 tr
📄 Microsoft.CodeAnalysis.resources.dll (44,12 KB)
📄 Microsoft.CodeAnalysis.CSharp.resources.dll (366,65 KB)
📁 BenchmarkDotNet.Artifacts
📄 VisitorPattern.Program-20251029-141418.log (14350,01 KB)
📄 VisitorPattern.Program-20251029-154029.log (10672,75 KB)
📄 VisitorPattern.Program-20251029-140100.log (14349,62 KB)
📄 VisitorPattern.Program-20251029-141341.log (14349,80 KB)
📄 VisitorPattern.Program-20251029-140513.log (14349,81 KB)
📄 VisitorPattern.Program-20251029-141256.log (21517,92 KB)
📄 VisitorPattern.Program-20251029-153910.log (10672,97 KB)
📄 VisitorPattern.Program-20251029-153953.log (10672,72 KB)
📄 VisitorPattern.Program-20251029-141211.log (10765,70 KB)
📁 results
📄 VisitorPattern.Program-report.html (1,18 KB)
Total size: 200239,73 KB
As you can see, the types do not contain any actions. If you want to add another action, you don't need to change the types; you only need to create a new visitor.
Benchmark
How can this affect performance? Let's find out together.
As expected, the visitor approach is somewhat slower because of the additional classes involved.
Extension
Now, imagine you need to make some changes and add a new action. Let's implement item counting for each folder. In a non-Visitor scenario, you'd modify the DirectoryElement class by adding a new method and updating the PrintReport method. This example might seem straightforward, but I want to highlight the core idea.
public override void PrintReport(int depth = 0)
{
Console.WriteLine($"{new string(' ', depth * 2)}📁 {Name} ({CountElementsInCurrentDirectory()} items)");
foreach (var child in Children)
child.PrintReport(depth + 1);
}
private int CountElementsInCurrentDirectory() => Children.Count;
In the Visitor pattern, you do not modify DirectoryElement. Instead, you create a new visitor class and implement the required functionality there.
public class CountingVisitor : IFileSystemVisitor
{
private int TotalFiles { get; set; }
private int TotalDirectories { get; set; }
public void Visit(FileElement file)
{
TotalFiles++;
}
public void Visit(DirectoryElement directory)
{
TotalDirectories++;
foreach (var child in directory.Children)
child.Accept(this);
}
}
And also need to add showing in the report.
Console.WriteLine($"{new string(' ', _depth * 2)}📁 {directory.Name} ({directory.Children.Count} items)");
And don't forget to call the visitor.
var count = new Visitor.CountingVisitor();
root.Accept(count);
Conclusion
The Visitor pattern is a great approach for working with trees or hierarchical structures. It enables you to separate objects from the actions performed on them. Each visitor follows the single responsibility principle.
Pros
- Easily add a new behavior
- Flexibility
- Good in trees, file systems
Cons
- Performance
- Complicated
- Hard add a new type
Where can you use it? It is an excellent choice if you work with tree hierarchies or file systems, especially when you frequently add or modify behavior.
If you found this article helpful, I would appreciate your support.

This article's example, along with others, highlights the design patterns featured in my repository.



Top comments (0)