When diving into the world of design patterns, one of the more intriguing patterns you’ll encounter is the Visitor Design Pattern. While it might not be as commonly used as patterns like Singleton or Factory, it becomes incredibly powerful when you need to operate on a hierarchy of objects.
At first glance, the Visitor Design Pattern can seem challenging. Its name might feel familiar, yet the technical concept can appear tricky to grasp. But don’t worry — once you understand its core principles, implementing it will be much easier than you expect.
In this guide, we’ll break down the Visitor Pattern step by step. We’ll start by understanding its fundamentals, explore its structure in detail, and then finish with practical examples to solidify your understanding.
Ready to simplify this complex pattern? Let’s dive in and master the Visitor Design Pattern in Kotlin..!
What Is the Visitor Design Pattern?
The Visitor Design Pattern is a behavioral design pattern. Its primary goal is to separate an algorithm from the object structure on which it operates. In short, it allows you to add new operations (or behaviors) to a set of classes without modifying their source code.
Imagine this: You’ve just arrived in a new city and you’re super excited to explore. You quickly hop onto Google and search for “must-visit places near me.” The results are full of options: a beautiful park with breathtaking views, a cozy restaurant that serves your favorite cuisine, and a renowned museum with tons of fascinating exhibits. You decide to visit all of them.
The next morning, you set out. First, you head to the park and take a bunch of photos because, well, it’s stunning. Then, you go to the restaurant and indulge in that delicious dish you’ve been craving. Finally, you make your way to the museum, grab a ticket, and check out some of the exhibits before catching a movie in the cinema hall.
Each place is different, and at each one, you do something unique based on what it is — take photos, eat, or watch a movie. But here’s the key: you didn’t change the park, the restaurant, or the museum. You simply experienced them in different ways.
This is where the Visitor Design Pattern comes into play.
In short, the Visitor Design Pattern is all about interacting with different objects (in this case, the park, restaurant, and museum) in different ways, but without changing the objects themselves. It’s like you, as the “visitor,” can do different activities based on the type of place (object) you’re visiting, but the places (objects) remain exactly the same.
How Does It Work?
In programming, this pattern allows you to add new operations (or behaviors) to objects without changing their internal code. This is important because often, we don’t want to go digging into existing code and modifying it when we want to add new features. Instead, we can add new operations externally.
This pattern involves two main components:
- Visitor: Encapsulates the new operation you want to perform.
- Visitable or Element: Represents the classes that the visitor will operate on.
The Visitor Design Pattern mainly relies on the double dispatching mechanism. To grasp double dispatching, it’s important to first understand single dispatching.
Single Dispatch
Most design patterns that use delegation rely on a feature called single dispatch. This means:
- The specific function that gets called is determined by the type of one object (the object the method is being called on).
- This is also known as a virtual function call, where the dynamic type (the actual type of the object at runtime) decides which function to execute.
In languages like Java or Kotlin, if you have a method in a superclass that is overridden in a subclass, the method that gets executed is determined at runtime based on the actual object type, not the reference type. This behavior, known as polymorphism, is an example of single dispatch because the method is chosen based on the type of a single object (the one calling the method).
Double Dispatch
The Visitor Design Pattern extends this concept by introducing double dispatch, where the method that gets called depends on two factors:
- The method being invoked
- The runtime types of two objects (e.g., the object being visited and the visitor object).
This means:
- Double dispatch is a mechanism where the function call depends on the runtime types of two objects instead of one.
- In the Visitor Design Pattern, double dispatch ensures that the correct function is executed based on the combination of the Visitor and the element being visited.
So, final thoughts on them:
- Single Dispatch: The method invoked depends on the method name and the type of the receiver (polymorphism).
- Double Dispatch: The method invoked depends on the method name and the types of two receivers (the element and the visitor). This allows adding operations to an object structure without modifying the structure itself.
Structure of the Visitor Design Pattern
There are five main players in this pattern:
Visitor
- An interface or abstract class defining operations to be performed on elements (Visitable objects) in the structure.
- Declares methods for performing specific operations on each type of element in the structure.
interface Visitor {
fun visitConcreteElementA(element: ConcreteElementA)
fun visitConcreteElementB(element: ConcreteElementB)
}
Concrete Visitor
- Implements the
Visitor
interface. - Defines specific behaviors for each type of element it visits.
- May maintain state or data relevant to the operations performed during visitation.
class ConcreteVisitor : Visitor {
override fun visitConcreteElementA(element: ConcreteElementA) {
// Specific operation for ConcreteElementA
}
override fun visitConcreteElementB(element: ConcreteElementB) {
// Specific operation for ConcreteElementB
}
}
Visitable (Element)
- An interface or abstract class representing elements that can be visited by a
Visitor
. - Declares an
accept(visitor: Visitor)
method, enabling the element to accept aVisitor
and let it perform its operation.
interface Visitable {
fun accept(visitor: Visitor)
}
Concrete Visitable Classes
- Implements the
Visitable
interface. - Provides the
accept(visitor: Visitor)
method implementation, which calls the appropriatevisit()
method on the visitor, passing itself as a parameter.
class ConcreteElementA : Visitable {
override fun accept(visitor: Visitor) {
visitor.visitConcreteElementA(this)
}
}
class ConcreteElementB : Visitable {
override fun accept(visitor: Visitor) {
visitor.visitConcreteElementB(this)
}
}
Object Structure
- A container (e.g., array, list, or set) that holds the elements to be visited.
- Provides a way to traverse its elements and allows a
Visitor
to access and operate on each element.
class ObjectStructure(private val elements: List<Visitable>) {
fun accept(visitor: Visitor) {
elements.forEach { it.accept(visitor) }
}
}
In short, here’s how they work together:
- The Visitor defines operations for each element type.
- The ConcreteVisitor implements those operations.
- Elements (via
accept()
) allow the Visitor to perform operations on them without modifying their code. - The ObjectStructure organizes and manages the elements, letting the Visitor traverse and interact with them.
Let’s go further and implement a simple example using the Visitor design pattern to handle different types of shapes, such as Circle and Rectangle.
Define the Visitor interface: This will declare the visit()
method for each concrete shape.
// Visitor interface
interface ShapeVisitor {
fun visit(circle: Circle)
fun visit(rectangle: Rectangle)
}
Create ConcreteVisitor: We’ll define a concrete visitor that performs an operation on the shapes.
// ConcreteVisitor
class AreaCalculator : ShapeVisitor {
override fun visit(circle: Circle) {
println("Calculating area of circle: π * ${circle.radius}^2")
val area = Math.PI * circle.radius * circle.radius
println("Area: $area")
}
override fun visit(rectangle: Rectangle) {
println("Calculating area of rectangle: ${rectangle.width} * ${rectangle.height}")
val area = rectangle.width * rectangle.height
println("Area: $area")
}
}
Define the Visitable interface: This will declare the accept()
method, which allows a visitor to visit the object.
// Visitable interface
interface Shape {
fun accept(visitor: ShapeVisitor)
}
Create ConcreteVisitable classes: These are concrete shapes like Circle
and Rectangle
that implement the Shape
interface and define the accept()
method.
// ConcreteVisitable classes
class Circle(val radius: Double) : Shape {
override fun accept(visitor: ShapeVisitor) {
visitor.visit(this)
}
}
class Rectangle(val width: Double, val height: Double) : Shape {
override fun accept(visitor: ShapeVisitor) {
visitor.visit(this)
}
}
Client code: This is where we use the visitor pattern. We create the shapes and use the AreaCalculator
to perform operations.
fun main() {
val shapes: List<Shape> = listOf(Circle(5.0), Rectangle(4.0, 6.0))
// Create the visitor
val areaCalculator = AreaCalculator()
// Visit each shape and calculate the area
shapes.forEach { shape ->
shape.accept(areaCalculator)
}
}
//////////// OUTPUT //////////////////
Calculating area of circle: π * 5.0^2
Area: 78.53981633974483
Calculating area of rectangle: 4.0 * 6.0
Area: 24.0
Now, you might be thinking, ‘What is the benefit of doing this?‘ Yes, there are several:
- Open for extension, closed for modification: You can add new operations (visitors) without modifying the existing code for shapes.
- Separation of concerns: The logic for operations is kept separate from the objects, making it easier to manage and extend.
In this way, the Visitor pattern helps you add new functionalities to existing classes without changing their code. It’s especially useful when you need to perform different operations on a group of objects that share a common interface.
Real-world scenario: Campaigner visiting voters
This year has been a whirlwind of elections, including the US Presidential Election. On June 9, 2024, Narendra Modi was sworn in for his third term as Prime Minister of India. Election season is always full of energy, with promises aimed at addressing the needs of all sections of society.
Imagine a campaign in full swing: a candidate going door-to-door, tailoring their message for each voter. To high-income groups, they promise tax cuts to fuel growth. For low-income families, the focus is on welfare programs to improve daily life. For middle-income voters, the emphasis is on policies that ensure economic stability.
This adaptability mirrors the Visitor Design Pattern. It allows you to add new strategies or actions — just like a campaigner fine-tuning their pitch — without altering the core structure. It’s flexible, efficient, and perfect for managing change.
Let’s implement the Visitor Design Pattern in Kotlin to see how it works.
Define the Visitor
Interface
This represents the actions a Campaigner can take for different types of residents.
interface Visitor {
fun visit(highIncome: HighIncome)
fun visit(lowIncome: LowIncome)
fun visit(mediumIncome: MediumIncome)
}
Define the Resident
Interface
This allows residents to accept a visitor.
interface Resident {
fun accept(visitor: Visitor)
}
Implement Concrete Elements
(Residents)
Each type of resident implements the accept()
method to interact with the visitor.
class HighIncome : Resident {
override fun accept(visitor: Visitor) {
visitor.visit(this) // Calls the visitor's method for HighIncome
}
fun incomeGroup() = "High Income Group"
}
class LowIncome : Resident {
override fun accept(visitor: Visitor) {
visitor.visit(this) // Calls the visitor's method for LowIncome
}
fun incomeGroup() = "Low Income Group"
}
class MediumIncome : Resident {
override fun accept(visitor: Visitor) {
visitor.visit(this) // Calls the visitor's method for MediumIncome
}
fun incomeGroup() = "Medium Income Group"
}
Implement Concrete Visitor
(Campaigner)
The campaigner performs different actions for each type of resident.
class Campaigner : Visitor {
override fun visit(highIncome: HighIncome) {
println("Campaigning to ${highIncome.incomeGroup()} with promises of tax cuts.")
}
override fun visit(lowIncome: LowIncome) {
println("Campaigning to ${lowIncome.incomeGroup()} with promises of welfare programs.")
}
override fun visit(mediumIncome: MediumIncome) {
println("Campaigning to ${mediumIncome.incomeGroup()} with promises of economic growth.")
}
}
Client Code (main()
)
The client creates residents and a campaigner, then executes the campaign.
fun main() {
// List of residents
val residents: List<Resident> = listOf(
HighIncome(),
LowIncome(),
MediumIncome()
)
// Create a campaigner (visitor)
val campaigner = Campaigner()
// Campaigner visits each resident
residents.forEach { resident ->
resident.accept(campaigner) // Accept allows double dispatch
}
}
///////////////////// OUTPUT ///////////////////////////////
Campaigning to High Income Group with promises of tax cuts.
Campaigning to Low Income Group with promises of welfare programs.
Campaigning to Medium Income Group with promises of economic growth.
First, let’s take a look at how Double Dispatch works here:
First Dispatch: The Resident
objects (HighIncome
, LowIncome
, MediumIncome
) call the accept()
method and pass the Campaigner
object to it.
resident.accept(campaigner) // First dispatch: HighIncome, LowIncome, MediumIncome
Second Dispatch: Inside the accept()
method, the visitor (Campaigner
) calls the appropriate visit()
method based on the specific type of the Resident
object.
campaigner.visit(this) // Second dispatch: visit() based on Resident type
This allows the Campaigner
(visitor) to perform different actions depending on the concrete type of Resident
(HighIncome
, LowIncome
, MediumIncome
).
Other code is quite self-explanatory, so let’s take a look at the benefits we gain from using the Visitor Design Pattern.
Stable Resident Structure:
You can add new behaviors, like a new type of campaigner, without changing the existing Resident hierarchy.
Flexible Behavior:
To introduce a new visitor, like a Surveyor, you simply create a new class — no need to modify the Resident classes at all.
Encapsulation of Logic:
Each visitor keeps its own behavior separate, meaning the Resident classes stay clean and focused on their core responsibilities.
This pattern works great when behaviors need to change often, but the overall structure of the objects remains stable.
We’ve talked a lot about the benefits, but the Visitor Pattern has its downsides too. Let’s take a look at them.
- Inflexibility: Adding new element types requires changes to the visitor interface and all its implementations.
- Coupling: Tight coupling between visitors and element classes.
- Complexity: The pattern can increase complexity when there are numerous element types and visitors.
Conclusion
The Visitor pattern is a powerful way to add operations to object structures without modifying them. While it might seem complex initially, once you break it down (like we did here), its elegance becomes apparent. It’s especially useful when working with stable hierarchies that require diverse operations.
By applying this pattern thoughtfully in Kotlin, we can write clean, maintainable, and extensible code.
Keep Discovering with Visitor Magic..!