C#
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Object-Oriented C#
-
Section 4: Working with Data
-
Section 5: Error Handling
-
Section 6: Delegates and Events
-
Section 7: Async Programming
-
Section 8: More Language Features
-
Section 9: File I/O and Serialization
-
Section 10: Networking in .NET
-
Section 11: The .NET Ecosystem
-
Section 12: Memory and Performance
-
Section 13: Concurrency Beyond Async
-
Section 14: Reflection and Attributes
-
Section 15: Testing and Best Practices
-
Section 16: Design Patterns in C#
-
Section 17: Standard Library Deep Dive
-
Section 18: Data Structures and Algorithms in C#
-
Section 19: GUI and Desktop Development Overview
-
Section 20: Practical Projects
-
Section 21: More Practice Exercises
-
Section 22: More Standard Library and Text Processing
-
Section 23: More Design Patterns
-
Section 24: More Projects
-
Section 25: Interview Practice
-
Section 26: C# Keywords Reference (Modifiers)
-
Section 27: C# Keywords Reference (Statements)
-
Section 28: C# Keywords Reference (Operators)
-
Section 29: BCL: System.Collections.Generic
-
Section 30: BCL: System.Linq
-
Section 31: BCL: System.Threading
-
Section 32: BCL: System.IO
-
Section 33: BCL: System.Text and System.Text.Json
-
Section 34: BCL: System.Net.Http
-
Section 35: C# Language Specification Topics
-
Section 36: More Practice Exercises
-
Section 37: More Async Patterns
-
Section 38: More BCL: System.Reflection and System.Diagnostics
-
Section 39: Nullable Reference Types In Depth
-
Section 40: C# Records and Pattern Matching In Depth
-
Section 41: Dependency Injection Deep Dive
-
Section 42: More Interview and Whiteboard Practice
151: The Visitor Pattern
Listen, the Visitor pattern is one of those patterns that feels like total over-engineering until you've actually had to maintain a massive class hierarchy. If you only have two or three classes, just add a method to them and move on. But when you have a complex object structure and you find yourself constantly adding new operations to every single class in that hierarchy, you're in for a world of pain. That's where the Visitor pattern saves your sanity.
Let's imagine we're building a document processing system. We have different types of elements: text paragraphs, images, and tables. We want to be able to perform various operations on these elements—like exporting them to HTML or calculating a word count—without bloating the element classes themselves.
Defining our Document Elements
First, I'll set up the basic structure. We need a base class for our elements. The key here is the Accept method. This is the "hook" that allows a visitor to enter the object.
public abstract class DocumentElement
{
public abstract void Accept(IDocumentVisitor visitor);
}
public class TextElement : DocumentElement
{
public string Text { get; set; } = "";
public override void Accept(IDocumentVisitor visitor) => visitor.Visit(this);
}
public class ImageElement : DocumentElement
{
public string Source { get; set; } = "";
public string AltText { get; set; } = "";
public override void Accept(IDocumentVisitor visitor) => visitor.Visit(this);
}
The Wall I Hit with Direct Methods
Now, here's where I usually mess up. In a previous project, my instinct was to just add a RenderHtml() method directly to DocumentElement. It seems intuitive, right? But then the client asked for Markdown export. Then they asked for a Plain Text export. Then they wanted a "Security Scanner" to check for malicious links in the text.
Suddenly, my TextElement class is 500 lines long, filled with logic that has nothing to do with being a "Text Element" and everything to do with exporting and scanning. I'm violating the Single Responsibility Principle and the Open/Closed Principle every time I add a feature. I've essentially turned my data objects into a dumping ground for every possible operation the system might ever need.
Decoupling with the Visitor Interface
To fix this, I'll move the logic out of the elements and into a separate Visitor hierarchy. I'll define an interface that knows how to handle every type of element we have.
public interface IDocumentVisitor
{
void Visit(TextElement textElement);
void Visit(ImageElement imageElement);
}
Now, the elements don't need to know how to render themselves. They just need to know how to "accept" a visitor. The logic for the actual operation lives entirely inside the visitor implementation. Let's build an HTML exporter.
public class HtmlExportVisitor : IDocumentVisitor
{
private StringBuilder _sb = new StringBuilder();
public void Visit(TextElement textElement)
{
_sb.AppendLine($"<p>{textElement.Text}</p>");
}
public void Visit(ImageElement imageElement)
{
_sb.AppendLine($"<img src=\"{imageElement.Source}\" alt=\"{imageElement.AltText}\" />");
}
public override string GetResult() => _sb.ToString();
}
Adding New Features Without Touching the Core
Here is the magic: if I want to add a word count feature, I don't touch TextElement or ImageElement. I don't risk breaking the HTML export. I just create a new visitor.
public class WordCountVisitor : IDocumentVisitor
{
public int TotalWords { get; private set; }
public void Visit(TextElement textElement)
{
TotalWords += textElement.Text.Split(' ', StringSplitOptions.RemoveEmptyEntries).Length;
}
public void Visit(ImageElement imageElement)
{
// Images don't have words, so we just do nothing here.
}
}
To put it all together, you just iterate through your list of elements and call Accept. The double-dispatch mechanism (the element calling the visitor, which then calls the specific method for that element's type) ensures the right logic runs every time.
var document = new List<DocumentElement>
{
new TextElement { Text = "Hello world!" },
new ImageElement { Source = "logo.png", AltText = "Company Logo" },
new TextElement { Text = "This is a Visitor pattern example." }
};
var htmlVisitor = new HtmlExportVisitor();
foreach (var element in document)
{
element.Accept(htmlVisitor);
}📋 Practical Task
Implementing a Document Plain-Text Stripper
You have a document system using the Visitor pattern as described in the lesson. Your task is to create a new visitor called PlainTextVisitor.
- The
PlainTextVisitormust implementIDocumentVisitor. - For
TextElement, it should append the raw text followed by a newline. - For
ImageElement, it should append a placeholder string:[Image: AltText]followed by a newline. - The visitor should maintain a internal string or
StringBuilderthat can be retrieved via aGetResult()method.
Ensure that you do not modify the existing DocumentElement, TextElement, or ImageElement classes.
There are no comments for now.