Overview

The Visitor pattern isn’t something you see very often. While preparing this article, I had to brush up on it myself! In all my years as a software engineer, I don’t think I’ve ever applied it in a real project but.. maybe now I will.

A Visitor class is essentially a bundle of business logic or operations that you want to perform on one or more entities, without putting that logic inside the entities themselves.

It’s a useful pattern to be familiar with, so let’s dive in. I like to keep these patterns in my back pocket, you never know when one might come in handy, like having an emergency tissue when you’ve sneezed all over your face, twice, and you’re on a date, and everyone is looking at you (sorry, I can’t think of anything witty, like at all, so here we are).

The problem

Let’s say you have Book and Document classes and we want to know how many pages each have. The Document class simply has a getPageCount method.

class Document 
{
    public function __construct(
        private int $pages, 
        private string $content = ''
    ) {

    }
    
    public function getPageCount(): int {
        return $this->pages;
    }

    public function getContent(): string {
        return $this->content;
    }
}

However, Book has an array of Chapter objects, which have a getPageCount function.

class Chapter
{
    public function __construct(
        private int $pages, 
        private string $content = ''
    ) {

    }
    
    public function getPageCount(): int {
        return $this->pages;
    }

    public function getContent(): string {
        return $this->content;
    }
}
 
class Book
{
    public function getChapters(): array
    {
        return [
            new Chapter(5, 'some text'),
            new Chapter(7, 'some more text'),
            new Chapter(2, 'some more text'),
        ];
    }
}

To handle this, we quickly knock together a service. It ends up looking something like this:

class PageCountService
{
    public function handle(mixed $entity): int
    {
        if ($entity instanceof Document) {
            return $entity->getPageCount();
        } elseif ($entity instanceof Book) {
            $count = 0;
            foreach ($entity->getChapters() as $chapter) {
                $count += $chapter->getPageCount();
            }
 
            return $count;
        }

        throw new Exception('invalid entity');
    }
}

And we can call it like this:

$document = new Document(20);
$book = new Book();
 
$dumper = new PageCountService();
 
$dumper->handle($document); // 20
$dumper->handle($book); // 14

Because there is no shared interface or abstraction between Document and Book, the handle() function can only receive a mixed $entity and contains the logic for either situation.

When adding on more entities, this type checking will pile on and can become quite cumbersome and unreadable.

Also, we throw an exception when the entity type is unknown to avoid improper use.

We can definitely clean this up.

The solution

With the Visitor pattern, we’ll end up removing the need for the instanceOf type checks, while keeping the reference to the entity type intact and it will remove the need to explicitly throw an exception.

Let's start breaking this down, first up, let’s remove the instanceof checks, add entity methods for each possible type and rename the class from Service to Visitor.

class PageCountVisitor
{
    public function visitBook(Book $book): void
    {
        $count = 0;
 
        foreach ($book->getChapters() as $chapter) {
            $count += $chapter->getPageCount();
        }
 
        echo $count;
    }
 
    public function visitDocument(Document $document): void
    {
        echo $document->getPageCount();
    }
}

With the new type-hinted arguments methods, we've removed the need for the instanceOf checks and since we can only call these methods with the appropriate type, there is no need to throw an exception.

If in future, we need to add another entity, maybe it’s a Report, we’d add a new public function visitReport(Report $report) and implement the logic separately.

We’re not quite finished with this class yet. Let’s create an interface for it and make the Visitor class implement it.

interface VisitorInterface
{
    public function visitBook(Book $book): void;
    public function visitDocument(Document $document): void;
}

class PageCountVisitor implements VisitorInterface
{
    public function visitBook(Book $book): void
    {
        $count = 0;
 
        foreach ($book->getChapters() as $chapter) {
            $count += $chapter->getPageCount();
        }
 
        echo $count;
    }
 
    public function visitDocument(Document $document): void
    {
        echo $document->getPageCount();
    }
}

Then we create a VisitableInterface and apply it on Book and Document.

interface VisitableInterface
{
    public function accept(VisitorInterface $visitor): void;
}

