Dive into the Power of Kotlin Sealed Interfaces

Dive into the Power of Kotlin Sealed Interfaces

Kotlin sealed interface
When Kotlin was first introduced, developers quickly fell in love with its powerful language features, including sealed classes. However, one thing still seems to be missing: sealed interfaces. At the time, the Kotlin compiler had no guarantees that interfaces would not be implemented in Java code, making it difficult to implement sealed interfaces in Kotlin.

But times have changed, and now starting with Kotlin 1.5 and Java 15, sealed interfaces are finally available. Using sealed interfaces, developers can create more robust and type-safe APIs, just like using sealed classes. In this blog post, we'll dive into the basics of Kotlin's sealed interfaces and explore how they can help you build better code. We'll cover everything from the basics of sealing interfaces to advanced techniques and best practices, so get ready to master this powerful new feature!

Basics of Kotlin Sealed Interfaces

Like sealed classes, sealed interfaces provide a way of defining a closed hierarchy of types in which all possible subtypes are known at compile time. This makes it possible to create more robust and type-safe APIs, while also ensuring that all possible use cases are covered.

To create a sealed interface in Kotlin, use the sealed modifier before the interface keyword. Here is an example:

sealed interface Shape {
    
    
    fun draw()
}

This creates a sealed interface called Shape with only one draw()method. Note that sealed interfaces can have abstract methods, just like regular interfaces. A sealed interface can only be implemented by classes or objects declared in the same file or in the same package.

Now, let's see how to use sealed interfaces in practice. Here is an example:

sealed interface Shape {
    
    
    fun area(): Double
}

class Circle(val radius: Double) : Shape {
    
    
    override fun area() = Math.PI * radius * radius
}

class Rectangle(val width: Double, val height: Double) : Shape {
    
    
    override fun area() = width * height
}

fun calculateArea(shape: Shape): Double {
    
    
    return shape.area()
}

In this example, we define a sealed interface Shape with one abstract area() method . We then define two classes that implement the Shape interface: Circleand Rectangle. Finally, we define a function calculateArea()called that takes a parameter of type Shape and returns the area of ​​that shape.

Since the Shape interface is sealed, we cannot implement it outside of the current file or package. This means that only Circleand Rectangleclasses can implement the Shape interface.

Sealed interfaces are especially useful when we want to define a set of related interfaces that can only be implemented by a specific set of classes or objects. For example, we can define a sealed interface Serializable called that can only be implemented by classes designed to be serializable.

Subtypes of sealed interfaces

To create a subtype of a sealed interface, use the sealed modifier before the class keyword as you would a sealed class. Here is an example:

sealed interface Shape {
    
    
    fun draw()
}

sealed class Circle : Shape {
    
    
    override fun draw() {
    
    
        println("Drawing a circle")
    }
}

sealed class Square : Shape {
    
    
    override fun draw() {
    
    
        println("Drawing a square")
    }
}

class RoundedSquare : Square() {
    
    
    override fun draw() {
    
    
        println("Drawing a rounded square")
    }
}
fun drawShape(shape: Shape) {
    
    
    when(shape) {
    
    
        is Circle -> shape.draw()
        is Square -> shape.draw()
        is RoundedSquare -> shape.draw()
    }
}

This example creates two sealed classes Circle and Square that implement the Shape interface, and a non-sealed class RoundedSquare that inherits from Square. Note that since RoundedSquare does not have any direct subtypes, it is not a sealed class.

Use sealed class interfaces in pattern matching

One of the main benefits of using When expressions with sealed interfaces (and sealed classes) is that they can be used to provide exhaustive pattern matching. Here is an example:

fun drawShape(shape: Shape) {
    
    
    when(shape) {
    
    
        is Circle -> shape.draw()
        is Square -> shape.draw()
        is RoundedSquare -> shape.draw()
    }
}

