Skip to Content
Course content

151: The Visitor Pattern

Click on the "Edit" button in the top corner of the screen to edit your slide content.

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 PlainTextVisitor must implement IDocumentVisitor.
  • 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 StringBuilder that can be retrieved via a GetResult() method.

Ensure that you do not modify the existing DocumentElement, TextElement, or ImageElement classes.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.