0

Visitor — operations on object structures without modifying them

Advanced5 min read·eng-04-009
compare

Concept

Visitor pattern lets you add new operations to an object structure without modifying the objects. You define a visitor class with a method for each type of element in the structure. Elements "accept" visitors and let them operate on them.

The double dispatch problem: PHP doesn't have built-in method overloading based on parameter types. $visitor->visit($element) — what visit() does depends on $element's type. You'd need instanceof checks. Visitor solves this with double dispatch: $element->accept($visitor) calls $visitor->visitCircle($this) inside Circle, and $visitor->visitSquare($this) inside Square.

Structure:

  • Visitor Interface: One visit*() method per concrete element type.
  • Concrete Visitor: Implements all visit*() methods. Encapsulates an operation.
  • Element Interface: accept(Visitor $visitor).
  • Concrete Elements: Call the appropriate visit*($this) on the visitor.

Benefits:

  • Add new operations (visitors) without modifying elements.
  • Gather related operations in one place (visitor class) rather than scattered across element classes.
  • Accumulate state across elements (e.g., total price across all items).

Drawbacks:

  • Adding a new element type requires updating ALL visitor classes.
  • Elements must expose their implementation for the visitor to do its job — can break encapsulation.

When to use: Object structure rarely changes (fixed element types), but you frequently add new operations (export formats, validations, reports).

Code Example

php
<?php
// Visitor Interface
interface DocumentVisitor
{
    public function visitHeading(Heading $heading): void;
    public function visitParagraph(Paragraph $paragraph): void;
    public function visitImage(Image $image): void;
    public function visitCodeBlock(CodeBlock $code): void;
}

// Element Interface
interface DocumentElement
{
    public function accept(DocumentVisitor $visitor): void;
}

// Concrete Elements
class Heading implements DocumentElement
{
    public function __construct(public readonly string $text, public readonly int $level = 1) {}
    public function accept(DocumentVisitor $visitor): void { $visitor->visitHeading($this); }
}

class Paragraph implements DocumentElement
{
    public function __construct(public readonly string $text) {}
    public function accept(DocumentVisitor $visitor): void { $visitor->visitParagraph($this); }
}

class Image implements DocumentElement
{
    public function __construct(public readonly string $src, public readonly string $alt) {}
    public function accept(DocumentVisitor $visitor): void { $visitor->visitImage($this); }
}

class CodeBlock implements DocumentElement
{
    public function __construct(public readonly string $code, public readonly string $language = 'php') {}
    public function accept(DocumentVisitor $visitor): void { $visitor->visitCodeBlock($this); }
}

// Concrete Visitors — separate operations for same element structure
class HtmlExporter implements DocumentVisitor
{
    private string $output = '';

    public function visitHeading(Heading $heading): void
    {
        $this->output .= "<h{$heading->level}>{$heading->text}</h{$heading->level}>\n";
    }

    public function visitParagraph(Paragraph $paragraph): void
    {
        $this->output .= "<p>{$paragraph->text}</p>\n";
    }

    public function visitImage(Image $image): void
    {
        $this->output .= "<img src='{$image->src}' alt='{$image->alt}'>\n";
    }

    public function visitCodeBlock(CodeBlock $code): void
    {
        $this->output .= "<pre><code class='language-{$code->language}'>{$code->code}</code></pre>\n";
    }

    public function getOutput(): string { return $this->output; }
}

class WordCountVisitor implements DocumentVisitor
{
    private int $count = 0;

    public function visitHeading(Heading $heading): void    { $this->count += str_word_count($heading->text); }
    public function visitParagraph(Paragraph $para): void   { $this->count += str_word_count($para->text); }
    public function visitImage(Image $image): void          { /* images have no words */ }
    public function visitCodeBlock(CodeBlock $code): void   { /* code not counted */ }

    public function getCount(): int { return $this->count; }
}

// Object structure
$document = [
    new Heading('PHP Design Patterns', 1),
    new Paragraph('Design patterns are reusable solutions to common programming problems.'),
    new Image('/images/patterns.png', 'Design Patterns Overview'),
    new CodeBlock("class Visitor { ... }", 'php'),
];

// Apply different visitors to the same structure
$html = new HtmlExporter();
foreach ($document as $element) { $element->accept($html); }
echo $html->getOutput();

$wordCount = new WordCountVisitor();
foreach ($document as $element) { $element->accept($wordCount); }
echo "Word count: " . $wordCount->getCount();