The Power Of Kotlin Sealed Classes
At Lectra, an increasing number of backend applications use Kotlin. The need of using a strongly typed language came from a simple fact: it drastically reduces bugs, since everything your compiler/IDE does for you is something you don't have to think about. Then you can simply focus on the domain code, and more importantly what your application is supposed to do. Kotlin was designed with this idea as ground rule.
Kotlin bundles a very powerful construct: sealed classes. We will only focus on one of its aspects in this article.
A sealed class can be considered as a type with a finite set of subtypes. It is simply defined this way:
sealed class BookingOutcome
This class is by default abstract, and cannot be instantiated.
Now we can define sub-classes. However you don't have to think about these sub-classes as inheritance (even if it is generated this way in the bytecode) but more like a type catalog, a type enum, or a kind of distinct union types (as in TypeScript)
sealed class BookingOutcome
class RoomBooked(key: Key, bookingRange: ClosedRange<Instant>) : BookingOutcome()
object RoomNotAvailable : BookingOutcome()
object HotelClosed : BookingOutcome()
Note the () constructor for each of these types. Now we have a concrete class to represent a booking, two singleton instances to improve the expressiveness of the different choices. You could also choose to use a data class if needed.
Then, here is the interesting part. In Kotlin, the compiler/IDE is aware of this sealed class hierarchy exhaustiveness. It reveals all its power when you provide a service for your consumers:
interface BookingService {
fun bookRoom(roomId: RoomID): BookingOutcome
}
Next, for the fun part. For every consumer of your service, the sealed class BookingOutcome becomes the contract. Meaning every caller will benefit from the compiler checks whether they exhaustively planned (or not) to handle all your possible outcomes!
For instance :
val outcome = bookingService.bookRoom(RoomID("roomIwant"))
when (outcome) {
}
The Kotlin compiler bundled with IntelliJ will now come to the rescue! When combined with the when keyword, it is a very strong way to enforce the way an API is used.
('when' is Kotlin's version of Java's 'switch' , with a pattern matching-like syntax)
It even does the beginning of the work for the consumer:
More importantly, it will not compile if the branches are not complete, or without a willingly declared else -> branch!
Since it is a "pattern matching"-like friendly language, it will even auto smartcast on the right side.
We have only seen one aspect of sealed classes in this article, but it should really become a reflex when you think about enumerations that carry any kind of useful information for your services/APIs or any kind of type expressiveness you want to achieve.
Kotlinly yours ;)