Menu
CoddyTech

Mẫu Observer

Thuộc phần Lập trình hướng đối tượng trong hành trình Kotlin của Coddy. Bài học 44 trên 57.

Mẫu observer cho phép các đối tượng đăng ký nhận sự kiện từ một đối tượng khác, gọi là subject. Subject lưu danh sách observer và thông báo cho từng đối tượng khi có sự việc xảy ra, mà không cần biết chúng sẽ làm gì với thông báo đó:

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")
}

Bên trong main:

val acme = Stock("ACME")
acme.subscribe(Ticker())
acme.price = 120
acme.price = 95

Đầu ra:

ACME is now 120
ACME is now 95

Trong Kotlin, observer thường là một hàm đơn giản. Đối tượng chủ thể lưu các lambda và gọi từng lambda, nên observer mới không cần lớp nào:

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) }
}

Bên trong main:

var clicks = 0
val save = Button("Save")
save.onClick { println("$it clicked") }
save.onClick { clicks++ }
save.click()
save.click()
println("clicks: $clicks")

Đầu ra:

Save clicked
Save clicked
clicks: 2

Một observer có thể muốn ngừng lắng nghe. Nếu subscribe trả về một hàm xóa observer đó, bên gọi có thể hủy đăng ký mà không cần truy cập vào danh sách của subject:

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) }
}

Bên trong 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")

Đầu ra:

ada reads: hello
bo reads: hello
bo reads: again

Thư viện chuẩn có thuộc tính có thể quan sát. Delegates.observable(initial) { property, old, new -> ... } chạy lambda sau mỗi lần gán; chương 10 giải thích từ khóa by mà nó sử dụng:

import kotlin.properties.Delegates

class Thermostat {
    var target: Int by Delegates.observable(20) { _, old, new ->
        println("target changed from $old to $new")
    }
}

Bên trong main:

val t = Thermostat()
t.target = 22
t.target = 18

Đầu ra:

target changed from 20 to 22
target changed from 22 to 18
challenge icon

Thử thách

Dễ

WeatherStation thông báo cho các observer triển khai TemperatureObserver đã được cung cấp. Viết các hàm attach(observer), detach(observer) và record(temp). Hàm record(temp) thông báo cho mọi observer đã được gắn, nhưng chỉ khi nhiệt độ khác với lần đo trước đó (lần đo đầu tiên luôn gửi thông báo). Viết ba observer: Display lưu các dòng display: 21 trong lines, Alarm(threshold) lưu các dòng alarm: 31 > 30 trong alerts khi nhiệt độ cao hơn ngưỡng, và Stats trả về min 18, max 31, updates 4 từ summary() (hoặc no data).

Code đã cung cấp đọc ngưỡng báo động, sau đó đọc nhiệt độ hoặc detach alarm / attach alarm, gắn cả ba observer ngay từ đầu, rồi cuối cùng in các dòng hiển thị, cảnh báo và bản tóm tắt.

Đặt code của bạn trong WeatherStation.kt, Display.kt, Alarm.kt, Stats.kt và TemperatureObserver.kt. Main.kt chứa code đầu vào/đầu ra đã cung cấp và bạn không thể chỉnh sửa tệp này.

Tự làm thử

fun main() {
    // Đoạn code đầu vào/đầu ra đã được cung cấp: giữ nguyên
    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 iconTự kiểm tra

Bài học này có một bài trắc nghiệm ngắn. Bắt đầu bài học để trả lời và theo dõi tiến độ.

Tất cả bài học trong Lập trình hướng đối tượng

Tự luyện tập: Playground Kotlin