Skip to content

Visitor Design Pattern

Video Lecture

Section Video Links
Visitor Overview Visitor Overview Visitor Overview 
Visitor Use Case Visitor Use Case Visitor Use Case 
hasattr() Method hasattr() Method hasattr() Method 
expandtabs() Method expandtabs() Method expandtabs() Method 

Overview

Your object structure inside an application may be complicated and varied. A good example is what could be created using the Composite structure.

The objects that make up the hierarchy of objects, can be anything and most likely complicated to modify as your application grows.

Instead, when designing the objects in your application that may be structured in a hierarchical fashion, you can allow them to implement a Visitor interface.

The Visitor interface describes an accept() method that a different object, called a Visitor, will use in order to traverse through the existing object hierarchy and read the internal attributes of an object.

The Visitor pattern is useful when you want to analyze, or reproduce an alternative object hierarchy without implementing extra code in the object classes, except for the original requirements set by implementing the Visitor interface.

Similar to the template pattern it could be used to output different versions of a document but more suited to objects that may be members of a hierarchy.

Terminology

  • Visitor Interface: An interface for the Concrete Visitors.
  • Concrete Visitor: The Concrete Visitor will traverse the hierarchy of elements.
  • Concrete Element: An object that will be visited. An application will contain a variable number of Elements that can be structured in any particular hierarchy.
  • Visitable Interface: The interface that elements should implement, that describes the accept() method that will allow them to be visited (traversed).

Visitor UML Diagram

Visitor Pattern UML Diagram

Source Code

In the concept code below, a hierarchy of any object is created. It is similar to a simplified composite. The objects of Element can also contain a hierarchy of sub elements.

The Element class could also consist of many variations, but this example uses only one.

Rather than writing specific code inside all these elements every time I wanted to handle a new custom operation, I can implement the IVisitable interface and create the accept() method that allows the Visitor to pass through it and access the Elements internal attributes.

Two different Visitor classes are created, PrintElementNamesVisitor and CalculateElementTotalsVisitor. They are instantiated and passed through the existing Object hierarchy using the same IVisitable interface.

./visitor/visitor_concept.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
# pylint: disable=too-few-public-methods
# pylint: disable=arguments-differ
"The Visitor Pattern Concept"
from abc import ABCMeta, abstractmethod

class IVisitor(metaclass=ABCMeta):
    "An interface that custom Visitors should implement"
    @staticmethod
    @abstractmethod
    def visit(element):
        "Visitors visit Elements/Objects within the application"

class IVisitable(metaclass=ABCMeta):
    """
    An interface the concrete objects should implement that allows
    the visitor to traverse a hierarchical structure of objects
    """
    @staticmethod
    @abstractmethod
    def accept(visitor):
        """
        The Visitor traverses and accesses each object through this
        method
        """

class Element(IVisitable):
    "An Object that can be part of any hierarchy"

    def __init__(self, name, value, parent=None):
        self.name = name
        self.value = value
        self.elements = set()
        if parent:
            parent.elements.add(self)

    def accept(self, visitor):
        "required by the Visitor that will traverse"
        for element in self.elements:
            element.accept(visitor)
        visitor.visit(self)

# The Client
# Creating an example object hierarchy.
Element_A = Element("A", 101)
Element_B = Element("B", 305, Element_A)
Element_C = Element("C", 185, Element_A)
Element_D = Element("D", -30, Element_B)

# Now Rather than changing the Element class to support custom
# operations, we can utilise the accept method that was
# implemented in the Element class because of the addition of
# the IVisitable interface

class PrintElementNamesVisitor(IVisitor):
    "Create a visitor that prints the Element names"
    @staticmethod
    def visit(element):
        print(element.name)

# Using the PrintElementNamesVisitor to traverse the object hierarchy
Element_A.accept(PrintElementNamesVisitor)

class CalculateElementTotalsVisitor(IVisitor):
    "Create a visitor that totals the Element values"
    total_value = 0

    @classmethod
    def visit(cls, element):
        cls.total_value += element.value
        return cls.total_value

# Using the CalculateElementTotalsVisitor to traverse the
# object hierarchy
TOTAL = CalculateElementTotalsVisitor()
Element_A.accept(CalculateElementTotalsVisitor)
print(TOTAL.total_value)

Output

python ./visitor/visitor_concept.py
D
B
C
A
561

SBCODE Editor

<>

Visitor Example Use Case

In the example, the client creates a car with parts.

The car and parts inherit an abstract car parts class with predefined property getters and setters.

Instead of creating methods in the car parts classes and abstract class that run bespoke methods, the car parts can all implement the IVisitor interface.

This allows for the later creation of Visitor objects to run specific tasks on the existing hierarchy of objects.

Visitor Example UML Diagram

Visitor Pattern Use Case UML Diagram

Source Code

./visitor/client.py

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
# pylint: disable=too-few-public-methods
# pylint: disable=arguments-differ
"The Visitor Pattern Use Case Example"
from abc import ABCMeta, abstractmethod

class IVisitor(metaclass=ABCMeta):
    "An interface that custom Visitors should implement"
    @staticmethod
    @abstractmethod
    def visit(element):
        "Visitors visit Elements/Objects within the application"

