Visitor

Represent an operation to be performed on the elements of an object structure. Visitor lets you define a new operation without changing the classes of the elements on which it operates.



Usage in JavaScript:
low


Summary


The Visitor pattern defines a new operation to a collection of objects without changing the objects themselves. The new logic resides in a separate object called the Visitor.

Visitors are useful when building extensibility in a library or framework. If the objects in your project provide a 'visit' method that accepts a Visitor object which can make changes to the receiving object then you are providing an easy way for clients to implement future extensions.

In most programming languages the Visitor pattern requires that the original developer anticipates functional adjustments in the future. This is done by including methods that accept a Visitor and let it operate on the original collection of objects.

Visitor is not important to JavaScript because it offers far more flexibility by the ability to add and remove methods at runtime.


Diagram



Participants


The objects participating in this pattern are:

  • ObjectStructure -- In sample code: employees array
    • maintains a collection of Elements which can be iterated over
  • Elements -- In sample code: Employee objects
    • defines an accept method that accepts visitor objects
    • in the accept method the visitor's visit method is invoked with 'this' as a parameter
  • Visitor -- In sample code: ExtraSalary, ExtraVacation
    • implements a visit method. The argument is the Element being visited. This is where the Element's changes are made

JavaScript Code


In this example three employees are created with the Employee constructor function. Each is getting a 10% salary raise and 2 more vacation days. Two visitor objects, ExtraSalary and ExtraVacation, make the necessary changes to the employee objects.

Note that the visitors, in their visit methods, access the closure variables salary and vacation through a public interface. It is the only way because closures are not accessible from the outside. In fact, salary and vacation are not variables, they are function arguments, but it works because they are also part of the closure.

Notice the self variable. It is used to maintain the current context (stored as a closure variable) and is used in the visit method.

The log function is a helper which collects and displays results.


var Employee = function (name, salary, vacation) {

    var self = this;
        
    this.accept = function (visitor) {
        visitor.visit(self);
    };
    this.getName = function () {
        return name;
    };
    this.getSalary = function () {
        return salary;
    };
    this.setSalary = function (sal) {
        salary = sal;
    };
    this.getVacation = function () {
        return vacation;
    };
    this.setVacation = function (vac) {
        vacation = vac;
    };
};

var ExtraSalary = function () {
    this.visit = function (emp) {
        emp.setSalary(emp.getSalary() * 1.1);
    };
};
var ExtraVacation = function () {
    this.visit = function (emp) {
        emp.setVacation(emp.getVacation() + 2);
    };
};

// log helper
var log = (function() {
    var log = "";
    return {
        add: function(msg) { log += msg + "\n"; },
        show: function() { alert(log); log = ""; }
    }
})();


function run() {
        
    var employees = [
        new Employee("John", 10000, 10),
        new Employee("Mary", 20000, 21),
        new Employee("Boss", 250000, 51)
    ];

    var visitorSalary = new ExtraSalary();
    var visitorVacation = new ExtraVacation();
        
    for (var i = 0, len = employees.length; i < len; i++) {

        var emp = employees[i];
            
        emp.accept(visitorSalary);
        emp.accept(visitorVacation);

        log.add(emp.getName() + ": $" + emp.getSalary() +
            " and " + emp.getVacation() + " vacation days");
    }

    log.show();
}
Run



JavaScript Optimized Code


The Namespace pattern is applied to keep the code out of the global namespace. Our namespace is named Patterns.Classic. A Revealing Module named Visitor returns (i.e. reveals) only a single item: the Employee constructor function.

In the run method an array with 3 employees is created. Next, two 'visitor' functions are defined: extraSalary and extraVacation which are going to be applied to each employee. We could have added these to the Visitor module, but this better demonstrates that you can arbitrarily create and apply functions to any object. The only requirement is that the properties and methods referenced in the function do exist on the object.

This shows that the Visitor pattern is essentially native to JavaScript as expressed by the Apply Invocation pattern.

The Patterns object contains the namespace function which constructs namespaces non-destructively, that is, if a name already exists it won't overwrite it.

The log function is a helper which collects and displays results.


var Patterns = {
    namespace: function (name) {
        var parts = name.split(".");
        var ns = this;

        for (var i = 0, len = parts.length; i < len; i++) {
            ns[parts[i]] = ns[parts[i]] || {};
            ns = ns[parts[i]];
        }

        return ns;
    }
};

Patterns.namespace("Classic").Visitor = (function () {

    var Employee = function (name, salary, vacation) {
           
        this.getName = function () { return name; };
        this.setName = function (value) { name = value; };
            
        this.getSalary = function () { return salary; };
        this.setSalary = function (value) { salary = value; };
            
        this.getVacation = function () { return vacation; };
        this.setVacation = function (value) { vacation = value; }
    };

    return { Employee: Employee };

})();

// log helper
var log = (function () {
    var log = "";
    return {
        add: function (msg) { log += msg + "\n"; },
        show: function () { alert(log); log = ""; }
    }
})();


function run() {

    var visitor = Patterns.Classic.Visitor;

    var employees = [
        new visitor.Employee("John", 10000, 10),
        new visitor.Employee("Mary", 20000, 21),
        new visitor.Employee("Boss", 250000, 51)
    ];

    // create 'visitor' functions
    var extraSalary = function () { 
        this.setSalary(this.getSalary() * 1.1) 
    };

    var extraVacation = function () { 
        this.setVacation(this.getVacation() + 2) 
    };

    for (var i = 0, len = employees.length; i < len; i++) {

        var emp = employees[i];

        // apply 'visitor' functions
        extraSalary.apply(emp);
        extraVacation.apply(emp);

        log.add(emp.getName() + ": $" + emp.getSalary() +
            " and " + emp.getVacation() + " vacation days");
    }

    log.show();
}
Run



  Strategy