Abstracting Kotlin Sealed Classes

June 4, 2019

Things tend to be similar. Cars and bikes are different for sure but both have wheels, engines, exhaust systems and so on. Such similarities between models are usually described using basic polymorphism in virtual domain modeling.

Kotlin makes this process a bit more pragmatic using sealed class declarations. Specifying type hierarchies using sealed classes is simple, but what about declaring common properties? Fortunately or not there are multiple ways to achieve this — partially because of the Java baggage. Which one is the best?

Domain

There is a drawing system. We want to declare shapes we are able to draw.

sealed class Shape {
    data class Circle(val radius: Int, val color: Int) : Shape()
    data class Square(val size: Int, val color: Int) : Shape()
}

This is a fine piece of code but Shape drawing is a bit messy.

fun draw(shape: Shape) {
    val paint = Paint(antialias = true, color = when (shape) {
        is Shape.Circle -> shape.color
        is Shape.Square -> shape.color
    })

    when (shape) {
        is Shape.Circle -> canvas.drawCircle(shape.radius, paint)
        is Shape.Square -> canvas.drawSquare(shape.size, paint)
    }
}

The Paint part looks weird. The color is common for both shapes but we are forced to dance around types to make it work. Let’s change that.

Refactoring

val

The basic refactoring is moving the code into the type declaration. This way we can use it everywhere.

sealed class Shape {
    data class Circle(val radius: Int, val color: Int) : Shape()
    data class Square(val size: Int, val color: Int) : Shape()

    val commonColor = when (this) {
        is Circle -> color
        is Square -> color
    }
}

There is a number of issues with this approach.

open val

We can define a Shape-level open property.

sealed class Shape(open val color: Int) {
    data class Circle(val radius: Int, override val color: Int) : Shape(color)
    data class Square(val size: Int, override val color: Int) : Shape(color)
}

It is better than the previous approach, but…

interface

Back to Java roots!

interface PaintedShape {
    val color: Int
}

sealed class Shape : PaintedShape{
    data class Circle(val radius: Int, override val color: Int) : Shape()
    data class Square(val size: Int, override val color: Int) : Shape()
}

It works and the supertype does not have anything not relevant.

public interface PaintedShape {
    int getColor();
}

public abstract class Shape implements PaintedShape {

    private Shape() {
    }

However, this approach requires creating a separate interface. And — as we know — naming is the hardest CS issue.

abstract val

Like an interface but causes less friction.

sealed class Shape {
    abstract val color: Int

    data class Circle(val radius: Int, override val color: Int) : Shape()
    data class Square(val size: Int, override val color: Int) : Shape()
}

The bytecode is as simple as the interface one.

public abstract class Shape {

    public abstract int getColor();

    private Shape() {
    }

Results

Use abstract val, Luke!

This kind of research is fun and all but sometimes Kotlin makes me sad. In this particular issue I’ve been struggling to reason what is better — open val or abstract val. Both work but it is easy to forget about the bytecode and make a random choice. The language could be more strict with this kind of choices but oh well.