Kotlin, Data Class Inheritance by Delegation

A project I’m working on came across the following issue, it seems very specific to our situation but probably not. We had a Person class which had all the properties our domain used to describe a person, name, date of birth, address etc. We’d made it a data class to simplify working with all those properties. However, we treated as Person an entity, which is to say a Person, to be a Person, was assigned a distinct identifier. The problem arose around how to deal with a Person, before it was a Person, i.e. before it had been assigned its identifier. In the initial rough cut, the Person was given a nullable id:

data class Person(
val fName: String,
val lName: String,
val dob: LocalDate,
// ...
val id: UUID? = null
)

A nullable id?! Yeah I did that. It was expedient. Instantiate one, bounced it off the system of record, and get one back with its id set.

Another dev saw a nullable property, and immediately ripped out the property, renamed the class to PersonInfo, and made a new Person class with all the same fields, manually copied over, plus a non nullable id:

data class PersonInfo(
val fName: String,
val lName: String,
val dob: LocalDate,
// ...
)
data class Person(
val fName: String,
val lName: String,
val dob: LocalDate,
// ...
val id: UUID
)

Then they added a sprinkling of transformation convenience methods. It was the only way they could see to have Person class with no id, and one with an id later, since you can’t subclass a data class. And so we sat and glowered at each other for a while. I understood the motivation but introducing data transfer objects so close to heart of a system will lead to systemic layering of transformations with each layer adding a bit more noise like the whisper game, until out at the edges a Person will be unrecognizable from the entity.

Then the other dev wondered aloud if we couldn’t employ delegation somehow. Eureka! Here’s what that led us to:

interface PersonDefinition(
val fName: String,
val lName: String,
val dob: LocalDate,
// ...
)
data class Person(
private val data: PersonInfo,
val id: UUID
) : PersonDefinition by data
data class PersonInfo(
override val fName: String,
override val lName: String,
override val dob: LocalDate,
// ...
) : PersonDefinition

Wait… it’s up to two classes and an interface?! Yes. But here’s what this gets you:

  • There’s a single definition of the Person in the interface and changing it will, at the compiler level, force you to propagate the changes.
  • Everyone is a data class so that’s a win.
  • While Person doesn’t inherit from PersonInfo, it delegates to it for everything pertaining to PersonDefinition and so all the getters and setters etc pull through.
  • No translation code needed, a Person just composites in a PersonInfo.
  • No more nullables. Bye bye null checks on the id.

Was all this worth it to get rid of that nullable id? I’m not sure myself, but it kept the peace.

Graybeard code monkey, started on an Apple IIe, got a CS degree in the 80’s, and coded my way through C, C++, Objective-C, Java, Kotlin — and now Go.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store