class Book implements VisitableInterface
{
    public function accept(VisitorInterface $visitor): void
    {
        $visitor->visitBook($this);
    }
}
 
class Document implements VisitableInterface
{
    public function accept(VisitorInterface $visitor): void
    {
        $visitor->visitDocument($this);
    }
}

The Visitor Pattern uses a technique called Double Dispatch, in which the entity calls the correct function on the visitor, while providing itself as the argument.

The Book class calls the visitBook() method on the visitor and Document calls visitDocument(). Both are providing themselves as the parameter. Because of this minor change to the entity we can now apply all kinds of different visitors that provide a certain functionality for every entity.

To use the visitor on the entities we need to adjust our calling code like this:

$document = new Document(20);
$book = new Book();
 
$visitor = new PageCountVisitor();
 
$document->accept($visitor); // int(20)
$book->accept($visitor); // int(14)

We are free to create more visitors that implement the VisitorInterface and can perform a certain feature for both Book and Document. A WordCountVisitor for example.

class WordCountVisitor implements VisitorInterface
{
    public function visitBook(Book $book): void
    {
        $words = 0;
 
        foreach ($book->getChapters() as $chapter) {
            $words += str_word_count($chapter->getContent());
        }
 
        echo $words;
    }
 
    public function visitDocument(Document $document): void
    {
        echo str_word_count($document->getContent());
    }
}

Simply, call like this:

$document = new Document(20, 'Some words in here');
$book = new Book();
 
$visitor = new WordCountVisitor();
 
$document->accept($visitor); // int(4)
$book->accept($visitor); // int(8)

Advantages

The Visitor pattern comes with some clear benefits:

  • Separation of Concerns
    Each visitor handles a specific functionality, keeping your entities clean. Your Book and Document classes don’t get bloated with unrelated logic like page counting, word counting, or exporting data.

  • Open/Closed Principle
    You can add new operations without modifying the existing entities. Need a WordCountVisitor, a PrintVisitor, or even a ExportToPdfVisitor? No problem, just create a new visitor class.

  • Consistency Across Entities
    Each visitor defines how it handles all entities. You avoid duplicating logic across multiple classes, keeping your code easier to maintain.

  • Avoids Type Checking
    Goodbye messy instanceof checks. Each entity knows how to “dispatch” itself to the correct visitor method, making your code more readable and less error-prone.

Disadvantages

Like any pattern, Visitor comes with trade-offs:

  • Tightly Coupled to Entity Structure
    If you add new entities, you must update every visitor interface and class. This can become tedious in a system with lots of entities and visitors.

  • Overkill for Simple Use Cases
    For small projects or when you only need a single operation, the pattern can add unnecessary complexity. Sometimes a simple service class is just fine.

  • Can Break Encapsulation
    Visitors often need to access internal data of entities. You might need to provide getter methods, which can expose internal state that wasn’t meant to be public.

  • The accept() and visit...() methods usually don't return anything, so you need to keep records on the visitor itself.


Conclusion

The Visitor pattern may not be something you use every day, but it’s a handy tool to have in your toolbox. It allows you to add functionality cleanly, maintain a single point of control for operations across multiple entities, and keep your codebase organised.

The Visitor pattern shines in situations where you have:

  • A fixed set of entities that rarely change.

  • Multiple unrelated operations you want to perform on those entities.

  • A need to keep entity classes clean and focused on their primary responsibilities.

If you find yourself constantly writing instanceof checks or bloating your entities with unrelated methods, it’s probably a good time to consider the Visitor pattern.

And ya know, even if you never use it in a real project, at least you’ll sound fancy dropping “double dispatch” in your next code review.

What’s double dispatch again?

// First step.
$book->accept($visitor); // Book “tells” visitor: call visitBook

// Second step.
$visitor->visitBook($book); // Visitor does the actual page counting

Want to see the Visitor Pattern in action? Check out this simple example: Github

The Grumpy Dev is a reader-supported publication. To receive new posts and support my work, consider becoming a free or paid subscriber.