Delegation

https://kotlinlang.org/docs/delegation.html
https://kotlinlang.org/docs/delegated-properties.html
Delegates

Делегирование представляет паттерн объектно-ориентированного программирования, который позволяет одному объекту делегировать/перенаправить все запросы другому объекту. В определенной степени делегирование может выступать альтернативой наследованию.

• Бывают 2 видов: на класс и на поле.

interface Messenger {
    fun send(message: String)
}

class InstantMessenger: Messenger {
    override fun send(message: String) {
        println(message)
    }
}

class SmartPhone(name: String, messenger: Messenger): Messenger by messenger

val telegram = InstantMessenger()
val pixel = SmartPhone("Pixel 9", telegram)
pixel.send("Hello World")
Delegated Properties

Делегированные свойства позволяют делегировать получение или присвоение их значения во вне - другому классу. Это позволяет нам добавить некоторую дополнительную логику при операции со свойствами, например, логгирование, какую-то предобработку и т.д

class Person(personName: String) {
    var name: String by LoggerDelegate(personName)
}

class LoggerDelegate(private var personName: String) {

    operator fun getValue(thisRef: Person, property: KProperty<*>): String {
        return personName
    }

    operator fun setValue(thisRef: Person, property: KProperty<*>, value: String) {
        personName = value
    }
}

val tom = Person("Tom")
tom.name = "Bob"
println(tom.name)
ReadOnlyProperty

Используется для создания делегатов только для чтения (для свойств val).

class LazyStringDelegate: ReadOnlyProperty<Any?, String> {
    override fun getValue(thisRef: Any?, property: KProperty<*>): String {
        return "Computed Value"
    }
}

val myProperty by LazyStringDelegate()
ReadWriteProperty

Используется для создания делегатов для свойств с возможностью записи (var). Наследуется от ReadOnlyProperty и добавляет метод setValue для управления записью данных.

class ObservableProperty(var value: Int): ReadWriteProperty<Any?, Int> {
    override fun getValue(thisRef: Any?, property: KProperty<*>): Int {
        return value
    }

    override fun setValue(thisRef: Any?, property: KProperty<*>, value: Int) {
        println("Value of ${property.name} is changing from $this.value to $value")
        this.value = value
    }
}

var myVar by ObservableProperty(10)
PropertyDelegateProvider

Интерфейс, позволяющий создать делегаты, которые могут учитывать дополнительные параметры на этапе инициализации. Это полезно, когда нужно передать начальные данные в делегат или выполнить дополнительную логику для настройки делегата в зависимости от контекста.

class ConfigurableDelegateProvider(
    private val initialValue: Int
): PropertyDelegateProvider<Any?, ReadWriteProperty<Any?, Int>> {
    
    override fun provideDelegate(thisRef: Any?, property: KProperty<*>): ReadWriteProperty<Any?, Int> {
        println("Initializing property '${property.name}' with initial value $initialValue")
        return ConfigurableDelegate(initialValue)
    }
}

class ConfigurableDelegate(
    var value: Int
): ReadWriteProperty<Any?, Int> {

    override fun getValue(thisRef: Any?, property: KProperty<*>): Int = value

    override fun setValue(thisRef: Any?, property: KProperty<*>, value: Int) {
        this.value = value
    }
}

class Example {
    var prop1 by ConfigurableDelegateProvider(10)
    var prop2 by ConfigurableDelegateProvider(20)
}
by

Используется для делегирования свойств или функций к другим объектам. Оно позволяет передать ответственность за реализацию или хранение значения другому объекту, минимизируя дублирование кода и повышая гибкость.

val lazyValue: String by lazy {
    println("Computed!")
    "Hello, Kotlin"
}
lazy

Инициализирует свойство лениво. Это значит, что свойство будет инициализировано только при первом обращении к нему.

LazyThreadSafetyMode.SYNCHRONIZED Используется по умолчанию. Значение вычисляется только в одном потоке выполнения, и все остальные потоки могут видеть одно и то же значение.

LazyThreadSafetyMode.PUBLICATION Используется, когда синхронизация не требуется, тогда несколько потоков смогут исполнять вычисление одновременно.

LazyThreadSafetyMode.NONE Используется, если инициализация всегда будет происходить в одном потоке исполнения. Не гарантирует никакой потокобезопасности и связанных с этим дополнительных затрат.

val name: String by lazy(::getName)
val name: String by lazy(LazyThreadSafetyMode.PUBLICATION) {
    getName()
}
observable

Позволяет следить за изменениями свойства и выполнять определенные действия при каждом изменении.

var observableValue: String by Delegates.observable("Initial") { prop, old, new ->
    println("Property ${prop.name} changed from $old to $new")
}

observableValue = "Changed" // Property observableValue changed from Initial to Changed
vetoable

Позволяет проверять новые значения свойства перед его изменением и решать, следует ли принять это значение или нет.

var vetoableValue: Int by Delegates.vetoable(0) { _, old, new ->
    new >= 0 // Разрешаем только положительные значения
}

vetoableValue = 10 // Успешно изменено
println(vetoableValue) // 10

vetoableValue = -1 // Не изменяется, так как значение отрицательное
println(vetoableValue) // 10
notNull

Используется для свойств, которые будут инициализированы позже, но которые никогда не должны быть null.

var name: String by Delegates.notNull()

fun initializeName(value: String) {
    name = value
}

fun printName() {
    println(name) // Если name не инициализирован, произойдет ошибка
}
map

Позволяет связывать свойства объекта с элементами Map. Это удобно для динамического маппинга данных.

class User(val map: MutableMap<String, Any?>) {
    val name: String by map
    val age: Int by map
}

val user = User(mutableMapOf("name" to "John", "age" to 30))
println(user.name) // John
println(user.age) // 30

user.name = "Jane"
println(user.map["name"]) // Jane
Вопросы на собесе (6)
  1. Какие виды делегатов есть в Kotlin?

    На класс и на поле.

  1. Какие стандартные делегаты есть в Kotlin?

    lazy()

    observable()

    vetoable()

    notNull()

    map()

  1. Как работает делегат на класс?

    Позволяет делегировать реализацию свойств или методов другому объекту. Для этого нужно использовать ключевое слово by, указывая на делегирующий объект.

  1. На каком потоке исполняется lazy?

    По умолчанию инициализация lazy выполняется на том потоке, где впервые запрашивается его значение. Однако, если используется LazyThreadSafetyMode.PUBLICATION или LazyThreadSafetyMode.SYNCHRONIZED, инициализация может быть потокобезопасной.

  1. Является ли lazy потокобезопасным?

    Да, lazy является потокобезопасным по умолчанию, используя LazyThreadSafetyMode.SYNCHRONIZED. Это означает, что при первом доступе к значению инициализация будет безопасной для потоков.

  1. Как создать свой собственный делегат?

    Реализовать интерфейс ReadOnlyProperty или ReadWriteProperty. Затем необходимо определить методы getValue и (при необходимости) setValue.