Item 16: Properties should represent state, not behavior

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

  1. // Kotlin property
  2. var name: String? = null
  3. // Java field
  4. 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. Starting with the fact that they can always have custom setters and getters:

  1. var name: String? = null
  2. get() = field?.toUpperCase()
  3. set(value) {
  4. if(!value.isNullOrBlank()) {
  5. field = value
  6. }
  7. }

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

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

For a read-write property var, we can make a property by defining a getter and 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 used Date from the Java stdlib. Then at some point for a reason, the object cannot store the property of this type anymore. Perhaps because of a serialization issue, or maybe because you lifted this object 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 property millis, and modify the date property to not hold data but instead to wrap/unwrap that other property.

  1. var date: Date
  2. get() = Date(millis)
  3. set(value) {
  4. millis = value.time
  5. }

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:

  1. interface Person {
  2. val name: String
  3. }

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

  1. open class Supercomputer {
  2. open val theAnswer: Long = 42
  3. }
  4. class AppleComputer : Supercomputer() {
  5. override val theAnswer: Long = 1_800_275_2273
  6. }

For the same reason, we can delegate properties:

  1. val db: Database by lazy { connectToDb() }

Property delegation is described in detail in Item 21: Use property delegation to extract common property patterns. Because properties are essentially functions, we can make extension properties as well:

  1. val Context.preferences: SharedPreferences
  2. get() = PreferenceManager
  3. .getDefaultSharedPreferences(this)
  4. val Context.inflater: LayoutInflater
  5. get() = getSystemService(
  6. Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
  7. val Context.notificationManager: NotificationManager
  8. get() = getSystemService(Context.NOTIFICATION_SERVICE)
  9. 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 behaviour like in the example below:

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

Here sum property iterates over all elements and so it represents algorithmic behavior. Therefore this property is misleading: finding the answer can be computationally heavy for big collections, and this is not expected at all for a getter. This should not be a property, this should be a function:

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

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

  • Operation is computationally expensive or has computational complexity higher than O(1) - A user does not expect that using a property might be expensive. If it is, using a function is better because it communicates that it might be and that user might be parsimonious using it, or the developer might consider caching it.
  • It involves business logic (how the application acts) - when we read code, we do not expect that a property might do anything more than simple actions like logging, notifying listeners, or updating a bound element.
  • It is not deterministic - Calling the member twice in succession produces different results.
  • It is a conversion, such as Int.toDouble() - It is a matter of convention that conversions are a method or an extension function. Using a property would seem like referencing some inner part instead of wrapping the whole object.
  • Getters should not change property state - We expect that we can use getters freely without worrying about property state modifications.

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

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

On the other hand, to get and set 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, and if you need to modify them later, use custom getters and setters:

  1. // DON’T DO THIS!
  2. class UserIncorrect {
  3. private var name: String = ""
  4. fun getName() = name
  5. fun setName(name: String) {
  6. this.name = name
  7. }
  8. }
  9. class UserCorrect {
  10. var name: String = ""
  11. }

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