--- title: Делегирование description: Делегирование интерфейсов через by, делегаты свойств и границы автоматического перенаправления tags: - kotlin - oop --- Делегирование - передача работы другому объекту. Kotlin поддерживает его на уровне языка словом `by`, поэтому композиция требует не больше кода, чем наследование. ## Делегирование интерфейса ```kotlin interface Logger5 { fun log(msg: String): String fun warn(msg: String): String } class ConsoleLogger3 : Logger5 { override fun log(msg: String) = "[LOG] $msg" override fun warn(msg: String) = "[WARN] $msg" } class Service15(logger: Logger5) : Logger5 by logger { fun work() = log("начинаю работу") } fun main() { val s = Service15(ConsoleLogger3()) println(s.work()) // [LOG] начинаю работу println(s.warn("внимание")) // [WARN] внимание — метод перенаправлен } ``` `Service15` реализует `Logger5`, но код методов не пишет: компилятор генерирует перенаправление на `logger`. Унаследованной реализации при этом нет - есть ссылка на другой объект. ## Переопределение части методов Делегировать можно всё, кроме того, что нужно изменить: ```kotlin interface Repository3 { fun find(id: Int): String? fun save(id: Int, value: String): String } class DbRepository : Repository3 { private val data = mutableMapOf() override fun find(id: Int) = data[id] override fun save(id: Int, value: String): String { data[id] = value return "сохранено" } } class CachingRepository(private val origin: Repository3) : Repository3 by origin { private val cache = mutableMapOf() override fun find(id: Int): String? = // только этот метод свой cache.getOrPut(id) { origin.find(id) ?: return null } } fun main() { val repo = CachingRepository(DbRepository()) println(repo.save(1, "Иван")) // сохранено — делегировано println(repo.find(1)) // Иван — через кеш println(repo.find(1)) // Иван — уже из кеша println(repo.find(99)) // null } ``` Это декоратор в три строки. На наследовании он потребовал бы `open`-класса и был бы связан с его внутренностями. ## Делегирование нескольких интерфейсов ```kotlin interface Reader { fun read(): String } interface Writer2 { fun write(data: String): String } class FileReader2 : Reader { override fun read() = "данные из файла" } class FileWriter2 : Writer2 { override fun write(data: String) = "записано: $data" } class ReadWriteFile( reader: Reader, writer: Writer2, ) : Reader by reader, Writer2 by writer fun main() { val f = ReadWriteFile(FileReader2(), FileWriter2()) println(f.read()) // данные из файла println(f.write("текст")) // записано: текст } ``` Это даёт эффект множественного наследования реализации - но без его проблем, потому что каждый интерфейс обслуживает отдельный объект. ## Расширение коллекции Классическая задача, которую нельзя безопасно решить наследованием: ```kotlin class CountingList3(private val inner: MutableList = mutableListOf()) : MutableList by inner { var addedCount = 0 private set override fun add(element: T): Boolean { addedCount++ return inner.add(element) } override fun addAll(elements: Collection): Boolean { addedCount += elements.size return inner.addAll(elements) } } fun main() { val list = CountingList3() list.add(1) list.addAll(listOf(2, 3)) println("${list.size} элементов, добавлений: ${list.addedCount}") // 3 элементов, добавлений: 3 println(list.toList()) // [1, 2, 3] } ``` При наследовании от `ArrayList` счётчик мог бы удвоиться, если `addAll` внутри вызывает `add`. С делегированием внутренние вызовы `inner` остаются внутри `inner` и наших переопределений не касаются. Обратите внимание на `list.toList()`: сам `println(list)` вывел бы `CountingList3@5fa7e7ff`, потому что `toString` - член `Any`, а члены `Any` делегирование не перенаправляет. Эта же особенность - источник главной ловушки делегирования, см. подводные камни. ## Делегаты свойств Тот же `by` для свойства передаёт работу объекту с методами `getValue` и `setValue`. ### Готовые делегаты ```kotlin import kotlin.properties.Delegates class Form3 { val config: String by lazy { "вычислено один раз" } var name: String by Delegates.observable("") { _, old, new -> println("name: '$old' -> '$new'") } var age: Int by Delegates.vetoable(0) { _, _, new -> new in 0..150 } var port: Int by Delegates.notNull() } fun main() { val f = Form3() println(f.config) // вычислено один раз f.name = "Иван" // name: '' -> 'Иван' f.age = 200 // отклонено println(f.age) // 0 f.port = 8080 println(f.port) // 8080 } ``` Подробнее: [Свойства](Свойства.md) ### Делегирование свойства в Map ```kotlin class Settings(private val map: Map) { val host: String by map val port: Int by map } fun main() { val s = Settings(mapOf("host" to "localhost", "port" to 8080)) println("${s.host}:${s.port}") // localhost:8080 } ``` Имя свойства становится ключом. Для `var` нужен `MutableMap`. ### Свой делегат Нужны операторы `getValue` и (для `var`) `setValue`: ```kotlin import kotlin.reflect.KProperty class NonBlank(private var value: String = "") { operator fun getValue(thisRef: Any?, property: KProperty<*>) = value operator fun setValue(thisRef: Any?, property: KProperty<*>, newValue: String) { require(newValue.isNotBlank()) { "${property.name} не может быть пустым" } value = newValue.trim() } } class User6 { var name: String by NonBlank() } fun main() { val u = User6() u.name = " Иван " println("'${u.name}'") // 'Иван' try { u.name = " " } catch (e: IllegalArgumentException) { println(e.message) // name не может быть пустым } } ``` `property` даёт имя и метаданные свойства - удобно для сообщений об ошибках и для чтения из конфигурации по имени. Для делегата только на чтение достаточно реализовать `ReadOnlyProperty`: ```kotlin import kotlin.properties.ReadOnlyProperty fun envVar(name: String) = ReadOnlyProperty { _, _ -> System.getenv(name) ?: "<не задано>" } class Config7 { val home: String by envVar("HOME") val missing: String by envVar("НЕТ_ТАКОЙ_ПЕРЕМЕННОЙ") } fun main() { println(Config7().home.isNotEmpty()) // true println(Config7().missing) // <не задано> } ``` ### provideDelegate Позволяет проверить свойство при создании делегата - например, что имя есть в конфигурации: ```kotlin import kotlin.reflect.KProperty class Required(private val source: Map) { operator fun provideDelegate(thisRef: Any?, property: KProperty<*>): Required { require(property.name in source) { "нет ключа '${property.name}'" } return this } operator fun getValue(thisRef: Any?, property: KProperty<*>) = source[property.name]!! } fun main() { val source = mapOf("host" to "localhost") class Ok3 { val host: String by Required(source) } println(Ok3().host) // localhost try { class Bad4 { val port: String by Required(source) } Bad4() } catch (e: IllegalArgumentException) { println(e.message) // нет ключа 'port' } } ``` Проверка происходит при создании объекта, а не при первом чтении - ошибка обнаруживается раньше. > [!info] Отличия от Java > | Java | Kotlin | > |---|---| > | все методы перенаправлять вручную | `by` генерирует их | > | декоратор = класс с полем и 10 методами | `class D(x: I) : I by x` | > | `Supplier` + `volatile` для ленивости | `by lazy` | > | `PropertyChangeSupport` | `Delegates.observable` | > | нет аналога делегатов свойств | `by` с `getValue`/`setValue` | > > **Делегирование интерфейса - главная экономия.** В Java декоратор над интерфейсом из десяти методов требует десять методов-перенаправлений, даже если менять нужно один. Любой новый метод в интерфейсе нужно добавить во все декораторы. Kotlin генерирует перенаправление сам: > ```java > class CachingRepository implements Repository { > private final Repository origin; > CachingRepository(Repository origin) { this.origin = origin; } > public String save(int id, String v) { return origin.save(id, v); } // шум > public String find(int id) { /* своя логика */ } > } > ``` > > **Делегатов свойств в Java нет совсем.** Ленивое поле требует ручной двойной проверки блокировки, наблюдаемое - `PropertyChangeSupport` и явных вызовов. В Kotlin это `by lazy` и `by Delegates.observable`. > > **Паттерн Декоратор перестаёт быть паттерном** - он становится однострочной конструкцией языка, поэтому его применяют там, где в Java предпочли бы наследование «чтобы не писать много кода». > > Сравнить: [Полиморфизм подтипов в Java](../../Программирование%20на%20Java/ООП/Полиморфизм/Полиморфизм%20подтипов.md), [Абстрактные классы](../../Программирование%20на%20Java/ООП/Абстрактные%20классы.md) > [!warning] Подводные камни > **Делегат не знает о переопределениях - это главная ловушка.** Если один метод делегата вызывает другой, он вызовет **свою** версию, а не вашу: > ```kotlin > interface Sender { > fun send(msg: String): String > fun sendAll(msgs: List): String > } > > class BasicSender : Sender { > override fun send(msg: String) = "отправлено: $msg" > override fun sendAll(msgs: List) = msgs.joinToString { send(it) } > } > > class LoggingSender(private val inner: Sender) : Sender by inner { > var logged = 0 > private set > > override fun send(msg: String): String { > logged++ > return inner.send(msg) > } > } > > fun main() { > val s = LoggingSender(BasicSender()) > s.sendAll(listOf("a", "b")) > println("залогировано: ${s.logged}") // 0 — наш send не вызвался! > } > ``` > `sendAll` делегирован и внутри вызывает `send` объекта `inner`. Чтобы счётчик работал, нужно переопределить и `sendAll`. Это обратная сторона изоляции: наследование здесь сработало бы «как ожидается», но стало бы хрупким. > > **Члены `Any` не делегируются.** `toString`, `equals` и `hashCode` остаются реализациями по умолчанию, то есть по ссылке: > ```kotlin > class Wrapped(private val inner: MutableList = mutableListOf()) : MutableList by inner > > fun main() { > val a = Wrapped().apply { add(1) } > val b = Wrapped().apply { add(1) } > println(a.toString()) // Wrapped@4554617c, а не [1] > println(a == b) // false, хотя содержимое одинаково > println(a == listOf(1)) // false > } > ``` > Обёртка над коллекцией, попавшая в `Set` или в сравнение, поведёт себя не как коллекция. Эти три метода нужно переопределять вручную. Подробнее: [equals, hashCode и toString](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/equals,%20hashCode%20и%20toString.md) > > **Делегат фиксируется при создании** и не может быть заменён: > ```kotlin > fun interface Source { fun get(): String } > > class Service16(private var source: Source) : Source by source { > fun replace(newSource: Source) { source = newSource } // не влияет на by! > } > > fun main() { > val s = Service16 { "первый" } > s.replace { "второй" } > println(s.get()) // первый — by хранит исходную ссылку > } > ``` > `by` копирует ссылку в скрытое поле при создании объекта. Для подменяемого делегата нужно перенаправлять вручную. > > **Делегирование нельзя сделать `by this`** или сослаться на собственное свойство в заголовке - объект ещё не создан. > > **Из делегированных методов нет доступа к `this` обёртки.** Делегат не знает, что его кто-то оборачивает, - он не может вызвать методы обёртки. > > **`by lazy` на `var` невозможен** - только `val`. > > **Делегат свойства - объект на каждый экземпляр:** > ```kotlin > class Heavy { > val a: String by lazy { "a" } > val b: String by lazy { "b" } // ещё один объект Lazy > } > > fun main() { println(Heavy().a + Heavy().b) } // ab > ``` > Для класса с десятками ленивых свойств, создаваемого миллионами экземпляров, это заметный расход памяти. > > **Делегирование в `Map` падает в рантайме при отсутствии ключа:** > ```kotlin > class Settings2(map: Map) { > val host: String by map > } > > fun main() { > val s = Settings2(emptyMap()) > try { > println(s.host) > } catch (e: NoSuchElementException) { > println("ключ отсутствует") // ключ отсутствует > } > } > ``` > Проверить это заранее помогает `provideDelegate`. > > **Делегирование `MutableList` раскрывает полный изменяемый API.** Обёртка, делегирующая `MutableList`, позволяет внешнему коду добавлять элементы в обход вашей логики - через `iterator().remove()`, `subList()` и другие пути. Дополнительно: [Свойства](Свойства.md) Далее: [Вложенные и внутренние классы](Вложенные%20и%20внутренние%20классы.md)