class IVisitable(metaclass=ABCMeta):
    """
    An interface that concrete objects should implement that allows
    the visitor to traverse a hierarchical structure of objects
    """
    @staticmethod
    @abstractmethod
    def accept(visitor):
        """
        The Visitor traverses and accesses each object through this
        method
        """

class AbstractCarPart():
    "The Abstract Car Part"
    @property
    def name(self):
        "a name for the part"
        return self._name

    @name.setter
    def name(self, value):
        self._name = value

    @property
    def sku(self):
        "The Stock Keeping Unit (sku)"
        return self._sku

    @sku.setter
    def sku(self, value):
        self._sku = value

    @property
    def price(self):
        "The price per unit"
        return self._price

    @price.setter
    def price(self, value):
        self._price = value

class Body(AbstractCarPart, IVisitable):
    "A part of the car"

    def __init__(self, name, sku, price):
        self.name = name
        self.sku = sku
        self.price = price

    def accept(self, visitor):
        visitor.visit(self)

class Engine(AbstractCarPart, IVisitable):
    "A part of the car"

    def __init__(self, name, sku, price):
        self.name = name
        self.sku = sku
        self.price = price

    def accept(self, visitor):
        visitor.visit(self)

class Wheel(AbstractCarPart, IVisitable):
    "A part of the car"

    def __init__(self, name, sku, price):
        self.name = name
        self.sku = sku
        self.price = price

    def accept(self, visitor):
        visitor.visit(self)

class Car(AbstractCarPart, IVisitable):
    "A Car with parts"

    def __init__(self, name):
        self.name = name
        self._parts = [
            Body("Utility", "ABC-123-21", 1001),
            Engine("V8 engine", "DEF-456-21", 2555),
            Wheel("FrontLeft", "GHI-789FL-21", 136),
            Wheel("FrontRight", "GHI-789FR-21", 136),
            Wheel("BackLeft", "GHI-789BL-21", 152),
            Wheel("BackRight", "GHI-789BR-21", 152),
        ]

    def accept(self, visitor):
        for parts in self._parts:
            parts.accept(visitor)
        visitor.visit(self)

class PrintPartsVisitor(IVisitor):
    "Print out the part name and sku"
    @staticmethod
    def visit(element):
        if hasattr(element, 'sku'):
            print(f"{element.name}\t:{element.sku}".expandtabs(6))

class TotalPriceVisitor(IVisitor):
    "Print out the total cost of the parts in the car"
    total_price = 0

    @classmethod
    def visit(cls, element):
        if hasattr(element, 'price'):
            cls.total_price += element.price
        return cls.total_price

# The Client
CAR = Car("DeLorean")

# Print out the part name and sku using the PrintPartsVisitor
CAR.accept(PrintPartsVisitor())

# Calculate the total prince of the parts using the TotalPriceVisitor
TOTAL_PRICE_VISITOR = TotalPriceVisitor()
CAR.accept(TOTAL_PRICE_VISITOR)
print(f"Total Price = {TOTAL_PRICE_VISITOR.total_price}")

Output

python ./visitor/client.py
Utility     :ABC-123-21
V8 engine   :DEF-456-21
FrontLeft   :GHI-789FL-21
FrontRight  :GHI-789FR-21
BackLeft    :GHI-789BL-21
BackRight   :GHI-789BR-21
Total Price = 4132

SBCODE Editor

<>

New Coding Concepts

Instance hasattr()

In the Visitor objects in the example use case above, I test if the elements have a certain attribute during the visit operation.

119
120
121
def visit(cls, element):
    if hasattr(element, 'price'):
        ...

The hasattr() method can be used to test if an instantiated object has an attribute of a particular name.

1
2
3
4
5
6
7
8
class ClassA():
    name = "abc"
    value = 123

CLASS_A = ClassA()
print(hasattr(CLASS_A, "name"))
print(hasattr(CLASS_A, "value"))
print(hasattr(CLASS_A, "date"))

Outputs

True
True
False

String expandtabs()

When printing strings to the console, you can include special characters \t that print a series of extra spaces called tabs. The tabs help present multiline text in a more tabular form that appears to be neater to look at.

abc    123
defg   456
hi     78910

The number of spaces added depends on the size of the word before the \t character in the string. By default, a tab makes up 8 spaces.

Now, not all words separated by a tab will line up the same on the next line.

abcdef  123
cdefghij        4563
ghi     789
jklmn   1011

The problem usually occurs when a word is already 8 or more characters long.

To help solve the spacing issue, you can use the expandtabs() method on a string to set how many characters a tab will use.

1
2
3
4
print("abcdef\t123".expandtabs(10))
print("cdefghij\t4563".expandtabs(10))
print("ghi\t789".expandtabs(10))
print("jklmn\t1011".expandtabs(10))

Now outputs

abcdef    123
cdefghij  4563
ghi       789
jklmn     1011

Summary

  • Use the Visitor pattern to define an operation to be performed on the elements of a hierarchical object structure.
  • Use the Visitor pattern to define the new operation without needing to change the classes of the elements on that it operates.
  • When designing your application, you can provision for the future possibility of needing to run custom operations on an element hierarchy, by implementing the Visitor interface in anticipation.
  • Usage of the Visitor pattern helps to ensure that your classes conform to the single responsibility principle due to them implementing the custom visitor behavior in a separate class.