DEV Community

Discussion on: The Visitor Pattern in PHP

Collapse
bdelespierre profile image
Benjamin Delespierre • Edited on

The double dispatch can be confusing and make the code harder to understand.

While it is true more indirection can create confusion, the naive implementation (a service relying on instanceof to determine the correct algorithm to use) is very rigid (creates static dependencies between components) and doesn't scale at all (imagine handling hundreds of components this way, good luck 🙄)

The visitor pattern benefits clearly outweights the added complexity and create much cleaner and understandable code IMHO.

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

I don't see why 🤷

All Visitors need every method on the VisitorInterface while it might not have an implementation for it.

You can mitigate that problem by applying the Interface Segregation Sprinciple (ISP) ("no client should be forced to depend on methods it does not use.")

Pro tip: using traits helps keep things clean & reusable.

Using the solution below, we see the Book class now only rely on the BookPageCountVisitorInterface and not the whole visitor. The visitor concrete class doesn't have to implement visitDocument if it's not needed or possible 👍

TL;DR makin small interfaces & traits gives you the flexibility to only implement what you need 😎

interface BookPageCountVisitorInterface
{
    public function visitBook(Book $book): int;
}

interface DocumentPageCountVisitorInterface
{
    public function visitDocument(Document $doc): int;
}

// you can still assemble them into a single interface if you wish
interface PageCountVisitorInterface extends BookPageCountVisitorInterface, DocumentPageCountVisitorInterface
{
    // empty
}

class Book
{
    public function accept(BookPageCountVisitorInterface $visitor)
    {
        return $visitor->visitBook($this);
    }
}

trait BookPageCountVisitor
{
    public function visitBook(Book $book): int
    {
        return 1;
    }
}

trait DocumentPageCountVisitor
{
    public function visitDocument(Document $doc): int
    {
        return 2;
    }
}

class PageCountVisitor implements PageCountVisitorInterface
{
    use BookPageCountVisitor;
    use DocumentPageCountVisitor;
}

$visitor = new PageCountVisitor();
$book = new Book();

echo $book->accept($visitor); // 1
Enter fullscreen mode Exit fullscreen mode
Collapse
doekenorg profile image
Doeke Norg Author • Edited on

I don't see why 🤷

Because every visitor has a different reason for being, so they all will have different results. There is no single return type or return value type.

TL;DR makin small interfaces & traits gives you the flexibility to only implement what you need 😎

This will however replace one VisitorInterface with several new interfaces. Which doesn't make much sense to me either. Because now my Book can only receive a BookPageCountVisitorInterface which, by its name only serves one purpose. It needs to have the generic VisitorInterface to be able to receive any visitor.

And those trait BookPageCountVisitor traits will never be reused, because they serve the purpose of one visitor. So they might as well live on that visitor only, right?

I personally would rather have one interface that has a function for every type, and create an abstract base class that implements all those functions with an empty body. Then there would be less overhead in files, and my visitors only need to overwrite the functions that matter.