Visitor Pattern

Definition

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.

Explanation

In object-oriented programming and software engineering, the visitor design 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 follow the open/closed principle.

In essence, the visitor allows one to add new virtual functions to a family of classes without modifying the classes themselves; instead, one creates a visitor class that implements all of the appropriate specializations of the virtual function. The visitor takes the instance reference as input, and implements the goal through double dispatch.

Example

Consider the design of a 2D CAD system. At its core there are several types to represent basic geometric shapes like circles, lines and arcs. The entities are ordered into layers, and at the top of the type hierarchy is the drawing, which is simply a list of layers, plus some additional properties.

A fundamental operation on this type hierarchy is saving the drawing to the system’s native file format. At first glance it may seem acceptable to add local save methods to all types in the hierarchy. But then we also want to be able to save drawings to other file formats, and adding more and more methods for saving into lots of different file formats soon clutters the relatively pure geometric data structure we started out with.

A naïve way to solve this would be to maintain separate functions for each file format. Such a save function would take a drawing as input, traverse it and encode into that specific file format. But if you do this for several different formats, you soon begin to see lots of duplication between the functions. For example, saving a circle shape in a raster format requires very similar code no matter what specific raster form is used, and is different from other primitive shapes; the case for other primitive shapes like lines and polygons is similar. The code therefore becomes a large outer loop traversing through the objects, with a large decision tree inside the loop querying the type of the object. Another problem with this approach is that it is very easy to miss a shape in one or more savers, or a new primitive shape is introduced but the save routine is implemented only for one file type and not others, leading to code extension and maintenance problems.

Instead, one could apply the Visitor pattern. The Visitor pattern encodes a logical operation on the whole hierarchy into a single class containing one method per type. In our CAD example, each save function would be implemented as a separate Visitor subclass. This would remove all duplication of type checks and traversal steps. It would also make the compiler complain if a shape is omitted.

Another motivation is to reuse iteration code. For example iterating over a directory structure could be implemented with a visitor pattern. This would allow you to create file searches, file backups, directory removal, etc. by implementing a visitor for each function while reusing the iteration code.

Screencast

TypeScript Code

module Visitor {
    interface IElementVisitor {
        visitElement(element: Element): void;
        visitElementNode(elementNode: ElementNode): void;
    }

    class Element {
        private _name: string;
        private _parent: ElementNode;

        constructor(name: string, parent?: ElementNode) {
            if (!name) {
                throw new Error("Argument null exception!");
            }

            this._name = name;
            this._parent = parent;
        }

        public get name(): string {
            return this._name;
        }

        public get parent(): ElementNode {
            return this._parent;
        }

        public set parent(value:ElementNode) {
            this._parent = value;
        }

        public get depth(): number {
            if (this._parent) {
                return this._parent.depth + 1;
            }

            return 0;
        }

        public accept(visitor: IElementVisitor): void {
            visitor.visitElement(this);
        }
    }

    class ElementNode extends Element {
        private _children = new Array<Element>();

        constructor(name: string, parent?: ElementNode) {
            super(name, parent);
        }

        public get length(): number {
            return this._children.length;
        }

        public appendChild(child: Element): ElementNode {
            child.parent = this;
            this._children.push(child);
            return this;
        }

        public accept(visitor: IElementVisitor): void {
            visitor.visitElementNode(this);
            this._children.forEach(function (child) {
                child.accept(visitor);
            });
        }
    }

    class LogWriter implements IElementVisitor {
        visitElement(element: Element): void {
            Output.WriteLine("Visiting the element: '" + element.name + "'");
        }

        visitElementNode(elementNode: ElementNode): void {
            Output.WriteLine("Visiting the element node: '" + elementNode.name + "'. Which has: " + elementNode.length + " child nodes.");
        }
    }

    class ConsoleWriter implements IElementVisitor {
        visitElement(element: Element): void {
            console.log("Visiting the element: '" + element.name + "'");
        }

        visitElementNode(elementNode: ElementNode): void {
            console.log("Visiting the element node: '" + elementNode.name + "'. Which has: " + elementNode.length + " child nodes.");
        }
    }

    window.addEventListener("load", function () {
        var constructedTree = new ElementNode("first").appendChild(new Element("firstChild"))
                                            .appendChild(new ElementNode("secondChild").appendChild(new Element("furtherDown")));
        var logwriter = new LogWriter();
        constructedTree.accept(logwriter);

        var consolewriter = new ConsoleWriter();
        constructedTree.accept(consolewriter);
    });
}

Output