Visitor Pattern

The pattern allows:

  • for one or more operations to be applied to a set of objects at run-time, decoupling the operations from the object structure.

Visitor Pattern


The Visitor pattern is a way of separating an algorithm from an object structure on which it operates. A practical result of this separation is the ability to add new operations to existing object structures without modifying those structures. It is one way to easily follow the OCP (open/closed principle).


PHP Example

Following example shows how hierarchical structure of objects of the same interface (Composite pattern) can be printed. Instead of creating "print" methods for each class (Book, Chapter, and Article), a single class (Reporter) performs the required printing action.

<?php
/*
 * @category Design Pattern Tutorial
 * @package Visitor Sample
 * @author Dmitry Sheiko <me@dsheiko.com>
 * @link http://dsheiko.com
 */

abstract class Content
{
    public $title;
    abstract public function save();
}
class Book extends Content
{
    public $author;
    public $isbn;
    public $chapters = array();
    public function addItem($chapter)
    {
        $this->chapters[] = $chapter;
    }
    public function save()
    {
        //..
    }
    public function accept(ContentVisitor $visitor)
    {
        $visitor->visit($this);
        array_map(function($element) use ($visitor) {
            $element->accept($visitor);
        }, $this->chapters);
    }
}
class Chapter extends Content
{
    public $articles = array();
    public function addItem($article)
    {
        $this->articles[] = $article;
    }
    public function save()
    {
        //..
    }
    public function accept(ContentVisitor $visitor)
    {
        $visitor->visit($this);
        array_map(function($element) use ($visitor) {
            $element->accept($visitor);
        }, $this->articles);
    }
}
class Artile extends Content
{
    public $text = "...";
    public function save()
    {
        //..
    }
    public function accept(ContentVisitor $visitor)
    {
        $visitor->visit($this);
    }
}

interface ContentVisitor
{
    public function visit(Content $content);
}

class Reporter implements ContentVisitor
{
    public function visit(Content $content)
    {
        echo "\nObject: ", get_class($content), " \n";
        foreach ($content as $property => $value) {
            echo $property . ": ", $value, " \n";
        }
    }
}


/**
 * Usage
 */

$book1 = new Book();
$book1->title = "Clean Code A Handbook of Agile Software Craftsmanship";
$book1->author = "Robert C. Martin";
$book1->isbn = "0132350882";
$chapter1 = new Chapter();
$chapter1->title = "Chapter 17: Smells and Heuristics";
$article1 = new Artile();
$article1->title = "C1: Inappropriate Information";
$article2 = new Artile();
$article2->title = "C2: Obsolete Comment";
$article3 = new Artile();
$article3->title = "C3: Redundant Comment";
$chapter1->addItem($article1);
$chapter1->addItem($article2);
$chapter1->addItem($article3);
$book1->addItem($chapter1);

$book1->accept(new Reporter());

// Output:
// Object: Book
// author: Robert C. Martin
// isbn: 0132350882
// chapters: Array
// title: Clean Code A Handbook of Agile Software Craftsmanship
//
// Object: Chapter
// articles: Array
// title: Chapter 17: Smells and Heuristics
//
// Object: Artile
// text: ...
// title: C1: Inappropriate Information
//
// Object: Artile
// text: ...
// title: C2: Obsolete Comment
//
// Object: Artile
// text: ...
// title: C3: Redundant Comment

JS/ES5 Example

Now consider G+ feed. Let’s say we are to develop something alike. When somebody comments on a user post the event is fired. ComentList object is aware, so it updates the list of comments on the user side. But that is not enough. User expects number of unread notifications increases and a new message in the notification list. Both object can be notified as visitors for CommentList object.

/*
 * @category Design Pattern Tutorial
 * @package Visitor Sample
 * @author Dmitry Sheiko <me@dsheiko.com>
 * @link http://dsheiko.com
 */
(function() {
/**
 * Helper function, which performs synchronous XMLHttpRequest
 */
var Gateway = {};
Gateway.post = function(url, data, callback) {
    var _xhr = new XMLHttpRequest();
    _xhr.open("POST", url, false);
    _xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");
    _xhr.setRequestHeader("Content-type","application/x-www-form-urlencoded");
    _xhr.send(data);
    if (_xhr.status == 0) {
        callback (JSON.parse(_xhr.responseText));
    }
};

var Session = {
    userId : 1
};

var CommentList = function(article) {
    var _article = article;
    return {
        arrivals : [],
        acceptVisitor : function(visitor) {
            visitor.visitCommentList(this);
        },
        update: function() {
            Gateway.post("/comment/getAllNew", 'articleId=' + _article.id, function(data){
                this.arrivals = data;
            });
        }
    }
}
var NotificationList = function() {
    return {
        list : [],
        visitCommentList : function(commentList) {
            for (var i in commentList.arrivals) {
                var message = commentList.arrivals[i];
                if (message.targetUserId == Session.userId) {
                    this.list.push('New comment from ' + message.targetUserName);
                }
            }
        }
    }
}
var NotificationIndicator = function() {
    return {
        counter: 0,
        visitCommentList : function(commentList) {
            for (var i in commentList.arrivals) {
                var message = commentList.arrivals[i];
                if (message.targetUserId == Session.userId) {
                   this.counter ++;
                }
            }
        }
    }
}

/**
 * Usage
 */
var widget = new CommentList({ id: 1});
widget.update();
widget.acceptVisitor(new NotificationList());
widget.acceptVisitor(new NotificationIndicator());

}());