Everything else is just a tool to model your domain in a way that’s easier to read and reason about, and to produce much shorter code. Interfaces define module boundaries and can be mocked to enable simpler testing. Through classes, objects, and data structures, you can model your domain entities. Enums make a list of limited options crystal clear. Extensions allow you to add functionality to existing types, without having access to their original source code, giving advantage of better logic structure. With generics, you can write universal code, promoting reuse. And overloaded operators give that extra edge to common operations that benefit from succinct, crisp syntax.
Extensions
It is possible to extend functionality of existing types without resorting to inheritance. It is also possible to program standard, reusable functionality using delegation.
Property wrappers
To intercept a property and make it behave in a special way that’s easy to reuse, for example, storing its value in a key-value map or tracking its changes, you can use property delegates. Such properties are called delegated properties.
Property delegates don’t have to implement any formal interfaces; they just need to provide special methods.
Read-only
Read-only delegates should provide a getValue(...) method.
import kotlin.reflect.KProperty
class TreasuryDelegate(
private val treasure: String,
private var quantity: Int
) {
operator fun getValue(thisRef: Any?, property: KProperty<*>): String? {
if (quantity == 0) return null
quantity -= 1
return treasure
}
}
val limitedToken: String? by TreasuryDelegate("precious", quantity = 5)
To avoid defining a new type for each delegate and polluting the name space, ReadOnlyProperty interface is provided as a convenience, so the delegate can be implemented via a function.
import kotlin.reflect.KProperty
import kotlin.properties.ReadOnlyProperty
fun treasury(treasure: String, quantity: Int): ReadOnlyProperty<Any?, String?>
= object: ReadOnlyProperty<Any?, String?> {
private var count = quantity
override fun getValue(thisRef: Any?, property: KProperty<*>): String? {
if (count == 0) return null
count -= 1
return treasure
}
}
val alsoLimitedToken: String? by treasury("precious", quantity = 5)
Read-write
Read-write delegates should additionally provide a setValue(...) method.
import kotlin.reflect.KProperty
enum class TextTransform {
UPPERCASE, LOWERCASE
}
class TextTransformDelegate(
private var text: String,
private val transform: TextTransform
) {
operator fun getValue(thisRef: Any?, property: KProperty<*>): String
= "${property.name}: " + when (transform) {
TextTransform.UPPERCASE -> text.uppercase()
TextTransform.LOWERCASE -> text.lowercase()
}
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
text = value
}
}
var formattedText: String by TextTransformDelegate(
"whatever",
transform = TextTransform.UPPERCASE
)
formattedText = "hello"
ReadWriteProperty convenience interface can be used to avoid new types and implement the delegate via a function.
import kotlin.reflect.KProperty
import kotlin.properties.ReadWriteProperty
fun textTransform(
initialText: String,
transform: TextTransform
): ReadWriteProperty<Any?, String>
= object: ReadWriteProperty<Any?, String> {
private var text = initialText
override fun getValue(thisRef: Any?, property: KProperty<*>): String
= "${property.name}: " + when (transform) {
TextTransform.UPPERCASE -> text.uppercase()
TextTransform.LOWERCASE -> text.lowercase()
}
override fun setValue(
thisRef: Any?,
property: KProperty<*>,
value: String
) {
text = value
}
}
var alsoFormattedText: String by textTransform(
"whatever",
transform = TextTransform.UPPERCASE
)
alsoFormattedText = "hello"
Delegated properties can also delegate to global variables, visible properties of other objects, or other properties of the same object. Local variables, inside a function, can use delegation, too.
The example moreover shows how to use the pattern for evolving API, deprecating old properties and proxying to them until a rewrite is ready.
@Deprecated(
"Global 'limitedToken' is deprecated",
ReplaceWith("DelegationContext.limitedToken")
)
val limitedToken: String? by TreasuryDelegate("precious", quantity = 5)
data class Storage(val data: String)
class DelegationContext(storage: Storage) {
val treasure: String? by TreasuryDelegate("precious", quantity = 5)
var formattedText: String by textTransform(
"whatever", transform = TextTransform.UPPERCASE
)
@Deprecated("Also deprecated", ReplaceWith("veryLimitedToken"))
val limitedToken: String? by ::limitedToken
val veryLimitedToken: String? by this::limitedToken
val data: String by storage::data
fun localContext() {
val treasure: String? by TreasuryDelegate("precious", quantity = 5)
var formattedText: String by textTransform(
"whatever", transform = TextTransform.UPPERCASE
)
this.formattedText = "hello"
formattedText = "hello"
}
}
Delegate provider
It is possible to insert one more level of indirection when delegating, for example, to validate the property to which the delegate is applied or even provide a different delegate implementation dynamically. To that end, a provideDelegate(...) operator method needs to be implemented. It can return any property delegate, like ReadOnlyProperty, ReadWriteProperty, or a custom class that just contains getValue(...) or setValue(...) methods.
PropertyDelegateProvider convenience interface can be used to avoid new types and implement the delegate provider via a function.
import kotlin.reflect.KProperty
import kotlin.properties.ReadOnlyProperty
import kotlin.properties.PropertyDelegateProvider
class GuardianAtTheGate(
private val treasure: String,
private var quantity: Int
) {
operator fun provideDelegate(
thisRef: Any?,
property: KProperty<*>
): ReadOnlyProperty<Any?, String?> {
if (!property.name.lowercase().endsWith("treasure"))
throw Exception("Wrong name!")
return treasury(treasure, quantity)
}
}
fun guardian(
treasure: String,
quantity: Int
): PropertyDelegateProvider<Any?, ReadOnlyProperty<Any?, String?>>
= PropertyDelegateProvider { thisRef: Any?, property ->
if (!property.name.lowercase().endsWith("treasure"))
throw Exception("Wrong name!")
treasury(treasure, quantity)
}
val sacredTreasure: String? by GuardianAtTheGate("one of a kind", quantity = 1)
val alsoSacredTreasure: String? by guardian("one of a kind", quantity = 1)