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 datadata 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.