Menu
Coddy logo textTech

Делегирование с помощью by

Часть раздела Объектно-ориентированное программирование путешествия по Kotlin на Coddy. Урок 46 из 57.

Делегирование означает, что объект передаёт работу другому объекту вместо наследования. Kotlin поддерживает это напрямую: class Polite(private val inner: Greeter) : Greeter by inner реализует Greeter, перенаправляя каждый вызов к inner, а класс переопределяет только то, что он изменяет:

interface Greeter {
    fun hello(name: String): String
    fun bye(name: String): String
}
class English : Greeter {
    override fun hello(name: String) = "Hello, $name"
    override fun bye(name: String) = "Bye, $name"
}
class Polite(private val inner: Greeter) : Greeter by inner {
    override fun hello(name: String) = inner.hello(name) + ", pleased to meet you"
}

Внутри main:

val g = Polite(English())
println(g.hello("Ada"))
println(g.bye("Ada"))

Вывод:

Hello, Ada, pleased to meet you
Bye, Ada

Делегирование — это альтернатива наследованию. Оно работает с интерфейсами, реализации которых являются финальными, а делегатом может быть любой объект, выбранный во время выполнения программы. Это множество подсчитывает каждый вызов add и передаёт всё остальное обычному MutableSet:

class CountingSet<T>(private val inner: MutableSet<T> = mutableSetOf()) : MutableSet<T> by inner {
    var attempts = 0
        private set

    override fun add(element: T): Boolean {
        attempts++
        return inner.add(element)
    }
}

Внутри main:

val tags = CountingSet<String>()
tags.add("kotlin")
tags.add("jvm")
tags.add("kotlin")
println("${tags.size} tags after ${tags.attempts} adds, contains jvm: ${"jvm" in tags}")

Результат:

2 tags after 3 adds, contains jvm: true

Делегированные свойства передают геттер и сеттер свойства другому объекту — снова с помощью by. lazy из главы 2 — один из таких делегатов, а ещё одним может быть map: каждое свойство считывает элемент с собственным именем:

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

Внутри main:

val u = User(mapOf("name" to "Ada", "age" to 36))
println("${u.name} is ${u.age}")

Вывод:

Ada is 36

Вы можете написать собственный делегат свойства: класс с operator fun getValue, а также с setValue для var. Kotlin вызывает их при каждом чтении и записи свойства. Этот делегат обрезает каждый присвоенный текст и подсчитывает изменения:

import kotlin.reflect.KProperty

class Trimmed {
    private var value = ""
    var changes = 0

    operator fun getValue(thisRef: Any?, property: KProperty<*>) = value
    operator fun setValue(thisRef: Any?, property: KProperty<*>, newValue: String) {
        value = newValue.trim()
        changes++
    }
}

class Form {
    val nameField = Trimmed()
    var name: String by nameField
}

Внутри main:

val form = Form()
form.name = "  Ada  "
form.name = "Ada L. "
println("[${form.name}] after ${form.nameField.changes} changes")

Вывод:

[Ada L.] after 2 changes
challenge icon

Задание

Легко

Интерфейс Storage (с put, get и keys) и его реализация MapStorage предоставлены. Напишите два декоратора с делегированием классов, каждый из которых переопределяет только put: LoggingStorage(inner) записывает put a=1 в свой доступный только для чтения список log и передаёт вызов дальше, а LimitedStorage(inner, max) отказывается (возвращает false) добавлять новый ключ, когда уже существует max ключей, при этом обновление существующих ключей по-прежнему разрешено. Всё остальное передаётся дальше с помощью by.

Предоставленный код оборачивает MapStorage в LoggingStorage, а если первая строка содержит положительный лимит — также в LimitedStorage. Он выполняет команды put a 1, get a и keys, выводя ok, refused, значение или missing, а также список ключей, и в конце — log: put a=1, ....

Ваш код должен находиться в файлах LoggingStorage.kt, LimitedStorage.kt, Storage.kt и MapStorage.kt. В Main.kt находится предоставленный код ввода/вывода, и его нельзя изменять.

Попробуйте сами

fun main() {
    // Предоставленный код ввода/вывода: оставьте его как есть
    val input = generateSequence(::readLine).toList()
    val base = MapStorage()
    val logging = LoggingStorage(base)
    val limit = input[0].toInt()
    val storage: Storage = if (limit > 0) LimitedStorage(logging, limit) else logging
    for (cmd in input.drop(1)) {
        val p = cmd.split(" ")
        when (p[0]) {
            "put" -> println(if (storage.put(p[1], p[2])) "ok" else "refused")
            "get" -> println(storage.get(p[1]) ?: "missing")
            else -> println(storage.keys())
        }
    }
    println("log: " + logging.log.joinToString(", ").ifEmpty { "empty" })
}
quiz iconПроверьте себя

В этом уроке есть небольшой тест. Начните урок, чтобы ответить на вопросы и сохранить прогресс.

Все уроки раздела Объектно-ориентированное программирование

Потренируйтесь самостоятельно: Песочница Kotlin