object-oriented programming visitor pattern

Sealed classes in Kotlin and Java 21 provide an alternative to the Visitor pattern for handling different implementations of classes and interfaces known at compile time. By using a sealed interface, switch-case statements can be made exhaustive without needing a default branch. This approach decouples elements like Item, Box, and Container from other logic and allows for the addition of new calculation logic or item types without modifying the structure. Although there are some caveats, sealed classes offer a simpler and more flexible solution compared to the Visitor pattern.

What are sealed classes and how can they serve as an alternative to the Visitor pattern in Kotlin and Java 21?

Sealed classes in Kotlin and Java 21, where different implementations of classes and interfaces are known at compile time, can serve as an alternative to the Visitor pattern. By using a sealed interface, a switch-case doesn’t need a default branch since all implementations are known at compile-time. This approach decouples elements like Item, Box, and Container from any other logic, such as calculation or visitation, allowing new calculation logic or item types to be added without modifying the structure and by adding a new class.

In this article, we shed light on how the functionality of sealed classes and exhaustive pattern matching can serve as an alternative solution to the Visitor pattern in Kotlin and Java 21. The information provided in this article applies to both Java 21 and Kotlin.

The Structure of the Article

This article is divided into four parts:

  • A brief overview of a problem
  • An uncomplicated approach to solve the problem without using patterns or sealed classes
  • A look at what the Visitor pattern is and how it can resolve the problem
  • How sealed classes and interfaces can also provide a solution to the problem

Problem Overview

To keep things straightforward, let’s say we’re attempting to design a program to manage Boxes and Containers. A container is an entity that can contain boxes as well as other containers.

Requirements

The program requirements include the ability to create containers and boxes, placing boxes inside containers in a recursive pattern, calculating the total weight of our ensemble, and printing the composition of our ensemble. Additionally, the program needs to have the capacity to implement more types of calculations (such as calculating total price) and more types of items (such as a shiny box).

Boxes have a weight parameter, and Containers have a weight of 2 plus the weight of what’s inside them.

A Simple Approach

This problem screams for the use of object orientation and for-loops to explore through containers and boxes to calculate and print their contents. Creating two record classes: Box and Container(Box[]), a weight function is added to the Item interface and implemented in Container and Box.

The Item interface can have boxes and containers of other items which is essentially a Composite pattern. The structure of the items can be printed in a similar process, but instead of weight calculation and returning an int, we can print a structure recursively.

However, the method has its advantages and drawbacks:

  • Advantage: More types of items (such as ShinyBox) can be added by only adding code
  • Disadvantage: The code of Box and Container is coupled with the code for calculating their weights and printing their structure. This becomes problematic if we want to add more functionality like calculating total price.
  • Disadvantage: In order to add the functionality for the printing the structure of the boxes, we had to modify the code and structure of the items.

A new requirement for printing an XML for our structure would entail modifying the contents of the classes to add this feature.

The Visitor Pattern

The Visitor pattern presents an alternative solution to these problems.

Why Visitor Pattern Exists

By changing the structure of our code, we can add more functionality (like adding more types of items and more types of calculation) by mostly adding code. However, Java, Kotlin, and many other languages cannot determine the subtype of the Item inside the for-loop since it is an Item and dispatch it to the proper function, so they want a function that accepts an Item to solve this problem.

Using The Visitor Pattern

Implementing the visitor pattern implies removing the calculate(Item) function and making each implementation of an item to be responsible to call its own function on the calculator. This method of calling functions is called double-dispatch, which is the main method for the visitor pattern.

By using the visitor pattern, we can now add a new XMLVisitor class by implementing the visitor and let the logic of XML calculation be completely decoupled from the items. However, this pattern is known for its complexity. The method of double-dispatching makes things hard to understand or follow.

Sealed Classes

Kotlin and Java 21 introduce sealed classes and interfaces, in which different implementations of classes and interfaces are known at compile time. By using a sealed interface, we can have a switch-case that doesn’t need a default branch, since all the implementations of the class are known at compile-time.

This approach has many advantages. For instance, Item, Box, and Container are decoupled from any other logic such as calculation or visitation. If we add a new calculation logic (a Visitor in the visitor pattern), we can do so without modifying our structure and adding a new class. If we add a new type of Item, we can do so by adding the code for its calculation logic in the calculators and get a compiler error until we have updated all the calculators (same as visitor pattern).

However, some caveats remain. For example, there is a repetition of the switch-case in each calculator (Visitor) which is comparable to the repetition of the visit function in each Item in the Visitor pattern. The Visitor pattern has the advantage of creating custom visit functions for each item if needed. Also, switch-cases can still have a default branch (which it shouldn’t) and become compile-time unsafe, something that the Visitor pattern can still prevent.

Final Notes

To conclude, we have explored different ways to solve a boxing problem. We have seen how a simple solution can have a coupling drawback and how the visitor pattern can fix it while having more complexity. Then, we explored sealed classes and how they can solve the problem by restricting switch-cases for sealed classes and making them exhaustive.

  • Sealed classes in Kotlin and Java 21 provide an alternative to the Visitor pattern for handling different implementations of classes and interfaces known at compile time.
  • Using a sealed interface allows for switch-case statements to be made exhaustive without needing a default branch.
  • Sealed classes decouple elements like Item, Box, and Container from other logic, allowing for the addition of new calculation logic or item types without modifying the structure.
  • Sealed classes offer a simpler and more flexible solution compared to the Visitor pattern.
  • The article discusses the problem overview, a simple approach to solving it, the use of the Visitor pattern, and the advantages and drawbacks of sealed classes.
  • Sealed classes in Kotlin and Java 21 restrict switch-cases and make them exhaustive, but there are some caveats such as repetition of switch-cases and the possibility of default branches becoming compile-time unsafe.