Visitor Pattern

Greetings, fellow developers! Welcome back to .Net Core Central. Today’s exploration takes us into the intricate world of design patterns, with a spotlight on the Visitor Design Pattern – a gem within the Gang of Four design patterns.

Unveiling the Visitor Pattern

The Visitor Design Pattern, nestled within the realm of behavioral design patterns, is a powerful tool for representing operations performed on the elements of an object structure. This pattern brings flexibility to the table by allowing us to define new operations without altering the classes of the elements it operates on.

Navigating the Intent of Visitor Pattern

The primary objective of the Visitor Design Pattern is to provide a means of representing operations to be executed on the elements of an object structure. It accomplishes this by introducing a visitor that defines these operations, thereby separating concerns and promoting a more modular and extensible design.

Now, let’s dive into a practical example to grasp the essence of the Visitor Design Pattern.

The Notification Conundrum

Imagine a scenario where a system needs to send various types of notifications—invoices, marketing materials, or subscription expiration alerts. Each notification type requires different content and logic. Initially, the system supports email and text notifications, each implemented through an interface named INotificationSender.

namespace VisitorPattern.Demo;

internal interface INotificationSender
{
    void Send(string message);
}

However, a glaring problem arises when we contemplate adding new notification protocols, such as WebSockets or mobile notifications via Firebase. The existing implementation necessitates changes to accommodate these new protocols, violating the principle of open-closed design.

Refining the Solution with Visitor Pattern

To address this issue, let’s refactor our implementation using the Visitor Design Pattern. The core idea is to extract the setup responsibilities for each notification protocol into separate visitor classes. This ensures that adding new protocols won’t impact the existing notification senders.

We introduce a new method, Accept, in the INotificationSender interface, which accepts a IVisitor. This sets the stage for visitors to define the setup operations.

namespace VisitorPattern.Demo;

internal interface INotificationSender
{
    void Send(string message);
    void Accept(IVisitor visitor);
}

Crafting the Visitors

Two visitor classes emerge: EmailVisitor and TextVisitor, each responsible for setting up the email and text protocols, respectively. This neatly compartmentalizes the setup logic.

Now, adding a new notification protocol, like WebSockets, becomes a breeze. We create a WebsocketVisitor class without altering the existing notification senders.

namespace VisitorPattern.Demo;

internal interface IVisitor
{
    void Visit(INotificationSender notificationSender);
}

internal class EmailVisitor : IVisitor
{
    public void Visit(INotificationSender notificationSender)
    {
        Console.WriteLine("Setup email");
    }
}

internal class TextVisitor : IVisitor
{
    public void Visit(INotificationSender notificationSender)
    {
        Console.WriteLine("Setup text");
    }
}

internal class WebsocketVisitor : IVisitor
{
    public void Visit(INotificationSender notificationSender)
    {
        Console.WriteLine("Setup websocket");
    }
}

Embracing Flexibility with Visitors

In the final demonstration, we witnessed how easily we can adapt our system to support diverse notification protocols. The Accept method seamlessly integrates with different visitors, allowing each notification sender to be configured independently.

For the notification senders, we can have one for Invioce and one for Marketing.

namespace VisitorPattern.Demo;

internal class InvoiceNotificationSender : INotificationSender
{
    public void Accept(IVisitor visitor)
    {
        visitor.Visit(this);
    }

    public void Send(string message)
    {
        Console.WriteLine($"Notification sent: {message}");
    }
}

internal class MarketingNotificationSender : INotificationSender
{
    public void Accept(IVisitor visitor)
    {
        visitor.Visit(this);
    }

    public void Send(string message)
    {
        Console.WriteLine($"Notification sent: {message}");
    }
}

Finally, we can wire everything up.

using VisitorPattern.Demo;

var emailVisitor = new EmailVisitor();
var textVisitor = new TextVisitor();
var websocketVisitor = new WebsocketVisitor();

var notificationSender1 = new InvoiceNotificationSender();
notificationSender1.Accept(emailVisitor);
notificationSender1.Accept(textVisitor);
notificationSender1.Send("Invoice");

var notificationSender2 = new MarketingNotificationSender();
notificationSender2.Accept(emailVisitor);
notificationSender2.Accept(textVisitor);
notificationSender2.Accept(websocketVisitor);
notificationSender2.Send("Marketing");

Conclusion: Empowering Designs with Visitors

The Visitor Design Pattern showcases its prowess in enhancing flexibility and maintainability in our code. By delegating setup responsibilities to visitors, we adhere to the principle of separating concerns, making our system more adaptable to future changes.

And that concludes our journey into the Visitor Design Pattern in .NET development. If you found this exploration enlightening, do go through the rest of my blog posts.

For more coding adventures, consider going to my YouTube channel and subscribing to .Net Core Central. Until next time, happy coding!