George Silva
28 Sep 2021
β’
5 min read
First of all, welcome! This is the first post in a series of posts about design patterns. Today we will talk about the visitor pattern, a simple abstraction that will help you decouple your domain objects from application objects and keep your code clean.
But what are design patterns?
Design patterns are merely abstractions of a set of ideas. They have a name, a type, and a general implementation - slightly different depending on each context.
Abstracting these ideas in a vocabulary and giving them proper names, allows us to refer to them and communicate these complex ideas with ease.
There are several design patterns used in object-oriented programming languages that help us to solve common problems while designing and writing software and are a powerful tool in every software developer toolbox.
Not all patterns apply to all languages and all paradigms. Sometimes a pattern will not make any sense in a dynamically typed language, but it will make a lot of sense in a statically typed language.
These ideas, or recipes can be applied to different contexts and suffer variations, but use this as a guide.
The visitor pattern is a behavioral pattern that uses a technique called double dispatch to simplify your software object hierarchies and keep implementation details of processes, separate from your domain logic.
From Wikipedia, Double Dispatch is a technique to pass messages between methods, with varying arguments and types.
Let's imagine a scenario, for a more concrete view:
books
, magazines
, articles
and t-shirts
.Sounds pretty easy, right?
Let's get started with would be a barebones implementation of our cool bookstore in Python
(this example uses dataclasses
because I don't want to complicate it with framework details):
# models.py
from dataclasses import dataclass
from typing import List
@dataclass
class Author:
name: str
lives_in: str
@dataclass
class Publisher:
name: str
company_number: str
@dataclass
class Book:
title: str
author: List[Author]
genre: str
@dataclass
class Magazine:
title: str
publisher: Publisher
issue: int
@dataclass
class Article:
title: str
authors: List[Author]
abstract: str
full_text: str
To solve our problem, we need to be able to convert our models
for our API integration to send this information to our e-commerce partner. There are some ways of achieving this, but the visitor pattern is a way of achieving this with very little modification to our business objects and avoiding coupling our algorithm to convert my data to json
separate from the domain objects.
The easiest thing we can do to solve our problem is to implement a generic to_json
method to each of our classes and be done with it. Like so:
# models.py
# ...
@dataclass
class Author:
name: str
lives_in: str
def to_json(self):
return {"name": self.name, "lives_in": self.lives_in}
@dataclass
class Publisher:
name: str
company_number: str
def to_json(self):
return {"name": self.name, "company_number": self.company_number}
# ... rest of the file
While this take is not a bad idea, it suffers from the following problems:
Ok, let's refactor this approach in a second object! It will be clearer and decoupled!
In this approach, we take the details of each conversion and stick it into a new converter object.
class ECommerceExporter:
def _book_to_json(self, item):
authors = [self.to_json(author) for author in item.authors]
return {"name": self.name, "authors": authors, "genre": item.genre}
def _author_to_json(self, item):
return {"name": item.name, "lives_in": item.lives_in}
def to_json(self, item):
if isinstance(item, Book):
return self._book_to_json(item)
if isinstance(item, Author):
return self._author_to_json(item)
# ... all the other objects
While the following implementation is somewhat naive (you can use a dict
like structure to simplify the to_json
method) - this is hard to maintain. And this will only work for this specific use case. If you have a different e-commerce
partner, will you need to repeat the to_json
code again and again. For large object hierarchies, this will be difficult.
We can DRY (do not repeat yourself) a bit and completely separate the algorithm
(JSON conversion) from our objects, allowing us even to create different algorithms and keep supporting these.
So, what is this all about? The idea behind the Visitor Pattern
is to implement a technique called Double Dispatching
, decoupling the identification of a type and the method it calls from another object. Weird, but effective.
The first thing we need to do for this to work is to create a basic, generic Visitor
. This object will determine the required interface for all algorithms or processes to operate on.
class BookstoreVisitor:
def visit_book(self, item):
raise NotImplementedError
def visit_author(self, item):
raise NotImplementedError
def visit_magazine(self, item):
raise NotImplementedError
We want that all Bookstore
visitors to follow the above pattern. Each visitor decides what to do when it visits one object and that it's only responsibility.
Our converted would be the following:
class ECommerceVisitor:
def visit_book(self, item):
authors = [self.visit_author(author) for author in item.authors]
return {"name": item.name, "authors": authors, "genre": item.genre}
def visit_author(self, item):
return {"name": item.name, "lives_in": item.lives_in}
Ok - so far so good. But how this connects with our business model objects? We need to make a slight change in them, for them to support any visitor. It's general application logic that can be reused for any purpose, it's not tied to a specific feature or requirement and easy to maintain.
Let's implement our accept
method that receives a visitor. This is the catch. The accept method is the double dispatch
, where we determine implementation based on the input arguments (the visitor itself) and the type of the object it's calling the visitor.
# models.py
from dataclasses import dataclass
from typing import List
@dataclass
class Author:
name: str
lives_in: str
def accept(self, visitor):
return visitor.visit_author(self)
@dataclass
class Book:
title: str
author: List[Author]
genre: str
def accept(self, visitor):
return visitor.visit_book(self)
With this implementation, we could create many types of visitors to operate on collections of different objects and perform different work within each.
If a new requirement comes, asking us to export this to another format - let's say XML
, all we need to do is create a separate XMLVisitor
.
Now, this is all good and fine, but how do I use this? Let's imagine that our application has a cronjob that sends all the data we have to our e-commerce partner.
import json
def get_data():
author1 = Author(name="george", lives_in="Floripa")
author2 = Author(name="Dude# 1", lives_in"Anywhere")
book1 = Book(name="Design Patterns are Cool", authors=[author1, author2], genre="Software Development")
article1 = Article(name="study about stuff", authors=[author2], abstract="xyz", full_text="this is my new cool article")
return [book1, article1]
def integrate_with_ecommerce(data):
visitor = ECommerceVisitor()
result = [item.accept(visitor) for item in data]
response = requests.post("https://large-ecommerce.xyz/create", data=json.dumps(result))
This is very useful to specialize behavior in complex large object-oriented hierarchies. Imagine that in the following case, it was one URL per object type. It would still be doable, with a new visitor and delegating to the visitor the call to each URL.
item
calls a method using a visitor as a parameter and that visitor is called from within the object, passing self as an argument. Two method calls to one another.There were used several resources in writing this:
George Silva
See other articles by George
Ground Floor, Verse Building, 18 Brunswick Place, London, N1 6DZ
108 E 16th Street, New York, NY 10003
Join over 111,000 others and get access to exclusive content, job opportunities and more!