Introduction
The visitor pattern is a design pattern that allows for adding new operations to a collection of objects, without, and that is important, modifying the objects themselves.
It looks like this:
There are two main functionalities:
- The visit functionality as implemented in the Visitor interface. This ensures that Elements can be visited.
- The accept functionality as implemented in the Element interface. This ensures that when an Element is visited, an operation can be performed.
Implementation in Python
Since we will be using classes before they are defined, we need this at the head of our program:
from __future__ import annotations
The Visitor class
We will start by defining the Visitor class:
class Visitor:
def visit_person(self, person: Person):
pass
def visit_organization(self, organization: Organization):
pass
In our example, a visitor can only visit persons and organizations. Note that because of our preliminary import we are able to refer to the Person and Organization classes before we define them.
The Element class
In order for our persons and organizations to accept a visitor, they need an accept() method, provided by this class:
class Element:
def accept(self, new_visitor: Visitor):
pass
The Person class
Now we can see how this accept method actually works in the Person class:
class Person(Element):
_name: str = None
_email: str = None
def __init__(self, name: str, email: str):
self._name = name
self._email = email
def accept(self, new_visitor: Visitor):
new_visitor.visit_person(self)
@property
def name(self) -> str:
return self._name
@property
def email(self) -> str:
return self._email
Some notes:
- A Person in this example just has a name and an email
- The accept() method has a visitor as its parameter and a reference to the current instance which is of type Person. That is why we can pass self to visit_person. This is the actual visiting the visitor does.
- Note the two @property decorators. A property is a kind of virtual attribute, each time we access person.name for example, the name() method will be called and its return value used. This is a very easy way to make read-only attributes. An excellent article on this subject you can find here.
The Organization class
The Organization class is unsurprisingly very similar to the Person class:
class Organization(Element):
_name: str = None
_address: str = None
def __init__(self, name: str, address: str):
self._name = name
self._address = address
def accept(self, new_visitor: Visitor):
new_visitor.visit_organization(self)
@property
def name(self)->str:
return self._name
@property
def address(self)->str:
return self._address
Defining the visitor
Now we can define the visitor with the class EmailVisitor:
class EmailVisitor(Visitor):
def visit_person(self, person: Person):
print(f"Sending email to {person.name} at {person.email}")
def visit_organization(self, organization: Organization):
print(f"Sending mail to {organization.name} at {organization.address}")
This is an implementation of the Visitor class we defined in the beginning.
The functionality is rather limited, on purpose. All either function does is print out some message about an action. These message are of course different for different elements.
Testing time
Time to test our code:
if __name__ == "__main__":
elements: list[Element] = [Person("Alice", "alice@example"),
Organization("Acme Inc.", "123 Main Street"),
Person("Bob", "bob@example.com")
]
visitor: Visitor = EmailVisitor()
for e in elements:
e.accept(visitor)
Line by line:
- We initialize an array of objects from classes which derive from Element
- Then we construct a Visitor, an EmailVisitor in this case.
- Next we iterator over all the elements in the array and visit them one by one.
Conclusion
As you can see, the implementation of this pattern is both elegant and painless, as well as flexible. That is painless for new implementations, I realize that may not be the case for existing codebases.
An extension would be to make visiting multithreaded and therefore threadsafe, but that will be the subject for another post.