Simpler Kotlin class hierarchies using class delegation

Fabio Collini
ProAndroidDev
Published in
6 min readFeb 27, 2019

--

Big class hierarchies are out of fashion, an item of the book Effective Java written by Joshua Bloch suggests to favor composition over inheritance. But sometimes a class hierarchy can be useful to model the data in an application, if used with attention it can simplify the code and avoid duplication without increasing the complexity.

Let’s see a practical usage of a class hierarchy, we can use it to manage the data that we retrieve from a server. For example, the server response can contain a list of people:

[
{
"type": "student",
"name": "studentName",
"surname": "studentSurname",
"age": 20,
"university": "universityName"
},
//...
]

This JSON can be parsed and transformed into instances of a PersonJson class:

data class PersonJson(
val type: String,
val name: String,
val surname: String,
val age: Int,
val university: String?,
val company: String?
)

This class can be mapped to a hierarchy, a different class can be chosen based on the value of the type field (it can be student or worker).

This is a classical hierarchy example, in Java it can be implemented using a base class with the common fields:

public abstract class Person {
private final String name;
private final String surname;
private final int age;

public Person(String name, String surname, int age) {
this.name = name;
this.surname = surname;
this.age = age;
}

//...getters...
}

And two subclasses that extend the base class and add some extra fields. The first one to declare a Student:

public class Student extends Person {

private String university;

public Student(String name, String surname, int age,
String university) {
super(name, surname, age);
this.university = university;
}

public String getUniversity() {
return university;
}
}

And the second one to define a Worker:

public class Worker extends Person {

private final String company;

public Worker(String name, String surname, int age,
String company) {
super(name, surname, age);
this.company = company;
}

public String getCompany() {
return company;
}
}

We can easily convert this code to Kotlin using a simple CMD+Option+Shift+K to obtain something similar:

abstract class Person(
val name: String,
val surname: String,
val age: Int
)

class Student(
name: String,
surname: String,
age: Int,
val university: String
) : Person(name, surname, age)

class Worker(
name: String,
surname: String,
age: Int,
val company: String
) : Person(name, surname, age)

Using the properties we don’t need to write the getters, the code is definitely more concise and easier to read. Here we can see an advantage in using a hierarchy: we can manage nullability in the correct way. For example the university field in PersonJson class must be defined as nullable because it’s not available for Worker’s instances. This field can be defined as not nullable in the Student class.

These classes can be used to map a list of objects retrieved from a server call to a list of Person instances, the class to instantiate is chosen based on the type field (a String is used instead of an Enum to keep the code simple):

val json: List<PersonJson> = listOf(/* ... */)

val people = json.map {
if (it.type == "student")
Student(
it.name,
it.surname,
it.age,
it.university
)
else
Worker(
it.name,
it.surname,
it.age,
it.company
)
}

println(people.joinToString { "${it.name} ${it.surname}" })

This code is an example, so it’s quite simple; in a real project each class could contain many fields and you might need to handle more subclasses. Even in this simple example there are some duplications: how many modifications do we need to make to add a field to the base class? At least seven: the field on the base class, the two fields on the subclasses, the two parameters to the base class constructor and the two actual parameters when a subclass is created.

Abstract fields in subclass

The first aspect that can be simplified is how we invoke the superclass constructor: right now we need to pass all the parameters because the fields are defined in the base class.

We can easily avoid this duplication and simplify the code defining the base class fields as abstract:

abstract class Person {
abstract val name: String
abstract val surname: String
abstract val age: Int
}

data class Student(
override val name: String,
override val surname: String,
override val age: Int,
val university: String
) : Person()

data class Worker(
override val name: String,
override val surname: String,
override val age: Int,
val company: String
) : Person()

The override modifier must be used in the subclasses to make the code compile. In this way the properties are defined in each subclass, using Java this would cause code duplication. Using Kotlin the code is easy and we can avoid passing all the parameters to the base class constructor.

From abstract class to interface

The base class can be now converted to an interface to avoid defining all the fields as abstract (the fields in an interface are alwaysabstract):

interface Person {
val name: String
val surname: String
val age: Int
}

data class Student(
override val name: String,
override val surname: String,
override val age: Int,
val university: String
) : Person

data class Worker(
override val name: String,
override val surname: String,
override val age: Int,
val company: String
) : Person

The code is easier than the first example but it still contains some duplications, can we avoid defining the common fields in all the classes?

Class delegates to the rescue

Let’s start this refactoring defining a data class that contains the common fields and implements the Person interface:

data class PersonData(
override val name: String,
override val surname: String,
override val age: Int
) : Person

It can seem strange that this class implements the Person interface but this is the key to use it in the other classes. The Student and Worker classes can be now modified to use this new class:

data class Student(
val data: PersonData,
val university: String
) : Person by data

data class Worker(
val data: PersonData,
val company: String
) : Person by data

What’s that by data syntax after the extended interface definition? That’s the syntax for class delegation, in this way we can define that these two classes implement the Person interface and that the methods of this interface are delegates to the data field. Under the hood, this is a concise way to define a class like this one:

data class Student(
val data: PersonData,
val university: String
) : Person {
override val name: String
get() = data.name
override val surname: String
get() = data.surname
override val age: Int
get() = data.age
}

EDIT: as suggested in a comment, a correct usage of the Student instances can be enforced defining the data field as private.

From an external point of view there aren’t any differences because of the class delegation, the Person methods can be invoked normally. The only difference is when an object is created: a PersonData instance must be created first and then used to create the real object:

val json: List<PersonJson> = listOf(/* ... */)

val people = json.map {
val data = PersonData(
it.name,
it.surname,
it.age
)
if (it.type == "student")
Student(
data,
it.university
)
else
Worker(
data,
it.company
)
}

println(people.joinToString { "${it.name} ${it.surname}" })

An extra object must be created but the code is definitely simpler, the common fields are managed just once and there is a clear separation between them and the subclasses fields.

What about sealed classes?

In case we want to define the hierarchy using a sealed class we need to write extra code. An intermediate sealed class between the interface and the implementations can be created:

sealed class SealedPerson: Person

The two concrete classes can extend both the sealed class and the interface:

data class Student(
private val data: PersonData,
val university: String
) : SealedPerson(), Person by data

data class Worker(
private val data: PersonData,
val company: String
) : SealedPerson(), Person by data

After this modification, the Person interface is useful just to manage the class delegation. Probably it’s better to avoid using it in the external code that uses this hierarchy. This code can use the SeleadPerson class to take advantage of the sealed class and obtain an error in case not all subclasses are managed. Unfortunately, the interface can’t be declared as private to enforce this pattern. However, it can be renamed to something else (and then the SealedPerson class can be renamed to just Person).

Wrapping up

Delegation is a really powerful concept in Kotlin, it can be used at field level (some examples are available in these two posts) or at class level as we have seen here. Thanks to delegation and other great features it’s easy to minimize the duplication in our Kotlin code!

--

--

Android GDE || Android engineer @nytimes || author @androidavanzato || blogger @codingjam