article banner

Effective Kotlin Item 15: Properties should represent a state, not a behavior

This is a chapter from the book Effective Kotlin. You can find it on LeanPub or Amazon.

Kotlin properties look similar to Java fields, but they actually represent a different concept.

// Kotlin property var name: String? = null // Java field String name = null;

Even though they can be used the same way to hold data, we need to remember that properties have many more capabilities, the first of which is the fact that they can always have custom setters and getters:

var name: String? = null get() = field?.toUpperCase() set(value) { if(!value.isNullOrBlank()) { field = value } }

You can see here that we are using the field identifier. This is a reference to the backing field, which lets us hold data in this property. Such backing fields are generated by default because the default implementations of setters and getters use them. We can also implement custom accessors that do not use them; in this case, the property will not have a field at all. For instance, a Kotlin property can be defined using only a getter for a read-only val property :

val fullName: String get() = "$name $surname"

For a read-write var property, we can make a property by defining a getter and a setter. Such properties are known as derived properties, and they are not uncommon. They are the main reason why all properties in Kotlin are encapsulated by default. Just imagine that you have to hold a date in your object and you use Date from the Java stdlib. Then, at some point for some reason, the class cannot store the property of this type anymore, perhaps because of a serialization issue or maybe because you lifted this class to a common module. The problem is that this property has been referenced throughout your project. With Kotlin, this is no longer a problem as you can move your data into a separate millis property and modify the date property not to hold data but instead to wrap/unwrap that other property.

var date: Date get() = Date(millis) set(value) { millis = value.time }

Properties do not need fields. Rather, they conceptually represent accessors (getter for val; getter and setter for var). This is why we can define them in interfaces:

interface Person { val name: String }

This means that this interface promises to have a getter. We can also override properties:

open class Supercomputer { open val theAnswer: Long = 42 } class AppleComputer : Supercomputer() { override val theAnswer: Long = 1_800_275_2273 }

For the same reason, we can delegate properties:

val db: Database by lazy { connectToDb() }

Because properties are essential functions, we can make extension properties as well:

val Context.preferences: SharedPreferences get() = PreferenceManager .getDefaultSharedPreferences(this) val Context.inflater: LayoutInflater get() = getSystemService( Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater val Context.notificationManager: NotificationManager get() = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager

As you can see, properties represent accessors, not fields. This way, they can be used instead of some functions, but we should be careful what we use them for. Properties should not be used to represent algorithmic behavior, as in the example below:

// DON’T DO THIS! val Tree<Int>.sum: Int get() = when (this) { is Leaf -> value is Node -> left.sum + right.sum }

Here, the sum property iterates over all elements, representing algorithmic behavior. So, this property is misleading: finding the answer can be computationally heavy for big collections, which is not expected for a getter. This should be a function, not a property:

fun Tree<Int>.sum(): Int = when (this) { is Leaf -> value is Node -> left.sum() + right.sum() }

The general rule is that we should use properties only to represent or set a state, and no other logic should be involved. A useful heuristic to decide if something should be a property is: If I defined this property as a function, would I prefix it with get/set? If not, it should probably not be a property. More concretely, here are the most typical situations when we should use functions instead of properties:

  • An operation is computationally expensive or has computational complexity higher than O(1) - A user does not expect using a property to be expensive.

  • An operation throws an exception - A user does not expect that property getter or setter can throw an exception.

  • It involves business logic (how the application acts) - when we read code, we do not expect a property to do anything more than simple actions like logging, notifying listeners, or updating a bound element.

  • It is not deterministic - Calling a getter should not change a state, and calling a setter twice in succession should produce the same result (unless the object has been modified in another thread).

  • It is a conversion, such as Int.toDouble() - It is a matter of convention that conversions are methods. Using a property would seem like referencing some part of the state that is wrapped over by the object.

  • Getters should not change property state - We expect to be able to use getters freely without worrying about property state modifications.

For instance, calculating the sum of some elements requires iterating over all of them (this is behavior, not a state) and has linear complexity. Therefore, it should not be a property and is defined in the standard library as a function:

val s = (1..100).sum()

On the other hand, to get and set a state, we use properties in Kotlin, and we should not involve functions unless there is a good reason. We use properties to represent and set state; if you need to modify them later, use custom getters and setters:

// DON’T DO THIS! class UserIncorrect { private var name: String = "" fun getName() = name fun setName(name: String) { this.name = name } } class UserCorrect { var name: String = "" }

A simple rule of thumb is that a property describes and sets a state, while a function describes a behavior.