Today, we're diving deep into one of the design patterns often seen as a head-scratcher: The Visitor Pattern.

The "Visitor Pattern" Explained... Again

I've noticed that, throughout my career, the "Visitor Pattern" seems to be the one design pattern that stumps many of us.

Here's the textbook definition:
"Visitor is one of the behavioral design patterns. It allows new operations to be added to existing object structures without modifying them, adhering to the open/closed principle. It separates algorithms from objects they operate on, using a separate visitor object to encapsulate the new operation. This ensures flexibility, easy addition of new functions without changing object structure, and modular, maintainable changes."

What is the Visitor Pattern Solving For Us?

  1. Decoupling Operations from Objects: The primary goal of the Visitor pattern is to decouple operations from the objects they operate upon. This means you can add new actions without changing the classes of the elements they work on.
  2. Open/Closed Principle: This pattern ensures that you can add new operations without modifying existing code, adhering to the open/closed principle.

When to Use the Visitor Pattern

  • When you need to perform multiple different operations across a set of disparate objects and want to avoid "polluting" their classes with these operations.
  • When you have a complex object structure and want to add operations on these objects without altering their classes.

Still not clear? Fear not! You're not alone. Let me try with a video game analogy.

Design Patterns Meet Video Games

Imagine you're playing a World of Warcraft hardcore and just hit level 60. You're eager to learn new abilities, get some items, and accept end-game quests.

Now, here's the connection:

  • Your WoW character is like the 'Visitor' in the pattern. It has actions (methods) it can perform with each NPC: Visit(Trainer), Visit(Vendor), Visit(QuestGiver).
  • NPCs in the game are objects your character interacts with. Instead of NPCs dictating the outcome of the interaction, they "accept" the visitor (your character), who then decides what to do based on the NPC type.

This is a simplified way to understand the Visitor Pattern. Your character can be extended with new actions without changing the NPC code, a prime example of the open/closed principle.

This is what it would look like in code:

Interfaces

// Visitable Interface
public interface INpc
{
    void Accept(ICharacterVisitor visitor);
}

// Visitor Interface
public interface ICharacterVisitor
{
    void Visit(Trainer trainer);
    void Visit(QuestGiver questGiver);
    void Visit(Vendor vendor);
}

NPCs

public class Trainer : INpc
{
    public void Accept(ICharacterVisitor visitor)
    {
        visitor.Visit(this);
    }

    public string OfferAbility()
    {
        return "Frostbolt";
    }
}

public class QuestGiver : INpc
{
    public void Accept(ICharacterVisitor visitor)
    {
        visitor.Visit(this);
    }

    public string GiveQuest()
    {
        return "Attunement to the Core";
    }
}

public class Vendor : INpc
{
    public void Accept(ICharacterVisitor visitor)
    {
        visitor.Visit(this);
    }

    public string SellItem()
    {
        return "Ethereal Pomegranate";
    }
}

Player Character


public class PlayerCharacter : ICharacterVisitor
{
    public int Gold { get; private set; } = 100;
    public List<string> Spellbook { get; } = new();
    public List<string> QuestLog { get; } = new();
    public List<string> Inventory { get; } = new();

    public void Visit(Trainer trainer)
    {
        string ability = trainer.OfferAbility();
        if (Gold >= 20)
        {
            Gold -= 20;
            Spellbook.Add(ability);
            Console.WriteLine(
                $"Learned new ability: {ability}. " +
                $"Gold remaining: {Gold}");
        }
        else
        {
            Console.WriteLine($"Not enough gold to learn {ability}.");
        }
    }

    public void Visit(Vendor vendor)
    {
        string item = vendor.SellItem();
        if (Gold >= 5)
        {
            Gold -= 5;
            Inventory.Add(item);

            Console.WriteLine(
                $"Bought {item}. " +
                $"Gold remaining: {Gold}");
        }
        else
        {
            Console.WriteLine($"Not enough gold to buy {item}.");
        }
    }

    public void Visit(QuestGiver questGiver)
    {
        string quest = questGiver.GiveQuest();
        QuestLog.Add(quest);
        Console.WriteLine($"Accepted quest: {quest}");
    }
}

Execution

public static class Program
{
    public static void Main()
    {
        ICharacterVisitor playerCharacter = new PlayerCharacter();
        INpc[] npcs = { new Trainer(), new Vendor(), new QuestGiver() };

        foreach (var npc in npcs)
        {
            npc.Accept(playerCharacter);
        }
    }
}

// Output
Learned new ability: Frostbolt. Gold remaining: 80
Bought Ethereal Pomegranate. Gold remaining: 75
Accepted quest: Attunement to the Core

Is this what it looks like behind the scenes in WoW for real? Most likely not, but I hope the analogy helped.

Real World Examples

The visitor pattern is commonly used in various applications, especially when dealing with abstract syntax trees or other hierarchical data structures.

LINQ Expression Trees: In C#, Language-Integrated Query (LINQ) can be used to represent structured queries. Underneath, these queries can be represented as expression trees. When you want to process or transform these trees, a visitor can be handy. The .NET framework provides an ExpressionVisitor class that can be extended to traverse these trees and perform custom operations.

public class SampleExpressionVisitor : ExpressionVisitor
{
    protected override Expression VisitBinary(BinaryExpression node)
    {
        // Custom processing for binary expressions
        return base.VisitBinary(node);
    }
}

The above is a simple custom visitor that can process binary expressions in an expression tree.

Roslyn: The .NET Compiler Platform, known as Roslyn, provides open-source C# and Visual Basic compilers with rich code analysis APIs. Roslyn's APIs allow you to analyze and generate code. When dealing with syntax trees, Roslyn heavily uses the Visitor pattern, providing a SyntaxVisitor class that developers can extend for custom processing.

public class SampleSyntaxVisitor : SyntaxVisitor
{
    public override void VisitToken(SyntaxToken token)
    {
        // Custom processing for syntax tokens
        base.VisitToken(token);
    }
}

This is a basic example of where you might want to process specific syntax tokens within a code file.

Wrap Up

Design patterns can often feel abstract and challenging. But by connecting them to familiar scenarios, like gaming, we can make them more digestible and relatable.