The provided content discusses the use of sealed classes and sealed interfaces in Kotlin to enhance code safety by converting potential runtime errors into compile-time errors, thereby improving the robustness of dynamic dispatch implementations.
Abstract
The article delves into the practical benefits of using Kotlin's sealed hierarchies to emulate dynamic dispatch safely. It illustrates how sealed classes can prevent runtime errors by ensuring that all possible cases are handled at compile time. The author uses a scenario where extending an existing arithmetic expression hierarchy with a new Product class could lead to runtime errors due to an incomplete when expression in a simplify function. By sealing the hierarchy, developers are forced to address all cases, thus avoiding such errors. The article argues that sealed hierarchies provide a provable way to prevent errors, independent of test coverage or human vigilance, and are particularly useful when direct modification of class hierarchies is not feasible or would lead to poor design, such as creating a god object.
Opinions
The author suggests that responsible developers should avoid over-engineering and focus on current requirements, which in this case led to a simple implementation of arithmetic expressions.
The article implies that a common scenario in software development is working with legacy code where the original developers are no longer available, and firsthand knowledge of the codebase is lacking.
It is conveyed that even with mechanisms like test coverage in place, runtime errors can still occur due to unforeseen changes in the codebase.
The author expresses that sealed hierarchies are a powerful feature in Kotlin that can guarantee the absence of a class of errors related to dynamic dispatch, thus providing a safer and more maintainable codebase.
The article emphasizes the importance of grouping related business behaviors and segregating them from unrelated behaviors to avoid creating god objects, which are difficult to maintain.
It is suggested that sealed hierarchies offer a practical balance between the safety of compile-time checks and the flexibility of dynamic dispatch, making them a valuable tool in software development.
Safely Emulating Dynamic Dispatch
How you can use sealed classes and sealed interfaces to safely emulate dynamic dispatch by turning runtime errors into compile-time errors.
— — — — — — — — — — — — — — —
THE CURRENT VERSION OF THIS ARTICLE IS PUBLISHED HERE.
This article is part of the Kotlin Primer, an opinionated guide to the Kotlin language, which is indented to help facilitate Kotlin adoption inside Java-centric organizations. It was originally written as an organizational learning resource for Etnetera a.s. and I would like to express my sincere gratitude for their support.
In the previous chapter, we introduced sealed hierarchies and showed how they made it unnecessary to add an else branch to a when expression. This seems like a nice little perk, but it is not immediately apparent that it has real practical benefit in the real world.
Let’s take a closer look at the code we introduced in the previous chapter, but without the hierarchy being sealed.
Nothing out of the ordinary here. This code represents the backbone of some business requirement involving arithmetic expressions — probably a calculator of some sorts. Let’s say that, at the time of implementation, all that was needed to implement the customers requirements was sums of numbers, with the possibility of an invalid number being inputted as well. Since the code was written by responsible developers who don’t over-engineer, that’s all they implemented.
This code has been in production for 10 years, and has not been modified the entire time. The developers who wrote it are long dead (read: moved up to management) and nobody on the team has any firsthand knowledge about which parts are involved — a very common scenario.
Along comes the 11th year, and with it a new budget, part of which is dedicated to expanding existing functionality to allow receiving arithmetic expressions from a 3rd party API. “We’ve already implemented simple arithmetic expressions”, says the customer, “so it shouldn’t be difficult, right?”. Yeah. Sure.
Anyway, the 3rd party API is more sophisticated than ours, and also supports multiplication. So, after doing a quick search and finding the Expression interface and its implementations, you do the obvious thing — add a Product subclass, and implement your thing.
Unfortunately, you missed the simplify function above, because you didn’t know that it was there, and since you didn’t know it was there, you also didn’t know it was covered by tests, so you didn’t update those either. The tests only tested the subclasses that were there before, so everything is green, and off we go to prod.
And boom, simplify starts throwing runtime errors. Congratulations, you just broke the app.
Now, naturally, this is a simple example, and any programmer worth their salt probably wouldn’t make a mistake if things were this simple. There are usually multiple mechanisms (such as tracking test coverage) in place to prevent these errors from happening. But it’s easy to imagine a much more convoluted scenario where things like this can happen more easily. The point is, with this design, you can never guarantee it won’t. There is always the possibility, however remote, that a problem like this will appear.
And that’s precisely where sealed hierarchies come in. If you make the hierarchy sealed, and add a Product class, you are guaranteed that this mistake cannot happen, because the code won’t compile — the when expression in simplify stops being exhaustive, and the compiler lets you know.
Go ahead, try running it:
Also, no tests are necessary, so you save time on that as well.
Emulating Dynamic Dispatch
These types of errors appear when dynamic dispatch is emulated manually. If the simplify method was declared inside the classes, i.e. directly on Expression, this problem could never have appeared because the compiler would force you to implement the simplify method when you defined the Product class:
However, this is often not possible to do in a real-world application. For one, you might not be in control of the Expression hierarchy, but even if you were, there is another problem.
Sane design principles dictate that related (business) behaviors should be grouped together, and segregated from other business behaviors. For example, if you created a DatabaseTable class, you would probably not want it to implement a render method, or an exportAndSaveToFileSystem method — those should probably be implemented in completely different modules. If you crammed every behavior that needs to be dispatched dynamically into a class, it would grow without bounds and soon become a god object, which is a nightmare to maintain.
Sealed hierarchies solve this problem by allowing us to implement the equivalent of dynamic dispatch, while still retaining the safety of actual dynamic dispatch. Consequently, they prevent a whole class of errors from ever making it to production, and do so provably, independent of test coverage, correct design, or human thoroughness.