Паттерн «Наблюдатель»
Часть раздела Объектно-ориентированное программирование путешествия по Kotlin на Coddy. Урок 44 из 57.
Паттерн наблюдатель позволяет объектам подписываться на события другого объекта — субъекта. Субъект хранит список наблюдателей и уведомляет каждого из них, когда что-то происходит, не зная, что они с этим делают:
interface PriceListener {
fun priceChanged(symbol: String, price: Int)
}
class Stock(val symbol: String) {
private val listeners = mutableListOf<PriceListener>()
var price = 0
set(value) {
field = value
for (l in listeners) l.priceChanged(symbol, value)
}
fun subscribe(l: PriceListener) { listeners.add(l) }
}
class Ticker : PriceListener {
override fun priceChanged(symbol: String, price: Int) = println("$symbol is now $price")
}Внутри main:
val acme = Stock("ACME")
acme.subscribe(Ticker())
acme.price = 120
acme.price = 95Вывод:
ACME is now 120
ACME is now 95В Kotlin наблюдатель часто является обычной функцией. Субъект хранит лямбда-выражения и вызывает каждое из них, поэтому для нового наблюдателя не нужен класс:
class Button(val label: String) {
private val handlers = mutableListOf<(String) -> Unit>()
fun onClick(handler: (String) -> Unit) { handlers.add(handler) }
fun click() { for (h in handlers) h(label) }
}Внутри main:
var clicks = 0
val save = Button("Save")
save.onClick { println("$it clicked") }
save.onClick { clicks++ }
save.click()
save.click()
println("clicks: $clicks")Вывод:
Save clicked
Save clicked
clicks: 2Наблюдатель может захотеть прекратить прослушивание. Если subscribe возвращает функцию, которая снова удаляет наблюдателя, вызывающий код может отменить подписку, не получая доступа к списку субъекта:
class Feed {
private val readers = mutableListOf<(String) -> Unit>()
fun subscribe(reader: (String) -> Unit): () -> Unit {
readers.add(reader)
return { readers.remove(reader) }
}
fun post(text: String) { for (r in readers.toList()) r(text) }
}Внутри main:
val feed = Feed()
val cancel = feed.subscribe { println("ada reads: $it") }
feed.subscribe { println("bo reads: $it") }
feed.post("hello")
cancel()
feed.post("again")Вывод:
ada reads: hello
bo reads: hello
bo reads: againВ стандартной библиотеке есть наблюдаемое свойство. Delegates.observable(initial) { property, old, new -> ... } запускает лямбда-выражение после каждого присваивания; в главе 10 объясняется используемое им ключевое слово by:
import kotlin.properties.Delegates
class Thermostat {
var target: Int by Delegates.observable(20) { _, old, new ->
println("target changed from $old to $new")
}
}Внутри main:
val t = Thermostat()
t.target = 22
t.target = 18Вывод:
target changed from 20 to 22
target changed from 22 to 18Задание
ЛегкоWeatherStation уведомляет наблюдателей, реализующих предоставленный интерфейс TemperatureObserver. Напишите attach(observer), detach(observer) и record(temp), которые уведомляют каждого присоединённого наблюдателя, но только если температура отличается от предыдущего показания (первое показание всегда вызывает уведомление). Напишите трёх наблюдателей: Display сохраняет строки display: 21 в lines, Alarm(threshold) сохраняет в alerts строки alarm: 31 > 30 для температур выше порогового значения, а Stats возвращает из summary() значение min 18, max 31, updates 4 (или no data).
Предоставленный код считывает пороговое значение сигнала тревоги, а затем температуры или команды detach alarm / attach alarm, присоединяет всех трёх наблюдателей в начале и в конце выводит строки отображения, предупреждения и сводку.
Ваш код должен находиться в файлах WeatherStation.kt, Display.kt, Alarm.kt, Stats.kt и TemperatureObserver.kt. В Main.kt содержится предоставленный код ввода/вывода, и его нельзя редактировать.
Попробуйте сами
fun main() {
// Предоставленный код ввода/вывода: оставьте его как есть
val input = generateSequence(::readLine).toList()
val station = WeatherStation()
val display = Display()
val alarm = Alarm(input[0].toInt())
val stats = Stats()
station.attach(display)
station.attach(alarm)
station.attach(stats)
for (cmd in input.drop(1)) {
when (cmd) {
"detach alarm" -> station.detach(alarm)
"attach alarm" -> station.attach(alarm)
else -> station.record(cmd.toInt())
}
}
for (line in display.lines) println(line)
for (line in alarm.alerts) println(line)
println(stats.summary())
}
В этом уроке есть небольшой тест. Начните урок, чтобы ответить на вопросы и сохранить прогресс.
Все уроки раздела Объектно-ориентированное программирование
1Классы и объекты
Работа с файламиОпределение классовПервичные конструкторыБлоки init и валидацияВторичные конструкторыПовторение — читательский билет4Наследование
Открытые классы и подклассыПереопределение членовsuper и конструкторыАбстрактные классыПовторение — каталог сотрудников7Специальные классы
Классы данныхКлассы перечисленийЗапечатанные иерархииequals, hashCode, toStringПовторение — трекер заказов10Паттерны проектирования. Часть 2
Делегирование с помощью byПаттерн декораторПаттерн строительПаттерн состояние2Свойства и инкапсуляция
Пользовательские аксессорыМодификаторы видимостиПриватные сеттерыlazy и lateinitПовторение — банковский счёт5Интерфейсы
Объявление интерфейсовМетоды по умолчаниюМножественные интерфейсыИнтерфейсы как типыПовторение — способы оплаты8Обобщения
Обобщённые функцииОбобщённые классыОграничения типовВариативность с in и outПовторение — очередь с приоритетом11Проект: Управление библиотекой
Книги и читателиВыдача книг3Объекты и компаньоны
Объявления объектовОбъекты-компаньоныКонстанты с constОбъектные выраженияПовторение — билетная касса6Полиморфизм
Динамическая диспетчеризацияПроверка типов и умные приведения типовwhen с типамиФункции-расширенияПовторение — смотритель зоопарка9Паттерны проектирования. Часть 1
Паттерн «Одиночка»Фабричный паттернПаттерн «Наблюдатель»Паттерн «Стратегия»Потренируйтесь самостоятельно: Песочница Kotlin