
Moving forward with Kotlin #2: Inheritance
This is part of a series of articles. You can find the rest here.
For better or worse, we still require inheritance when developing with Kotlin. However, things are a bit different, mostly for the better. Let’s have a look at what tools do we have in the language to solve classical problems in Object Modelling.
The final modifier
In Java, inheritance is available by default. Meaning that when declaring a new type we can define children types that inherit their behaviour and data model. If we want to make sure that others can not extend our types we have to provide a final
modifier to them explicitly.
However, why would we want to do something like this? Let’s have a look at what Item 17 from Effective Java has to say:
Item 17: Design and document for inheritance or else prohibit it
In a few words, when allowing for inheritance, we have to make sure we are clear on how the base type behaves and how child types are expected to behave. Either by providing accurate and descriptive names, or by giving adequate documentation. Clarity is required as both sides may depend on each other’s behaviours, i.e. the order of invocation.
Another alternative is to avoid inheritance altogether. Making our type more deterministic and predictable as its behaviour is not going to change if a child type decides to change the way it works. Moreover, when we use them, we can be assured that we get what we expected unlike what happens in the next example.
‘Final’ example
Let’s say we have a component which takes a currency, and an amount, returning a String
representation.
Now, someone (probably ourselves) comes along some time later and decides our program needs a formatter that also rounds up the amount.
Here, we’ve added a new behaviour, and since we can still cast it to the original type of PriceFormatter
whoever receives an instance is not going to know we had that behaviour changed necessarily.
To make sure this doesn’t happen we can declare the type as final
, consequently forcing future implementers to have to create a bespoke formatter. Incidentally, making the code more explicit.
Kotlin has no finals
I lied there. What it doesn’t have is the final keyword. That is because types are final out of the box. Most of the time we declare types that don’t require inheritance. So, in Kotlin if we want something to have children, we have to declare it explicitly. There are a few ways: open
or abstract
, and sealed
. Let’s have a look at the first two.
These two may look similar and, as they are, we can say they are effectively the same. Both allow other types to inherit from them. The main difference so far is that we can instantiate open
classes, while the abstract
one requires another type to extend it.
Functions defined on either of them are also final by default so, in both cases, we have to be explicit regarding on which functions descendents are allowed to override.
It’s important to note that overriding a function in Kotlin requires to use the override
keyword. It’s not an annotation as it was in Java. Also, it’s compulsory. No chance there to leave a rogue function after a refactor because we forgot to annotate it before.
Now, in the case of abstract
classes, we have another way to allow for function ‘override’. We can declare functions as abstract without a body. Similar to what happens in Java, we are stating that the parent type doesn’t have a full implementation. It is the job of the inheritor to define such logic. In this case, we are forcing to override the given function.
As mentioned before, we can’t instantiate abstract classes. Banning such extension means that, unless we ‘reopen’ the potential for inheritance further, we can’t inherit from any of the types.
All this said, opening the hierarchy of a class should be done on rare occasions. As we’ll see, we have a better tool for this in our Kotlin arsenal.
Closed hierarchies
A closed hierarchy is one that doesn’t allow for extension. All the possible descending types are already declared. In a way, every class declared in Kotlin is a closed hierarchy of size one. However, we can also increase the family tree when needed. We’ve seen how to do this with open
and abstract
classes.
However, using abstract
or open
leaves the parent available to be implemented by anybody which can be almost appropriate when building an API or SDK, but never when building a system where we have a finite number of possibilities.
Closed hierarchies are not a feature restricted to Kotlin though. Enumerations, a.k.a.: enums
are classes in a perfect disguise which create a closed hierarchy. They behave like other classes, in the sense that they can have methods, fields and even constructors. However, they have some unique properties.
Enums
In Java, the system instantiates enum
at the static scope, often when they are first mentioned (depending on the VM). Also in Java, enums cannot extend from other classes. Mainly, because they already extend from the Enum
type. They can, however, implement interfaces.
With Kotlin, on the other hand, enums are closer to classes, to the point that we declare them using the enum
modifier before the class definition.
You may have noticed that I’ve named them in a similar way to classes and not as constants (uppercase snake). Opinion alert here! You should decide with your team what feel comfortable and intuitive. However, this follows a similar pattern to what we do with singleton object
types. So as I said in part one: this is a new paradigm.
One great benefit of closed hierarchies like enumerations in Kotlin is that we are forced to be exhaustive when using when
expressions. Let’s say that we have this function defined inside our previous enum:
This code does not compile because we are missing some of the possible cases, so it doesn’t know what to do. The compiler knows that there are more than two candidates for WeekDay
and, since the when
expression is expected to return a result, we get a complaint.
We can resolve this by adding an else
at the end of the matching clause:
However, this is not ideal as we may have a situation where we want to add a new item to the enumeration (something like LeapDay
it would fall into the else
category. We can be more explicit instead:
If we do this instead and we were to add a new item to this enumeration, in the future, the when
in shortName
as well as any others, stops working. Making the compiler unhappy. Which, in turn, forces us to stop and consider what do we need for each case.
Nevertheless, these are singleton object
which means that we only have one possible instance of each. Is there a way to do something similar but also carry an instance state? Yes, there is indeed.
Sealed classes
When declaring closed hierarchies, the ultimate choice in Kotlin are sealed classes. A sealed class is an abstract class (cannot be instantiated) which has a known number of children defined at compile time.
Sealed classes behave similarly to how enums do. They have a limited number of descendants. They also allow for exhaustive when
clauses. However, they include one significant addition. They may contain data.
With Enumeration types, every object is a singleton instance. Here, we can decide whether a subtype is a singleton or an instantiable type (with a constructor). Singletons are easy; we use the object
keyword. Types that we may want to instantiate with data can a data class. We could use a simple class instead of a data class
but I’m yet to see where that’s not better defined with an object
instead.
Being able to create instances of sealed classes combined with the fact that Kotlin is clever enough to auto-cast an instance if we have checked the type, means we can do things like the following:
As you can see, since we are checking the type in the condition of our when
Kotlin lets us magically access the properties of the instance without having to cast it. On the other hand, if we tried this in Java, we would get some horrible mess.
Such code is, by a long way, much harder to read. We have to add extra boilerplate to cast out types into the right one. We already know what it is, and so does the compiler. Kotlin knows better and automatically casts the property to the value we just checked letting us access its goodies.
Forced exhaustion
Now, both of these last two examples have a particular issue. Since they generate side effects instead of returning a value with terminating statements, we lose exhaustion. Meaning that, if we are going to add a new entry to our hierarchy the compiler doesn’t complain and we just missed a case which could end with surprise unwanted results.
One way to do this is to create a throwaway property which is removed after compilation but still complain when we are missing a case.
Which makes this code not compile:
A potential issue here is that is that now we have a global extension property. It doesn’t have to be a problem (especially as we often type the start of what we want), but some people don’t like that as it can contaminate the global scope. Also, don’t worry, if our class already has a property with the name exhaustive
we can still reference it.
Why do we need these?
One of the main benefits of closed hierarchies is, as we mentioned before, the deterministic nature of limiting the possibility for inheritance. Not only we can make our code less likely to error, but it also reduces the cognitive pressure on us as we don’t have to be thinking about what-ifs.
Result pattern
We can use them to define the possibility of errors.
We’ll talk another time about this in more detail. Especially that out
on the generics. In the meantime, we can use it quite straightforward.
This is a common datatype in functional programming. If you are more interested on learning come and check out our courses on FP 🔜 over at:
States
Another excellent use for closed hierarchies is for defining the state that a component can be in. The canonical example is the state used for a screen.
We can see this as an expansion of what we have with the Result
pattern but representing intermediate steps in between the Initial
state and a final one.
The great thing about this is that we can see all the possible states that a given screen has. Meaning we can test the presentation layer and the application logic separately and, at the same time keep them working together.
These are quite common in architectural patterns like MVI and MVVM, which vaguely resemble a state machine.
Expressing semantics
One final use case is to add semantic to primitive types. If we get a value from the server that represents if a user is a paid user, we can always convert it to a Boolean
in Koltin. Alternatively, we can add some extra meaning when parsing it into our model.
With a set up like this, it incredibly easy to determine what a value is. If we had a primitive Boolean
we would have to have pre-existing knowledge of what each of them means or look into the code to decipher what each of them does.
When using a value like this with Android Views, we can very easily map them into implementation details:
It reads like English!
The best part is that, if we are using minimiser tools, these get converted into primitives. Also, by the time inline classes
are out, we may see options for them to also work with closed hierarchies.
Closing
Hope you enjoyed this explanation on how type hierarchies and inheritance vary from Java to Kotlin, and how we can use them to better our code. As always, if you want updates on upcoming issues on this series please do subscribe here, or follow me here or on Twitter.