Purpose of the pattern
Separation of an algorithm from the object structure. Think of a Visitor as someone who visits multiple places and does something different depending on the place. This means we need to modify Visitor instead of the place itself.
It’s mostly used to implement new things that don’t make sense inside an object but are required for the new feature to work.
What do we get from that?
- Open/Closed: we can add new algorithms without changing the object structure.
- Single Responsibility: each class is responsible for a different behavior.
- Extension of a class with little to no modification to it.
- Avoidance of polluting the object class.
The biggest downside is that you must update all Visitors when a class is added or removed from the hierarchy.
Implementation
In the diagram, we have our Visitor
that has methods for visiting concrete implementations of Element
, therefore it’s dependent on each Element
implementation.
At the same time, Element
is dependent on a Visitor
interface. We’re doing this to avoid checking the type of Element
each time we want to use Visitor
instead, we’ll accept
Visitor
and use the right method inside Element
. The method will be a 1-liner that calls the right Visitor
method.
Moreover, we’re able to create multiple ConcreteVisitors
that can do different things.
Example
Your task is to add an Export to CSV feature to a body health system. Your manager is also thinking about adding other export options. Adding function to the Body would look at least strange it should be separated.
It’s a perfect use case for Visitor because we can add new export options easily without interfering with much of the body health system. Here’s how we’ll structure our code:
Job Offers
In real app BodyPart
would also have other functions, but we want to keep things simple in the example. Let’s start by coding BodyParts
along with Visitor
interface:
interface Visitor {
fun visitEye(eye: Eye)
fun visitMouth(mouth: Mouth)
}
interface BodyPart {
fun accept(visitor: Visitor)
}
class Eye(val color: String) : BodyPart {
override fun accept(visitor: Visitor) {
visitor.visitEye(this)
}
}
class Mouth(val size: Int) : BodyPart {
override fun accept(visitor: Visitor) {
visitor.visitMouth(this)
}
}
Now, Visitor
implementations have access to Mouth
and Eye
. Let’s start coding ExportToCSVVisitor
:
class ExportToCSVVisitor : Visitor {
override fun visitEye(eye: Eye) {
println("csv eye: ${eye.color}")
}
override fun visitMouth(mouth: Mouth) {
println("csv mouth: ${mouth.size}")
}
}
Again, for simplicity Eye
and Mouth
have 1 parameter and we’ll just print the info.
Here’s how to use it:
fun main() {
val visitor: Visitor = ExportToCSVVisitor()
val bodyParts = listOf(
Eye("blue"),
Eye("borwn"),
Mouth(20),
)
bodyParts.forEach { it.accept(visitor) }
// csv eye: blue
// csv eye: borwn
// csv mouth: 20
}
There is no need to know the type of BodyPart
when using Visitor
methods because all of them have accept
function that chooses the right Visitor
function.
Thanks for reading! Please clap if you learned something, and follow me for more!
Learn more about design patterns:
Based on the book:
“Wzorce projektowe : elementy oprogramowania obiektowego wielokrotnego użytku” — Erich Gamma Autor; Janusz Jabłonowski (Translator); Grady Booch (Introduction author); Richard Helm (Author); Ralph Johnson (Author); John M Vlissides (Author)
This article is previously published on proandroiddev.com