Unmasking the Magic: Demystifying Double Dispatch and the Visitor Pattern in Kotlin

 Master single dispatch limitations and learn idiomatic Kotlin techniques to handle complex object interactions with ease.

Double Dispatch and the Visitor Pattern in Kotlin

Ever felt like your code was playing hide-and-seek with types at runtime? You want a specific interaction between two objects, but the language only seems to look at one of them. Welcome to the world of Single Dispatch, and the fascinating techniques we use to break free: Double Dispatch and its powerful cousin, the Visitor Pattern.

The Single Dispatch Conundrum

Most object-oriented languages, including Kotlin and Java, use Single Dispatch. This means when you call objectA.doSomething(objectB), the version of the method that runs is chosen based only on the runtime type of objectA (the receiver). The runtime type of objectB (the argument) is ignored; only its declared compile-time type matters.

Imagine a Vehicle hierarchy and a Road hierarchy:

interface Road
data object DirtRoad : Road
data object Highway : Road

interface Vehicle {
fun driveOn(road: Road) {
println("A generic vehicle drives on a generic road.")
}
}

class Car : Vehicle {
override fun driveOn(road: Road) {
// Even if 'road' is a DirtRoad, the compiler only sees it as 'Road'
println("A car drives on a generic road.")
}
}

Classic Double Dispatch: The Two-Step Dance

To solve this, we use a “flip-flop” mechanism. Instead of one object trying to guess the other’s type, they work together:

  1. The Vehicle calls a method on the Road.
  2. The Road calls a method back on the Vehicle, passing itself as this.
interface Road {
fun accept(car: Car)
fun accept(truck: Truck)
}

data object DirtRoad : Road {
override fun accept(car: Car) = println("Car kicks up dust on Dirt Road!")
override fun accept(truck: Truck) = println("Truck rumbles on Dirt Road.")
}

class Car : Vehicle {
override fun driveOn(road: Road) = road.accept(this) // The Flip!
}

The Kotlin Way: Sealed Types and when

While the classic approach works, it’s boilerplate-heavy. Note: Kotlin does not have native “Multiple Dispatch,” but we can simulate it elegantly using sealed hierarchies.

This is a language-assisted workaround. It’s not true dispatch (it’s type checking), but in practice, it achieves the same result with more safety.

sealed interface Road {
data object DirtRoad : Road
data object Highway : Road
}

class Car : Vehicle {
override fun driveOn(road: Road) {
when (road) {
is Road.DirtRoad -> println("Car vs Dirt")
is Road.Highway -> println("Car vs Highway")
}
}
}

The Trade-off: This approach is asymmetric. It works best when the Road hierarchy is stable (closed). If you add a new road type, you must update every when expression across all vehicles.

The Visitor Pattern: Double Dispatch for Operations

The Visitor Pattern is essentially “Double Dispatch as a Service.” It allows you to add new operations to an existing object structure without modifying the objects themselves.

For the following example, imagine we want to generate an Inspection Report. By using Generics (<R>), our Visitor can now return values!

// 1. The Visitor (The "Operation")
interface VehicleVisitor<R> {
fun visit(car: Car): R
fun visit(truck: Truck): R
}

// 2. The Elements (The "Data")
interface Vehicle {
fun <R> accept(visitor: VehicleVisitor<R>): R
}

class Car : Vehicle {
override fun <R> accept(visitor: VehicleVisitor<R>): R = visitor.visit(this)
}

// 3. Concrete Operation: Generating a Report
class SafetyReport : VehicleVisitor<String> {
override fun visit(car: Car) = "Car: Airbags Checked."
override fun visit(truck: Truck) = "Truck: Air Brakes Checked."
}

Frequently Asked Questions (FAQs)

Is Kotlin’s when better than the Visitor Pattern?

If you own the code and one hierarchy is stable, when is more idiomatic. If you are building a library where users need to add their own "inspections" without changing your Vehicle classes, use the Visitor Pattern.

Why do we say when is not "true" dispatch?

Dispatch is a polymorphic selection handled by the language runtime. when is a conditional check. While the result is similar, dispatch is generally more extensible in open systems.

Can a Visitor return a value?

Yes! By using generics like VehicleVisitor<R>, you can make your visitors return strings, integers, or even other objects, making the pattern much more useful for data processing.

Questions for You:

  • In your current project, do you have a hierarchy that is “stable” enough to benefit from sealed interfaces?
  • Have you ever struggled with instanceof checks in Java? How does Kotlin's when change your perspective on that?

📘 Master Your Next Technical Interview

Since Java is the foundation of Android development, mastering DSA is essential. I highly recommend “Mastering Data Structures & Algorithms in Java”. It’s a focused roadmap covering 100+ coding challenges to help you ace your technical rounds.

Comments

Popular posts from this blog

Stop Writing Massive when Statements: Master the State Pattern in Kotlin

Coroutines & Flows: 5 Critical Anti-Patterns That Are Secretly Slowing Down Your Android App

Code Generation vs. Reflection: A Build-Time Reliability Analysis