The function takes a Shapeas parameter and uses a when expression to call the appropriate draw()method based on the shape's subtype. Note that since Shapeis a sealed interface, whenthe expression is comprehensive, meaning that all possible subtypes are covered.
sealed class VS sealed Interface

Advanced Techniques and Best Practices

While sealed interfaces provide a powerful tool for creating type-safe APIs, there are some advanced techniques and best practices to be aware of when using them.

interface delegation

One technique available for sealing interfaces is interface delegation. This involves creating a separate class that implements the sealed interface and then delegating calls to the appropriate methods to another object. Here is an example:

sealed interface Shape {
    
    
    fun draw()
}

class CircleDrawer : Shape {
    
    
    override fun draw() {
    
    
        println("Drawing a circle")
    }
}

class SquareDrawer : Shape {
    
    
    override fun draw() {
    
    
        println("Drawing a square")
    }
}

class DrawingTool(private val shape: Shape) : Shape by shape {
    
    
    fun draw() {
    
    
        shape.draw()
        // additional drawing logic here
    }
}

In this example, we create two classes that implement the Shape interface CircleDrawerand SquareDrawer. We then created a class DrawingToolthat Shapetakes parameters and draw()delegates calls to methods to the shape. Note that DrawingTooladditional drawing logic that is performed after the shape is drawn is also included.

avoid subclassing

Another best practice to keep in mind when using sealed interfaces is to avoid subclassing as much as possible. While sealed interfaces can be used to create closed subtype hierarchies, it's usually better to use composition rather than inheritance to achieve the same effect.

For example, consider the following sealed interface hierarchy:

sealed interface Shape {
    
    
    fun draw()
}

sealed class Circle : Shape {
    
    
    override fun draw() {
    
    
        println("Drawing a circle")
    }
}

sealed class Square : Shape {
    
    
    override fun draw() {
    
    
        println("Drawing a square")
    }
}

class RoundedSquare : Square() {
    
    
    override fun draw() {
    
    
        println("Drawing a rounded square")
    }
}

While this hierarchy is closed and type-safe, it can also be inflexible if you need to add new types or behaviors. Instead, you can use composition to achieve the same effect:

sealed interface Shape {
    
    
    fun draw()
}

class CircleDrawer : (Circle) -> Unit {
    
    
    override fun invoke(circle: Circle) {
    
    
        println("Drawing a circle")
    }
}

class SquareDrawer : (Square) -> Unit {
    
    
    override fun invoke(square: Square) {
    
    
        println("Drawing a square")
    }
}

class RoundedSquareDrawer : (RoundedSquare) -> Unit {
    
    
    override fun invoke(roundedSquare: RoundedSquare) {
    
    
        println("Drawing a rounded square")
    }
}

class DrawingTool(private val drawer: (Shape) -> Unit) {
    
    
    fun draw(shape: Shape) {
    
    
        drawer(shape)
        // additional drawing logic here
    }
}

In this example, we create different shape classes, and a DrawingTool class that accepts a function that knows how to draw the shape. This approach is more flexible than using a closed subtype hierarchy because it allows you to add new shapes or behaviors without having to modify existing code.

Extended closed interface

Finally, it's worth noting that closed interfaces can be extended just like regular interfaces. This can be useful if you need to add new behavior to a closed interface without breaking existing code. Here is an example:

sealed interface Shape {
    
    
    fun draw()
}

interface FillableShape : Shape {
    
    
    fun fill()
}

sealed class Circle : Shape {
    
    
    override fun draw() {
    
    
        println("Drawing a circle")
    }
}

class FilledCircle : Circle(), FillableShape {
    
    
    override fun fill() {
    
    
        println("Filling a circle")
    }
}

In this example, we extend the Shape interface, adding a new FillableShapeinterface that includes a fill() method. Then we created a new FilledCircleclass that inherits from Circle and implements it FillableShape. This way we can add a new behavior ( fill()) to the Shape hierarchy without breaking existing code.
Sealed class Vs Interface

Closed class vs closed interface

