Kotlin Code Smell 19 - Setters
Code Missteps: When Objects Play Hard to Set
Play this article
The first exercise that junior programmers often do involves IDEs, tutorials, and senior developers who teach them an anti-pattern.
Problems
Mutability
Information Hiding
Anemic Models
Fail Fast
Integrity
Duplicated Code
Concurrent programming execution
Solutions
Avoid Setters
Set essential attributes on object construction.
Sample Code
Wrong
// Anemic mutable class
data class PhoneCall(
var origin: String? = null,
var destination: String? = null,
var duration: Long? = null
)
fun main() {
val janePhoneCall = PhoneCall()
janePhoneCall.origin = "555-5555"
janePhoneCall.destination = "444-4444"
janePhoneCall.duration = 60
}
Mutation brings lots of problems to your code.
fun main() {
// Since we have a setter, we can create invalid combinations.
// For example:
// - We can't exchange the call destination during the call,
// However, this is not enforced due to the setters' usage.
val janePhoneCall = PhoneCall()
janePhoneCall.origin = "555-5555"
janePhoneCall.destination = "555-5555"
janePhoneCall.duration = 60
}
// Origin and Destination cannot be the same
// To validate this, we're repeating the same code twice
class PhoneCall(
origin: String? = null,
destination: String? = null,
duration: Long? = null
) {
var origin: String? = origin
set(value) {
if (value == origin)
throw IllegalArgumentException("Destination cannot be the same as origin")
field = value
}
var destination: String? = destination
set(value) {
if (value == destination)
throw IllegalArgumentException("Destination cannot be the same as origin")
field = value
}
// duration is exposed in seconds as a ripple effect
// this violates information hiding principle and prevents
// us from changing its representation
var duration: Long? = duration
set(durationInSeconds) {
field = durationInSeconds
}
}
// Moreover, multiple threads (or coroutines) can change the same
// object. What is the right state of the object at the end?
Right
class PhoneCall(
val origin: String,
val destination: String,
val duration: Long
) {
// Single control point.
// We only create valid phone calls, and they remain valid
// since they cannot mutate.
init {
if (origin == destination)
throw IllegalArgumentException("Destination cannot be the same as origin")
}
// We're explicit about which measure of unit is used
fun durationInSeconds() = duration
fun durationInMilliSeconds() = duration * MILLISECONDS_IN_SECOND
companion object {
private const val MILLISECONDS_IN_SECOND = 1000
}
}
Examples
- DTOs
Exceptions
- Setting attributes is safe for non-essential attributes. However, it has all the drawbacks and considerations already mentioned.
Conclusion
Creating incomplete and anemic objects is a very bad practice that violates mutability, the fail fast principle, and real-world bijections.
Credits
- Code Smell 28 - Setters by Maximiliano Contieri