5

Seal the Deal with Kotlin — A Look at Sealed Classes and Sealed Interfaces

 1 year ago
source link: https://medium.com/codex/seal-the-deal-with-kotlin-a-look-at-sealed-classes-and-sealed-interfaces-71a22545f26a
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
1*IUB_zAlSrcjpvIGXnJvMBw.jpeg

Two people shaking hands to seal a deal

Seal the Deal with Kotlin — A Look at Sealed Classes and Sealed Interfaces

How Sealed Classes and Interfaces can improve your coding experience

Published in
5 min readMay 30

If you’ve ever come across the sealed keyword in Kotlin, you might have wondered why it’s part of the language and how you can use it. In this article, I am going to discuss the reasoning behind the feature and also present some common use cases. If you’re coming from Java, you will also benefit from this article because with Java 17 sealed classes and interfaces were added to the language (later than Kotlin but still). The sealed keyword in Kotlin provides a valuable feature for developers by restricting inheritance and enabling exhaustive handling. It ensures a closed set of subclasses, facilitates safer code, and allows for expressive modeling of complex systems and APIs.

The sealed keyword has been part of Kotlin since its initial public release versions, however, it was only useable for classes, not interfaces. With Kotlin 1.5 (released in May 2021), two important improvements were added to the sealed feature. Join me to learn about all of this in the following sections.

What Is The Sealed Keyword And How To Use It

When we talk about sealed classes and interfaces, we discuss the idea of limiting class hierarchies in our code. Sealed classes can be used when we want to restrict the inheritance hierarchy to a limited set of subclasses. By marking a class as sealed, we ensure that all its subclasses are defined within the same compilation unit. This can be useful when we want to define a closed set of related classes with exhaustive handling, such as representing different states, events, or error cases in your application.

Let’s consider a first example here.

This is a simplified class hierarchy with an open class followed by two implementations as you probably know from hundreds of your own use cases. A reasonable usage of this hierarchy can be through a function that works on any kind of Mammal, i.e. the superclass in this hierarchy.

This code does not compile. It assumes that only Human and Cat need to be handled as subclasses which is true in the known context of our trivial application. However, we can’t know what kind of additional subclasses (Dog, Bear, Monkey, …) will exist at runtime, e.g. in various client applications using our module. The compiler complains with the following error message:

‘when’ expression must be exhaustive, add necessary ‘else’ branch

As the compiler cannot assure that all cases are covered in the when, it requires a trailing else case to handle all unknown scenarios. The following solves the issue:

Technically, of course, it’s not really possible to ever know all existing mammals (because there are too many 😮) but we have a very specific application making many assumptions about the world. We know we only ever want to deal with cats and humans so we can safely restrict our little hierarchy.

Applying the sealed keyword

Kotlin supports our specific use case by offering the powerful sealed keyword. Let’s apply it to the example above.

We have added the sealed keyword to the Mammal class and removed the unwanted else branch. The compiler is happy now since it knows that only two subclasses will ever exist. The sealed keyword prevents more subclasses to be created outside the current compilation unit which enables features such as the when clause from above that now covers all possible cases and doesn’t require the else branch.

Relation To Enum Type

One might think that sealed classes seem pretty similar to enums as the set of values for an enum type is also restricted. However, each enum constant can only exist exactly once, whereas a subclass of a sealed class can have many instances and, in addition, contain an arbitrary state. This is not the case for enums and thus limits their capabilities.

Facts About The Sealed Feature

  • A sealed class is abstract by itself and cannot be instantiated directly.
  • A sealed class may have abstract member functions and properties.
  • Besides classes, we can also make interfaces sealed. More about this later.
  • Subclasses of sealed classes and interfaces can only exist in the same package as the base class. The package may contain multiple files containing those subclasses.
  • Subclasses of sealed types can be defined as top-level or nested in other types.

What Changes With Kotlin Release 1.5

Before Kotlin 1.5, which was released in May 2021, the usage of sealed classes was still a bit restricted. We could only define subclasses of a sealed base class in the very same file which limited the freedom of a software development team. The other relevant limitation of the original sealed class feature was that the keyword could only be applied on a class but not an interface.

Both things were changed with Kotlin 1.5:

  1. Sealed interfaces can now be defined and
  2. All subclasses of sealed types can appear in any file within the same package of the base class

For the interested reader, you can find discussions about the topics linked in the related KEEP.

Sealed interfaces

As already mentioned before, sealed interfaces were not a thing before Kotlin 1.5 but got introduced because many people asked for it. A common use case is that a library needs to expose a certain interface but does not want external implementations to exist. By marking this interface as sealed, the developer gets exactly that. They may create implementations only in their module and users of the library won’t be allowed to create more subclasses. By that, you can continue hiding implementation details via interfaces in your module, but avoid external developers trying to reimplement that contract.

An important difference between (abstract) base classes and interfaces is that a class may implement multiple interfaces but can always ever extend a single class. Imagine a case where we need a class to implement more than one sealed supertype. This can only be solved via interfaces and was not possible before Kotlin 1.5 got released. Find one related discussion here.

To define a sealed interface, we simply add the sealed keyword to it as shown with a simple Error type here:

Use Cases For Sealed Keyword

You will find sealed classes used whenever types can be limited to a defined set of possibilities. This is often the case in things like state machines, application-specific error classes, event handling, or when we represent API versions in our code. As we learned, sealed classes make it easy to have exhaustive pattern matching using the when expression that all of the mentioned use cases benefit from. Another more specific use case of the sealed keyword is related to public APIs that on the one hand want to expose an interface in order to hide implementation details but on the other hand, want to avoid clients to reimplement this interface. Making such an interface sealed solves this.

Wrapping up

The sealed keyword is simple yet powerful and can be used for both classes and interfaces. Using the keyword helps us limit the subclasses for this sealed type and with that provides various benefits. It’s vital to know this keyword and understand when to apply it. I am sure you will find more than a single example in your non-trivial application code that would actually benefit from a sealed structure, which I can say based on my own experience.

If you want to read more of my articles, please find them here. Thanks for reading! 🤗


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK