Sealed classes are an integral feature of Kotlin that bring numerous advantages to modeling restricted class hierarchies. They are a powerful tool for domain modeling, allowing only a finite set of subclasses and preventing the creation of outside subclasses.
In conventional programming paradigms, handling exhaustive logic based on the possible states of a system can be cumbersome. With sealed classes, you gain the safety of exhaustive when expressions, enabling compiler checks for all possible cases. This is especially useful in Kotlin, where immutability and type safety are key design principles. Let's explore the mechanics and benefits of sealed classes through practical examples.
Defining Sealed Classes
To define a sealed class in Kotlin, use the sealed keyword before the class keyword. The subclasses of a sealed class must be defined within the same source file, although they can be in different classes or within the sealed class itself.
sealed class PaymentMethod {
object Cash : PaymentMethod()
data class CreditCard(val number: String) : PaymentMethod()
data class PayPal(val email: String) : PaymentMethod()
}
In this example, PaymentMethod represents a restricted class hierarchy with three subclasses: Cash, CreditCard, and PayPal. This enforces that only these three types can be used as a payment method.
Advantages of Using Sealed Classes
One significant advantage of using sealed classes over regular inheritance in Kotlin is the ability to exhaustively iterate over every subclass using a when expression. This guarantees that you handle every possible case or an explicit default, leading to safer and self-documenting code.
fun processPayment(payment: PaymentMethod) = when (payment) {
is PaymentMethod.Cash -> println("Processing cash payment")
is PaymentMethod.CreditCard -> println("Processing credit card payment")
is PaymentMethod.PayPal -> println("Processing PayPal payment")
// No need for an "else" clause since all cases are covered
}
This exhaustive pattern matching at compile time assists developers in anticipating all variations of subclasses, reducing the likelihood of runtime exceptions caused by unhandled cases.
Sealed Classes vs. Enum Classes
You might wonder whether to choose sealed classes or enum classes to represent a restricted set of hierarchies. While both offer similar properties by providing constraints, sealed classes are more flexible. They allow each type to have its own state and behavior, unlike enums, which only offer a finite set of constant values.
sealed class NetworkResponse {
data class Success(val data: String) : NetworkResponse()
data class Error(val error: Throwable) : NetworkResponse()
object Timeout : NetworkResponse()
}
We'll notice here how sealed classes allow modeling complex types where constants can have their own meanings and properties. This cannot be easily mimicked using enum classes.
Restrictions and Considerations
While sealed classes provide a lot of benefits, they do come with certain restrictions:
- All implementations and subclasses of a sealed class must reside in the same file.
- Sealed classes cannot be instantiated directly; you can only work with their subclasses.
- They do not provide serialization by default. Additional work is necessary if you need to serialize objects from sealed classes.
Conclusion
Sealed classes, when used appropriately, offer a robust means to express restricted class hierarchies, facilitating safer and more maintainable Kotlin applications. They neatly fill the gap between data integrity checks and hierarchies where you would need exhaustive handling. As you explore deeper into Kotlin and need custom classes to tightly control inherited types, Kotlin’s sealed classes will undoubtedly become an invaluable part of your toolkit.