Master the Visitor Design Pattern in Kotlin: Complexity Simplified

Table of Contents

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.

Pu La Deshpande Udyan (Pune okayama friendship park)

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:

  1. Visitor: Encapsulates the new operation you want to perform.
  2. 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:

  1. The method being invoked
  2. 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.
Kotlin
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.
Kotlin
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 a Visitor and let it perform its operation.
Kotlin
interface Visitable {
    fun accept(visitor: Visitor)
}

Concrete Visitable Classes

  • Implements the Visitable interface.
  • Provides the accept(visitor: Visitor) method implementation, which calls the appropriate visit() method on the visitor, passing itself as a parameter.
Kotlin
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.
Kotlin
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.

Kotlin
// 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.

Kotlin
// 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.

Kotlin
// 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.

Kotlin
// 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.

Kotlin
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.

Kotlin
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.

Kotlin
interface Resident {
    fun accept(visitor: Visitor)
}

Implement Concrete Elements (Residents)

Each type of resident implements the accept() method to interact with the visitor.

Kotlin
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.

Kotlin
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.

Kotlin
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.

Kotlin
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.

Kotlin
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..!

Skill Up: Software & AI Updates!

Receive our latest insights and updates directly to your inbox

Related Posts

error: Content is protected !!