Confused about the Visitor Design Pattern

The code in the OP resembles a well-known variation of the Visitor design pattern known as an Internal Visitor (see e.g. Extensibility for the Masses. Practical Extensibility with Object Algebras by Bruno C. d. S. Oliveira and William R. Cook). That variation, however, uses generics and return values (instead of void) to solve some of the problems that the Visitor pattern addresses.

Which problem is that, and why is the OP variation probably insufficient?

The main problem addressed by the Visitor pattern is when you have heterogenous objects that you need to treat the same. As the Gang of Four, (the authors of Design Patterns) states, you use the pattern when

“an object structure contains many classes of objects with differing interfaces, and you want to perform operations on these objects that depend on their concrete classes.”

What’s missing from this sentence is that while you’d like to “perform operations on these objects that depend on their concrete classes”, you want to treat those concrete classes as though they have a single polymorphic type.

A period example

Using the animal domain is rarely illustrative (I’ll get back to that later), so here’s another more realistic example. Examples are in C# – I hope they’re still useful to you.

Imagine that you’re developing an online restaurant reservation system. As part of that system, you need to be able to show a calendar to users. This calendar could display how many remaining seats are available on a given day, or list all reservations on the day.

Sometimes, you want to display a single day, but at other times, you want to display an entire month as a single calendar object. Throw in an entire year for good measure. This means that you have three periods: year, month, and day. Each has differing interfaces:

public Year(int year)

public Month(int year, int month)

public Day(int year, int month, int day)

For brevity, these are just the constructors of three separate classes. Many people might just model this as a single class with nullable fields, but this then forces you to deal with null fields, or enums, or other kinds of nastiness.

The above three classes have different structure because they contain different data, yet you’d like to treat them as a single concept – a period.

To do so, define an IPeriod interface:

internal interface IPeriod
{
    T Accept<T>(IPeriodVisitor<T> visitor);
}

and make each class implement the interface. Here’s Month:

internal sealed class Month : IPeriod
{
    private readonly int year;
    private readonly int month;

    public Month(int year, int month)
    {
        this.year = year;
        this.month = month;
    }

    public T Accept<T>(IPeriodVisitor<T> visitor)
    {
        return visitor.VisitMonth(year, month);
    }
}

This enables you to treat the three heterogenous classes as a single type, and define operations on that single type without having to change the interface.

Here, for example, is an implementation that calculates the previous period:

private class PreviousPeriodVisitor : IPeriodVisitor<IPeriod>
{
    public IPeriod VisitYear(int year)
    {
        var date = new DateTime(year, 1, 1);
        var previous = date.AddYears(-1);
        return Period.Year(previous.Year);
    }

    public IPeriod VisitMonth(int year, int month)
    {
        var date = new DateTime(year, month, 1);
        var previous = date.AddMonths(-1);
        return Period.Month(previous.Year, previous.Month);
    }

    public IPeriod VisitDay(int year, int month, int day)
    {
        var date = new DateTime(year, month, day);
        var previous = date.AddDays(-1);
        return Period.Day(previous.Year, previous.Month, previous.Day);
    }
}

If you have a Day, you’ll get the previous Day, but if you have a Month, you’ll get the previous Month, and so on.

You can see the PreviousPeriodVisitor class and other Visitors in use in this article, but here are the few lines of code where they’re used:

var previous = period.Accept(new PreviousPeriodVisitor());
var next = period.Accept(new NextPeriodVisitor());

dto.Links = new[]
{
    url.LinkToPeriod(previous, "previous"),
    url.LinkToPeriod(next, "next")
};

Here, period is an IPeriod object, but the code doesn’t know whether it’s a Day, and Month, or a Year.

To be clear, the above example uses the Internal Visitor variation, which is isomorphic to a Church encoding.

Animals

Using animals to understand object-oriented programming is rarely illuminating. I think that schools should stop using that example, as it’s more likely to confuse than help.

The OP code example doesn’t suffer from the problem that the Visitor pattern solves, so in that context, it’s not surprising if you fail to see the benefit.

The Cat and Dog classes are not heterogenous. They have the same class field and the same behaviour. The only difference is in the constructor. You could trivially refactor those two classes to a single Animal class:

public class Animal {
    private int health;

    public Animal(int health) {
        this.health = health;
    }

    public void increaseHealth(int healthIncrement) {
        this.health += healthIncrement;
    }

    public int getHealth() {
        return health;
    }
}

Then define two creation methods for cats and dogs, using the two distinct health values.

Since you now have a single class, no Visitor is warranted.

Leave a Comment