Smart constructors in Kotlin
Making illegal states unrepresentable is a good engineering practice. To do this we want to add checks before object creation to make sure it is created in a correct state. Throwing exceptions in the constructor would work for this but it would mean introducing runtime exceptions in your software. If you want to safely create objects without runtime exceptions then smart constructors might be a good solution for you.
Creating an object in a correct state
So let’s define a class called Positive which should hold a positive ( >0 ) Int.
Since we are only using one value and we want to remove object overhead we can use a value class for this.
This technique will work for any class but do keep in mind that a data class
has a copy
method which can circumvent the constructor.
@JvmInline
value class Positive(val value : Int)
Right now there is no checking to see if the class actually contains a positive number.
We could use Kotlin’s require
to add a runtime check.
@JvmInline
value class Positive(val value: Int) {
init {
require(value > 0)
}
}
This will now throw an IllegalArgumentException
in case the value is ⇐ 0.
An alternative way of doing this is using a factory method to create the object.
This method will only give back an instance if the object is created in a correct state and otherwise return null.
Then we can use Kotlin’s nullability system to only do actions on the object if creation succeeded e.g. Positive(1)?.let { println(it) }
@JvmInline
value class Positive private constructor(val value: Int) {
companion object {
fun fromValue(value: Int): Positive? {
return if (value > 0) {
Positive(value)
} else {
null
}
}
}
}
We can make our constructor private so the only way to create an instance of the object now is through the factory method Positive.fromValue(1)
.
Smart constructors
While this works, Kotlin actually has a neat trick to make this more elegant.
Kotlin supports overriding the invoke
operator in the companion object.
Doing so we can actually call the normal constructor and the invoke
method will get called instead.
@JvmInline
value class Positive private constructor(val value: Int) {
companion object {
operator fun invoke(value: Int): Positive? {
return if (value > 0) {
Positive(value)
} else {
null
}
}
}
}
So with this smart constructor we can now create a Positive?
by just calling Positive(1)
.