Item 16: Properties should represent state, not 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.
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:
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 :
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.
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:
This means that this interface promises to have a getter. We can also override properties:
For the same reason, we can delegate properties:
Property delegation is described in detail in Item 21: Use property delegation to extract common property patterns. Because properties are essential functions, we can make extension properties as well:
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:
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:
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.
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 states - 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:
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 states; if you need to modify them later, use custom getters and setters:
A simple rule of thumb is that a property describes and sets a state, while a function describes a behavior.