Menu
Coddy logo textTech

Beobachtermuster

Teil des Abschnitts Objektorientierte Programmierung der Kotlin-Journey von Coddy. Lektion 44 von 57.

Das Beobachter-Muster ermöglicht es Objekten, die Ereignisse eines anderen Objekts, des Subjekts, zu abonnieren. Das Subjekt führt eine Liste von Beobachtern und benachrichtigt jeden einzelnen, wenn etwas passiert, ohne zu wissen, was sie damit tun:

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

Innerhalb von main:

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

Ausgabe:

ACME is now 120
ACME is now 95

In Kotlin ist ein Beobachter oft eine einfache Funktion. Das Subjekt speichert Lambdas und ruft jedes einzelne auf, daher benötigt ein neuer Beobachter keine Klasse:

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

Innerhalb von main:

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

Ausgabe:

Save clicked
Save clicked
clicks: 2

Ein Beobachter möchte möglicherweise aufhören zuzuhören. Wenn subscribe eine Funktion zurückgibt, die den Beobachter wieder entfernt, kann der Aufrufer abbrechen, ohne Zugriff auf die Liste des Subjekts zu benötigen:

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

Innerhalb von 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")

Ausgabe:

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

Die Standardbibliothek verfügt über eine beobachtbare Eigenschaft. Delegates.observable(initial) { property, old, new -> ... } führt das Lambda nach jeder Zuweisung aus; Kapitel 10 erklärt das dabei verwendete Schlüsselwort by:

import kotlin.properties.Delegates

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

Innerhalb von main:

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

Ausgabe:

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

Aufgabe

Einfach

Ein WeatherStation benachrichtigt Beobachter, die TemperatureObserver implementieren. Schreibe attach(observer), detach(observer) und record(temp), wobei jeder angehängte Beobachter benachrichtigt wird, jedoch nur, wenn sich die Temperatur von der vorherigen Messung unterscheidet (die erste Messung benachrichtigt immer). Schreibe drei Beobachter: Display speichert display: 21-Zeilen in lines, Alarm(threshold) speichert alarm: 31 > 30-Zeilen in alerts für Temperaturen über dem Schwellenwert, und Stats gibt über summary() min 18, max 31, updates 4 zurück (oder no data).

Der bereitgestellte Code liest den Alarmschwellenwert und anschließend Temperaturen oder detach alarm / attach alarm ein, hängt zu Beginn alle drei Beobachter an und gibt schließlich die Display-Zeilen, die Alarme und die Zusammenfassung aus.

Dein Code gehört in WeatherStation.kt, Display.kt, Alarm.kt, Stats.kt und TemperatureObserver.kt. Main.kt enthält den bereitgestellten Eingabe-/Ausgabecode und kann nicht bearbeitet werden.

Probier es selbst

fun main() {
    // Gelieferter Ein-/Ausgabecode: belasse ihn wie er ist
    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 iconTeste dich selbst

Diese Lektion enthält ein kurzes Quiz. Starte die Lektion, um es zu beantworten und deinen Fortschritt zu speichern.

Alle Lektionen in Objektorientierte Programmierung

Übe selbstständig: Kotlin-Playground