Kotlin guide done. (20.10.26)
This commit is contained in:
@@ -0,0 +1,577 @@
|
||||
---
|
||||
title: Паттерны проектирования
|
||||
description: Какие паттерны GoF встроены в язык, какие упростились и какие остались нужны
|
||||
tags:
|
||||
- kotlin
|
||||
- functional
|
||||
- patterns
|
||||
---
|
||||
|
||||
Многие паттерны проектирования - это обходные приёмы для того, чего не хватает языку. Kotlin часть этих возможностей встроил, поэтому соответствующие паттерны сводятся к одной конструкции.
|
||||
|
||||
| Паттерн | В Kotlin |
|
||||
|---|---|
|
||||
| Singleton | `object` |
|
||||
| Builder | именованные аргументы, `apply`, DSL |
|
||||
| Factory Method | функция верхнего уровня или `invoke` в companion |
|
||||
| Strategy | функциональный тип |
|
||||
| Decorator | делегирование `by` |
|
||||
| Observer | `Flow`, `StateFlow` или список лямбд |
|
||||
| Template Method | функция высшего порядка |
|
||||
| Command | лямбда |
|
||||
| Adapter | функции-расширения |
|
||||
| Visitor | `sealed` + `when` |
|
||||
| Prototype | `copy()` у `data class` |
|
||||
| Null Object | nullable-тип и `?:` |
|
||||
| Facade, Repository, Proxy | остаются как есть |
|
||||
|
||||
## Singleton
|
||||
```kotlin
|
||||
object Settings {
|
||||
private val values = mutableMapOf<String, String>()
|
||||
|
||||
var theme: String = "light"
|
||||
|
||||
fun put(key: String, value: String) { values[key] = value }
|
||||
fun get(key: String): String? = values[key]
|
||||
}
|
||||
|
||||
fun main() {
|
||||
Settings.theme = "dark"
|
||||
Settings.put("lang", "ru")
|
||||
println("${Settings.theme} ${Settings.get("lang")}") // dark ru
|
||||
}
|
||||
```
|
||||
`object` - потокобезопасный ленивый синглтон: экземпляр создаётся при первом обращении, инициализация защищена загрузчиком классов. Никакого `getInstance()`, `volatile` и двойной проверки блокировки. Подробнее: [Object и companion object](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/Object%20и%20companion%20object.md)
|
||||
|
||||
Цена та же, что у любого синглтона: глобальное состояние, которое трудно подменить в тестах. Если объект нужно мокировать - он должен реализовать интерфейс и внедряться параметром.
|
||||
|
||||
## Builder
|
||||
Три варианта по возрастанию сложности. Первый - именованные аргументы с значениями по умолчанию:
|
||||
```kotlin
|
||||
data class HttpRequest(
|
||||
val url: String,
|
||||
val method: String = "GET",
|
||||
val timeoutMs: Int = 5_000,
|
||||
val headers: Map<String, String> = emptyMap(),
|
||||
val retries: Int = 0,
|
||||
)
|
||||
|
||||
fun main() {
|
||||
val r = HttpRequest(url = "https://example.com", method = "POST", retries = 3)
|
||||
println(r)
|
||||
// HttpRequest(url=https://example.com, method=POST, timeoutMs=5000, headers={}, retries=3)
|
||||
}
|
||||
```
|
||||
Это закрывает подавляющее большинство случаев, ради которых пишут builder. Плюс к этому `copy` даёт модификацию:
|
||||
```kotlin
|
||||
data class HttpRequest(
|
||||
val url: String,
|
||||
val method: String = "GET",
|
||||
val headers: Map<String, String> = emptyMap(),
|
||||
)
|
||||
|
||||
fun main() {
|
||||
val base = HttpRequest("https://example.com")
|
||||
val withAuth = base.copy(headers = mapOf("Authorization" to "Bearer x"))
|
||||
println(withAuth.headers) // {Authorization=Bearer x}
|
||||
println(base.headers) // {} — оригинал цел
|
||||
}
|
||||
```
|
||||
|
||||
Второй вариант - `apply` для классов с изменяемыми свойствами (включая чужие):
|
||||
```kotlin
|
||||
fun main() {
|
||||
val sb = StringBuilder().apply {
|
||||
append("строка")
|
||||
append(" ")
|
||||
append(42)
|
||||
}
|
||||
println(sb) // строка 42
|
||||
}
|
||||
```
|
||||
|
||||
Третий - DSL, когда структура вложенная:
|
||||
```kotlin
|
||||
class Form2 {
|
||||
private val fields = mutableListOf<String>()
|
||||
fun field(name: String, required: Boolean = false) {
|
||||
fields.add(if (required) "$name*" else name)
|
||||
}
|
||||
fun render() = fields.joinToString(", ")
|
||||
}
|
||||
|
||||
fun form(block: Form2.() -> Unit) = Form2().apply(block).render()
|
||||
|
||||
fun main() {
|
||||
println(form {
|
||||
field("имя", required = true)
|
||||
field("телефон")
|
||||
})
|
||||
// имя*, телефон
|
||||
}
|
||||
```
|
||||
Подробнее: [DSL](DSL.md)
|
||||
|
||||
## Factory Method
|
||||
```kotlin
|
||||
sealed interface Shape2 {
|
||||
data class Circle2(val radius: Double) : Shape2
|
||||
data class Square(val side: Double) : Shape2
|
||||
}
|
||||
|
||||
fun shapeOf(type: String, size: Double): Shape2? = when (type) {
|
||||
"circle" -> Shape2.Circle2(size)
|
||||
"square" -> Shape2.Square(size)
|
||||
else -> null
|
||||
}
|
||||
|
||||
fun main() {
|
||||
println(shapeOf("circle", 2.0)) // Circle2(radius=2.0)
|
||||
println(shapeOf("unknown", 1.0)) // null
|
||||
}
|
||||
```
|
||||
Отдельный класс-фабрика не нужен: функция верхнего уровня делает то же. Когда фабрика должна выглядеть как конструктор, помогает `invoke` в companion:
|
||||
```kotlin
|
||||
class PositiveNumber private constructor(val value: Int) {
|
||||
companion object {
|
||||
operator fun invoke(raw: Int): PositiveNumber? =
|
||||
if (raw > 0) PositiveNumber(raw) else null
|
||||
}
|
||||
override fun toString() = "+$value"
|
||||
}
|
||||
|
||||
fun main() {
|
||||
println(PositiveNumber(5)) // +5
|
||||
println(PositiveNumber(-1)) // null
|
||||
}
|
||||
```
|
||||
Настоящий конструктор не может вернуть `null` или кешированный экземпляр - здесь это возможно, а вызов выглядит как `PositiveNumber(5)`. Подробнее: [Инфиксные и операторные функции](Инфиксные%20и%20операторные%20функции.md)
|
||||
|
||||
## Strategy
|
||||
Классический паттерн - интерфейс с одним методом и набор реализаций. В Kotlin это функциональный тип:
|
||||
```kotlin
|
||||
class Checkout(private val discount: (Long) -> Long) {
|
||||
fun total(amount: Long) = discount(amount)
|
||||
}
|
||||
|
||||
fun main() {
|
||||
val none = Checkout { it }
|
||||
val tenPercent = Checkout { it * 90 / 100 }
|
||||
val fixed = Checkout { (it - 100).coerceAtLeast(0) }
|
||||
|
||||
println("${none.total(1000)} ${tenPercent.total(1000)} ${fixed.total(1000)}")
|
||||
// 1000 900 900
|
||||
}
|
||||
```
|
||||
Стратегии становятся значениями: их можно хранить в `Map`, выбирать по ключу, комбинировать:
|
||||
```kotlin
|
||||
fun main() {
|
||||
val strategies: Map<String, (Long) -> Long> = mapOf(
|
||||
"none" to { it },
|
||||
"ten" to { it * 90 / 100 },
|
||||
"half" to { it / 2 },
|
||||
)
|
||||
|
||||
val chosen = strategies["ten"] ?: strategies.getValue("none")
|
||||
println(chosen(1000)) // 900
|
||||
|
||||
val combined: (Long) -> Long = { strategies.getValue("ten")(strategies.getValue("half")(it)) }
|
||||
println(combined(1000)) // 450
|
||||
}
|
||||
```
|
||||
Когда у стратегии больше одной операции - интерфейс остаётся правильным выбором.
|
||||
|
||||
## Decorator
|
||||
```kotlin
|
||||
interface Repository8 {
|
||||
fun find(id: Int): String?
|
||||
}
|
||||
|
||||
class DatabaseRepository : Repository8 {
|
||||
override fun find(id: Int): String? = if (id == 1) "Иван" else null
|
||||
}
|
||||
|
||||
class CachingRepository(private val delegate: Repository8) : Repository8 by delegate {
|
||||
private val cache = mutableMapOf<Int, String?>()
|
||||
|
||||
override fun find(id: Int): String? = cache.getOrPut(id) {
|
||||
println("запрос в БД: $id")
|
||||
delegate.find(id)
|
||||
}
|
||||
}
|
||||
|
||||
class LoggingRepository(private val delegate: Repository8) : Repository8 by delegate
|
||||
|
||||
fun main() {
|
||||
val repo = LoggingRepository(CachingRepository(DatabaseRepository()))
|
||||
println(repo.find(1)) // запрос в БД: 1 \n Иван
|
||||
println(repo.find(1)) // Иван — из кеша
|
||||
}
|
||||
```
|
||||
`by delegate` генерирует делегирующие реализации для всех методов интерфейса, а переопределить нужно только изменённые. В Java каждый декоратор обязан вручную прописать все методы интерфейса. Подробнее: [Делегирование](../ООП/Делегирование.md)
|
||||
|
||||
## Observer
|
||||
Простая версия - список лямбд:
|
||||
```kotlin
|
||||
class Publisher<T> {
|
||||
private val subscribers = mutableListOf<(T) -> Unit>()
|
||||
|
||||
fun subscribe(handler: (T) -> Unit): () -> Unit {
|
||||
subscribers.add(handler)
|
||||
return { subscribers.remove(handler) } // функция отписки
|
||||
}
|
||||
|
||||
fun publish(event: T) = subscribers.forEach { it(event) }
|
||||
}
|
||||
|
||||
fun main() {
|
||||
val publisher = Publisher<String>()
|
||||
val unsubscribe = publisher.subscribe { println("получено: $it") }
|
||||
publisher.subscribe { println("второй: $it") }
|
||||
|
||||
publisher.publish("событие 1")
|
||||
// получено: событие 1
|
||||
// второй: событие 1
|
||||
|
||||
unsubscribe()
|
||||
publisher.publish("событие 2")
|
||||
// второй: событие 2
|
||||
}
|
||||
```
|
||||
Возврат функции отписки вместо метода `unsubscribe(handler)` избавляет от хранения ссылки на обработчик - частая причина утечек в Java.
|
||||
|
||||
Для реальных задач вместо своей реализации берут `StateFlow` или `SharedFlow`: там уже решены обратное давление, конкурентность и жизненный цикл подписки. Подробнее: [StateFlow и SharedFlow](../Корутины/StateFlow%20и%20SharedFlow.md)
|
||||
|
||||
Для одного наблюдаемого свойства есть готовый делегат:
|
||||
```kotlin
|
||||
import kotlin.properties.Delegates
|
||||
|
||||
class Model {
|
||||
var status: String by Delegates.observable("новый") { _, old, new ->
|
||||
println("статус: $old -> $new")
|
||||
}
|
||||
}
|
||||
|
||||
fun main() {
|
||||
val m = Model()
|
||||
m.status = "в работе" // статус: новый -> в работе
|
||||
m.status = "готов" // статус: в работе -> готов
|
||||
}
|
||||
```
|
||||
|
||||
## Template Method
|
||||
Паттерн, где базовый класс задаёт скелет алгоритма, а подклассы заполняют шаги. В Kotlin это функция с лямбдами:
|
||||
```kotlin
|
||||
fun <T> processFile(
|
||||
lines: List<String>,
|
||||
parse: (String) -> T?,
|
||||
onError: (String) -> Unit = { println("пропущено: $it") },
|
||||
): List<T> = lines.mapNotNull { line ->
|
||||
parse(line) ?: null.also { onError(line) }
|
||||
}
|
||||
|
||||
fun main() {
|
||||
val result = processFile(
|
||||
lines = listOf("1", "два", "3"),
|
||||
parse = { it.toIntOrNull() },
|
||||
)
|
||||
println(result)
|
||||
// пропущено: два
|
||||
// [1, 3]
|
||||
}
|
||||
```
|
||||
Наследование не нужно: шаги передаются параметрами. Это и проще (нет иерархии), и гибче (шаги можно комбинировать в рантайме).
|
||||
|
||||
## Command
|
||||
```kotlin
|
||||
class History {
|
||||
private val undoStack = mutableListOf<() -> Unit>()
|
||||
|
||||
fun execute(action: () -> Unit, undo: () -> Unit) {
|
||||
action()
|
||||
undoStack.add(undo)
|
||||
}
|
||||
|
||||
fun undo() { undoStack.removeLastOrNull()?.invoke() }
|
||||
}
|
||||
|
||||
fun main() {
|
||||
val text = StringBuilder()
|
||||
val history = History()
|
||||
|
||||
history.execute({ text.append("Привет") }, { text.setLength(0) })
|
||||
println(text) // Привет
|
||||
history.undo()
|
||||
println("[$text]") // []
|
||||
}
|
||||
```
|
||||
Интерфейс `Command` с методом `execute` заменяется лямбдой. Если у команды есть состояние или несколько операций - `data class` с методами честнее.
|
||||
|
||||
## Visitor
|
||||
Паттерн нужен там, где над закрытой иерархией выполняют разные операции. В Kotlin это `sealed` + `when`:
|
||||
```kotlin
|
||||
sealed interface Expr {
|
||||
data class Num(val value: Int) : Expr
|
||||
data class Add(val left: Expr, val right: Expr) : Expr
|
||||
data class Mul(val left: Expr, val right: Expr) : Expr
|
||||
}
|
||||
|
||||
fun eval(e: Expr): Int = when (e) {
|
||||
is Expr.Num -> e.value
|
||||
is Expr.Add -> eval(e.left) + eval(e.right)
|
||||
is Expr.Mul -> eval(e.left) * eval(e.right)
|
||||
}
|
||||
|
||||
fun render(e: Expr): String = when (e) {
|
||||
is Expr.Num -> e.value.toString()
|
||||
is Expr.Add -> "(${render(e.left)} + ${render(e.right)})"
|
||||
is Expr.Mul -> "${render(e.left)} * ${render(e.right)}"
|
||||
}
|
||||
|
||||
fun main() {
|
||||
val expr = Expr.Add(Expr.Num(2), Expr.Mul(Expr.Num(3), Expr.Num(4)))
|
||||
println(render(expr)) // (2 + 3 * 4)
|
||||
println(eval(expr)) // 14
|
||||
}
|
||||
```
|
||||
Новая операция - новая функция, менять классы не нужно. Компилятор проверяет полноту `when`, поэтому добавление варианта в `sealed` сразу покажет все места, требующие правки - то, чего Visitor добивается ценой двойной диспетчеризации. Подробнее: [Sealed class и interface](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/Sealed%20class%20и%20interface.md)
|
||||
|
||||
## Prototype и Null Object
|
||||
```kotlin
|
||||
data class Config4(val host: String, val port: Int, val debug: Boolean = false)
|
||||
|
||||
fun main() {
|
||||
val prod = Config4("example.com", 443)
|
||||
val staging = prod.copy(host = "staging.example.com", debug = true)
|
||||
println(staging)
|
||||
// Config4(host=staging.example.com, port=443, debug=true)
|
||||
}
|
||||
```
|
||||
`copy` - это Prototype, встроенный в `data class`. Подробнее: [Data class](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/Data%20class.md)
|
||||
|
||||
Null Object создавался, чтобы избежать проверок на `null`. Nullable-типы решают ту же задачу явнее:
|
||||
```kotlin
|
||||
interface Logger3 { fun log(m: String) }
|
||||
|
||||
fun process6(data: String, logger: Logger3? = null) {
|
||||
logger?.log("начало")
|
||||
println("обработано: $data")
|
||||
logger?.log("конец")
|
||||
}
|
||||
|
||||
fun main() {
|
||||
process6("x") // обработано: x
|
||||
process6("y", object : Logger3 { override fun log(m: String) = println("[$m]") })
|
||||
// [начало]
|
||||
// обработано: y
|
||||
// [конец]
|
||||
}
|
||||
```
|
||||
Пустая реализация-заглушка не нужна: `?.` не вызывает метод при `null`. Подробнее: [Операторы null-безопасности](../Синтаксис%20и%20типы/Null-безопасность/Операторы%20null-безопасности.md)
|
||||
|
||||
## Что остаётся нужным
|
||||
Паттерны, описывающие **архитектуру**, а не обход ограничений языка, остаются актуальными: Repository, Facade, Adapter между слоями, Strategy с несколькими методами, State-машина, Chain of Responsibility.
|
||||
|
||||
```kotlin
|
||||
sealed interface OrderState {
|
||||
data object New : OrderState
|
||||
data object Paid : OrderState
|
||||
data class Shipped(val track: String) : OrderState
|
||||
data object Cancelled : OrderState
|
||||
}
|
||||
|
||||
fun next(state: OrderState, event: String): OrderState = when (state) {
|
||||
OrderState.New -> when (event) {
|
||||
"pay" -> OrderState.Paid
|
||||
"cancel" -> OrderState.Cancelled
|
||||
else -> state
|
||||
}
|
||||
OrderState.Paid -> if (event == "ship") OrderState.Shipped("TRACK-1") else state
|
||||
is OrderState.Shipped -> state
|
||||
OrderState.Cancelled -> state
|
||||
}
|
||||
|
||||
fun main() {
|
||||
var state: OrderState = OrderState.New
|
||||
for (event in listOf("pay", "ship", "cancel")) {
|
||||
state = next(state, event)
|
||||
println("$event -> $state")
|
||||
}
|
||||
// pay -> Paid
|
||||
// ship -> Shipped(track=TRACK-1)
|
||||
// cancel -> Shipped(track=TRACK-1)
|
||||
}
|
||||
```
|
||||
Здесь `sealed` + `when` не отменяют паттерн State, а дают ему надёжную реализацию: невозможное состояние не создать, а забытый переход не скомпилируется.
|
||||
|
||||
> [!info] Отличия от Java
|
||||
> | Паттерн в Java | В Kotlin |
|
||||
> |---|---|
|
||||
> | `private static final INSTANCE` + `getInstance()` | `object` |
|
||||
> | класс `Builder` с 10 методами и `build()` | именованные аргументы |
|
||||
> | `interface Strategy` + 3 класса | `(T) -> R` |
|
||||
> | декоратор с ручным проксированием всех методов | `by delegate` |
|
||||
> | `List<Listener>` + `addListener`/`removeListener` | лямбды или `Flow` |
|
||||
> | `abstract class` с `protected abstract` шагами | функция с лямбдами |
|
||||
> | `interface Visitor` с `visitX` на каждый тип | `sealed` + `when` |
|
||||
> | `clone()` или конструктор копирования | `copy()` |
|
||||
> | `NullLogger implements Logger {}` | `Logger?` + `?.` |
|
||||
>
|
||||
> **Главная разница - в количестве кода на тот же смысл.** Builder для класса с пятью полями в Java - это ~60 строк (вложенный класс, поле на каждый параметр, метод на каждый параметр, `build()`). В Kotlin это объявление `data class` с значениями по умолчанию, то есть ноль дополнительных строк. То же с Singleton (было: приватный конструктор, статическое поле, `getInstance`, возможно двойная проверка блокировки; стало: `object`) и Strategy (было: интерфейс плюс класс на каждую реализацию; стало: лямбда).
|
||||
>
|
||||
> **Декоратор - самый показательный случай.** В Java, чтобы задекорировать интерфейс с 15 методами, нужно написать 15 делегирующих методов, из которых интересен один. Отсюда классы вроде `HttpServletRequestWrapper` в стандартной библиотеке - их пишут специально, чтобы наследоваться. В Kotlin `by delegate` делает это автоматически:
|
||||
> ```kotlin
|
||||
> interface Big {
|
||||
> fun a(): Int
|
||||
> fun b(): Int
|
||||
> fun c(): Int
|
||||
> }
|
||||
>
|
||||
> class Impl : Big {
|
||||
> override fun a() = 1
|
||||
> override fun b() = 2
|
||||
> override fun c() = 3
|
||||
> }
|
||||
>
|
||||
> class OnlyBChanged(d: Big) : Big by d {
|
||||
> override fun b() = 99
|
||||
> }
|
||||
>
|
||||
> fun main() {
|
||||
> val x = OnlyBChanged(Impl())
|
||||
> println("${x.a()} ${x.b()} ${x.c()}") // 1 99 3
|
||||
> }
|
||||
> ```
|
||||
>
|
||||
> **Паттерны не исчезли - исчез их boilerplate.** Смысл Strategy (вынести изменяемое поведение) остался и в Kotlin, просто выражается функциональным типом. Полезно знать паттерны именно как словарь: «здесь Strategy» понятнее, чем «здесь лямбда в конструкторе».
|
||||
>
|
||||
> **Некоторые паттерны в Kotlin не нужны вообще.** Null Object существовал, чтобы не писать `if (x != null)`; при наличии `?.` он только добавляет класс. Double-Checked Locking для синглтона не нужен, потому что `object` и `lazy` уже потокобезопасны.
|
||||
>
|
||||
> Сравнить: [Singleton в Java](../../Программирование%20на%20Java/Паттерны%20проектирования/Singleton.md), [Builder](../../Программирование%20на%20Java/Паттерны%20проектирования/Builder.md)
|
||||
|
||||
> [!warning] Подводные камни
|
||||
> **`object` - глобальное состояние, которое не подменить в тестах:**
|
||||
> ```kotlin
|
||||
> object Clock2 {
|
||||
> fun now(): Long = System.currentTimeMillis()
|
||||
> }
|
||||
>
|
||||
> interface Clock3 { fun now(): Long }
|
||||
>
|
||||
> class Service4(private val clock: Clock3) {
|
||||
> fun timestamp() = clock.now()
|
||||
> }
|
||||
>
|
||||
> fun main() {
|
||||
> val fake = object : Clock3 { override fun now() = 1_000L }
|
||||
> println(Service4(fake).timestamp()) // 1000 — тест детерминирован
|
||||
> println(Clock2.now() > 0) // true — а это не подменить
|
||||
> }
|
||||
> ```
|
||||
> Между `object` и интерфейсом с внедрением выбирайте второе, если поведение нужно будет подменить.
|
||||
>
|
||||
> **`object` с изменяемым состоянием не потокобезопасен.** Создание экземпляра защищено, а доступ к полям - нет:
|
||||
> ```kotlin
|
||||
> object Counter5 {
|
||||
> var count = 0 // гонка при обращении из нескольких потоков
|
||||
> }
|
||||
>
|
||||
> fun main() {
|
||||
> Counter5.count++
|
||||
> println(Counter5.count) // 1
|
||||
> }
|
||||
> ```
|
||||
>
|
||||
> **Стратегия-лямбда теряет имя в стеке и логах:**
|
||||
> ```kotlin
|
||||
> fun main() {
|
||||
> val strategies: Map<String, (Int) -> Int> = mapOf("double" to { it * 2 })
|
||||
> val f = strategies.getValue("double")
|
||||
> println(f::class.simpleName?.contains("Lambda")) // true — имя сгенерировано
|
||||
> }
|
||||
> ```
|
||||
> Вместо осмысленного имени получается что-то вроде `MainKt$$Lambda/0x00001e0001040620` - с адресом, меняющимся от запуска к запуску. Отладка «какая стратегия сработала» требует хранить имя рядом. Если это важно - интерфейс или `data class` с полем `name`.
|
||||
>
|
||||
> **`by delegate` делегирует только методы интерфейса.** `equals`, `hashCode` и `toString` берутся из `Any`, а не из делегата:
|
||||
> ```kotlin
|
||||
> interface Named5 { val name: String }
|
||||
> data class Impl2(override val name: String) : Named5
|
||||
>
|
||||
> class Wrapper(d: Named5) : Named5 by d
|
||||
>
|
||||
> fun main() {
|
||||
> val w = Wrapper(Impl2("x"))
|
||||
> println(w.name) // x
|
||||
> println(w.toString().startsWith("Wrapper")) // true — не делегировано
|
||||
> println(w == Wrapper(Impl2("x"))) // false
|
||||
> }
|
||||
> ```
|
||||
>
|
||||
> **Делегат фиксируется при создании:**
|
||||
> ```kotlin
|
||||
> interface Source { fun value(): Int }
|
||||
>
|
||||
> class Changing(private var inner: Source) : Source by inner {
|
||||
> fun replace(newInner: Source) { inner = newInner }
|
||||
> }
|
||||
>
|
||||
> fun main() {
|
||||
> val first = object : Source { override fun value() = 1 }
|
||||
> val second = object : Source { override fun value() = 2 }
|
||||
> val c = Changing(first)
|
||||
> c.replace(second)
|
||||
> println(c.value()) // 1 — делегирует первому!
|
||||
> }
|
||||
> ```
|
||||
> `by` запоминает ссылку в момент конструирования. Для подменяемого делегата нужно явное проксирование метода.
|
||||
>
|
||||
> **Свой Observer течёт по памяти:**
|
||||
> ```kotlin
|
||||
> class Leaky3 {
|
||||
> private val handlers = mutableListOf<(Int) -> Unit>()
|
||||
> fun subscribe(h: (Int) -> Unit) { handlers.add(h) }
|
||||
> fun count() = handlers.size
|
||||
> }
|
||||
>
|
||||
> fun main() {
|
||||
> val p = Leaky3()
|
||||
> repeat(3) { p.subscribe { } }
|
||||
> println(p.count()) // 3 — и никогда не уменьшится
|
||||
> }
|
||||
> ```
|
||||
> Без механизма отписки подписчики живут до конца жизни издателя и держат свои замыкания. Поэтому для событий лучше `Flow` с привязкой к области корутины. Подробнее: [Структурированная конкурентность](../Корутины/Структурированная%20конкурентность.md)
|
||||
>
|
||||
> **Лямбда в коллекции не сравнивается:**
|
||||
> ```kotlin
|
||||
> fun main() {
|
||||
> val handlers = mutableListOf<() -> Unit>()
|
||||
> val h = { println("x") }
|
||||
> handlers.add(h)
|
||||
> println(handlers.remove(h)) // true — та же ссылка
|
||||
> handlers.add { println("x") }
|
||||
> println(handlers.remove { println("x") }) // false — другой объект
|
||||
> }
|
||||
> ```
|
||||
> Отписка по «такой же» лямбде не работает - нужна именно та ссылка или токен отписки.
|
||||
>
|
||||
> **Builder через `apply` на `val`-полях невозможен** - нужны `var`, а значит объект изменяем после сборки. Для неизменяемого результата используйте `data class` с конструктором или отдельный билдер, возвращающий immutable-объект.
|
||||
>
|
||||
> **Именованные аргументы не защищают от неверного сочетания.** `HttpRequest(url = "x", retries = -5)` скомпилируется. Валидацию нужно писать в `init`:
|
||||
> ```kotlin
|
||||
> data class Retry(val attempts: Int) {
|
||||
> init { require(attempts >= 0) { "attempts не может быть отрицательным" } }
|
||||
> }
|
||||
>
|
||||
> fun main() {
|
||||
> println(Retry(3))
|
||||
> try { Retry(-1) } catch (e: IllegalArgumentException) { println(e.message) }
|
||||
> // Retry(attempts=3)
|
||||
> // attempts не может быть отрицательным
|
||||
> }
|
||||
> ```
|
||||
> Подробнее: [Проверки и контракты](../Исключения/Проверки%20и%20контракты.md)
|
||||
>
|
||||
> **Применение паттерна там, где он не нужен, остаётся главной проблемой** - и язык от этого не спасает. Фабрика для класса с одним конструктором, Strategy с единственной реализацией и Repository поверх одного SQL-запроса добавляют уровни без выгоды. Критерий прежний: паттерн оправдан, когда вариативность уже существует, а не когда она предполагается.
|
||||
|
||||
Далее: [Корутины](../Корутины/Корутины.md)
|
||||
Reference in New Issue
Block a user