Files
obsidian/Kotlin/ООП/Делегирование.md
T

373 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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<Int, String>()
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<Int, String>()
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<T>(private val inner: MutableList<T> = mutableListOf())
: MutableList<T> by inner {
var addedCount = 0
private set
override fun add(element: T): Boolean {
addedCount++
return inner.add(element)
}
override fun addAll(elements: Collection<T>): Boolean {
addedCount += elements.size
return inner.addAll(elements)
}
}
fun main() {
val list = CountingList3<Int>()
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<String, Any?>) {
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<Any?, String> { _, _ ->
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<String, String>) {
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>): String
> }
>
> class BasicSender : Sender {
> override fun send(msg: String) = "отправлено: $msg"
> override fun sendAll(msgs: List<String>) = 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<T>(private val inner: MutableList<T> = mutableListOf()) : MutableList<T> by inner
>
> fun main() {
> val a = Wrapped<Int>().apply { add(1) }
> val b = Wrapped<Int>().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<String, Any?>) {
> 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)