A derived attribute is an attribute computed from other attributes e.g.:
- The
fullName
is aggregated from the first, middle and last name - The
age
is computed from the birthdate - etc.
Kotlin offers different options to manage such derived attributes. Let’s browse through them.
Inline field initialization
The simplest way to manage derived attributes is to declare a property, and compound its declaration with its initialization:
class Person(val firstName: String,
val middleName: String,
val lastName: String) {
val name = "$firstName $middleName $lastName"
}
That works pretty well if no logic is required.
But then, what if some of the properties are nullable, and null
values need to be managed properly?
Initialization block
Making code more readable when logic is involved requires to move initialization into a dedicated init
block:
class Person(val firstName: String? = null,
val middleName: String? = null,
val lastName: String? = null) {
val name: String (1)
init {
val fullName = """${firstName?.let { it }}
| ${middleName?.let { it }}
| ${lastName?.let { it }}""".trimMargin() (2)
if (fullName.isBlank()) name = fullName
else name = "John Doe"
}
}
1 | name is a val and has to be initialized |
2 | Initialization takes place in the init block, hence the compiler doesn’t complain |
Because the property is a val
, that approach is fine when the derived attribute depends only on constants and constructor parameters.
Yet, what if the valued depends on other factors, such as the current date?
Getter
For example, the above approach cannot be used to compute the age of a person, since it depends not only on the birth date, but also on the current date. The canonical way to compute such attributes in Kotlin is to use a dedicated getter method:
class Person(val birthDate: LocalDate) {
val age: Int
get() = birthDate.until(LocalDate.now(), ChronoUnit.DAYS).toInt()
}
Getters can easily replace both inline field initializiation and init blocks. There’s a huge difference, though: getters are methods, and might not be performant.
This approach is not suitable if the result takes time to compute, and keep returning the same result over and over.
Lazy delegate
The returned value benefits from being cached if the derived attribute:
- takes time to compute
- always return the same result over time
- and also - but not necessarily - needs to be accessed frequently
Kotlin offers a construct called delegated properties.
To cache results, the relevant kind is the lazy
delegate:
class Person(val firstName: String,
val middleName: String,
val lastName: String) {
val name by lazy { computeName() } (1)
private fun computeName() = "$firstName $middleName $lastName" (2)
}
1 | Get the value and cache it |
2 | Imagine this is an extremely time-consuming operation |
As expected, lazy
makes it so that:
- if the value is never accessed, it’s never computed
- if it is, it’s computed the first time and cached
- as soon as it’s accessed a second time, the value is returned from the cache