Enclosing classes and enclosing interfaces are both Kotlin language features that provide a way to restrict the possible types of variables or function parameters. However, there are some important differences between the two.

A closed class is a class that can be extended by a limited number of subclasses. When we declare a class as sealed, it means that all possible subclasses of that class must be declared in the same file as the sealed class itself. This makes it possible to use subclasses of the enclosing class in when expressions, ensuring that all possible cases are handled.

Here is an example of a closed class:

sealed class Vehicle {
    
    
    abstract fun accelerate()
}

class Car : Vehicle() {
    
    
    override fun accelerate() {
    
    
        println("The car is accelerating")
    }
}

class Bicycle : Vehicle() {
    
    
    override fun accelerate() {
    
    
        println("The bicycle is accelerating")
    }
}

In this example, we declare a sealed class called Vehicle. We also define two subclasses of Vehicle: Car and Bicycle. Since Vehicle is sealed, any other possible subclasses of Vehicle must also be declared in the same file.

On the other hand, a sealed interface is an interface that can be implemented by a limited number of classes or objects. When we declare an interface as sealed, it means that all possible implementations of that interface must be declared in the same file or in the same package as the sealed interface itself.

Here is an example of a sealed interface:

sealed interface Vehicle {
    
    
    fun accelerate()
}

class Car : Vehicle {
    
    
    override fun accelerate() {
    
    
        println("The car is accelerating")
    }
}

object Bicycle : Vehicle {
    
    
    override fun accelerate() {
    
    
        println("The bicycle is accelerating")
    }
}

In this example, we declare a sealed class called Vehicle. We also defined Vehicletwo subclasses of : Carand Bicycle. Since Vehicle is sealed, any other possible subclasses of Vehicle must also be declared in the same file.

On the other hand, a sealed interface is an interface that can be implemented by a limited number of classes or objects. When we declare an interface as sealed, it means that all possible implementations of that interface must be declared in the same file or in the same package.

An important difference between a sealed class and a sealed interface is that a sealed class can have both state and behavior, whereas a sealed interface can only have behavior. This means that sealed classes can have properties, methods, and constructors, while sealed interfaces can only have abstract methods.

Another difference is that sealed classes can be extended by regular classes or other sealed classes, while sealed interfaces can only be implemented by classes or objects. A sealed class can also have a subclass hierarchy, whereas a sealed interface can only have a flat list of implementations.

Advantage

  1. Type safety: Sealed interfaces allow you to define a closed subtype hierarchy, which ensures that all possible use cases are covered. This helps catch errors at compile time, rather than runtime, making your code more robust and maintainable.

  2. Flexibility: Sealed interfaces can be used to define complex subtype hierarchies while still allowing you to add new types or behaviors without breaking existing code. This makes it easier for you to evolve your code over time without having to make massive changes.

  3. Improved API design: By using sealed interfaces, you can create more intuitive and expressive APIs that better reflect the domain you're working in. This helps make your code easier to read and understand, especially for other developers who may not be familiar with your codebase.

shortcoming

Learning curve : While sealed interfaces are a powerful feature, they can be somewhat difficult to understand and use properly. In particular, if you are not used to working with type hierarchies, it may take some time to become familiar with using sealed interfaces.
Complexity : As your codebase grows and becomes more complex, it can become more difficult to use sealed interfaces. This is especially true if you have a large number of subtypes or need to modify the hierarchy in significant ways.
Performance : Because sealed interfaces use type checking at runtime to ensure type safety, they may have a performance impact compared to other approaches such as using enums. However, for most applications, this effect is usually negligible.

in conclusion

Sealed interfaces are a powerful new feature in Kotlin that provide a type-safe way to define closed type hierarchies. By using sealed interfaces, you can create more robust and flexible APIs while ensuring that all possible use cases are covered. Remember to use interface delegation, avoid subclassing, and consider extending sealed interfaces where appropriate to take full advantage of this powerful new feature!

Guess you like

Origin blog.csdn.net/u011897062/article/details/130678582