Menu
CoddyTech

观察者模式

本课属于 Coddy Kotlin 学习旅程的“面向对象编程”单元。第 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 中,观察者通常是一个普通函数。主题会存储 lambda 并逐个调用它们,因此新增观察者不需要类:

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 -> ... }会在每次赋值后运行 lambda;第 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
challenge icon

挑战

简单

WeatherStation 会通知实现了所提供的 TemperatureObserver 的观察者。编写 attach(observer)、detach(observer) 和 record(temp),通知每个已附加的观察者,但只有在温度与上一次读数不同时才通知(第一次读数总会触发通知)。编写三个观察者:Display 将 display: 21 行保存在 lines 中;Alarm(threshold) 会将温度高于阈值时的 alarm: 31 > 30 行保存在 alerts 中;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())
}
quiz icon自我测试

这节课包含一个小测验。开始学习即可作答,并记录你的进度。

面向对象编程中的所有课

自己练习: Kotlin 在线编辑器