Menu
Coddy logo textTech

Patron Observer

Fait partie de la section Programmation orientée objet du Journey Kotlin de Coddy. Leçon 44 sur 57.

Le motif observateur permet aux objets de s’abonner aux événements d’un autre objet, le sujet. Le sujet conserve une liste d’observateurs et informe chacun d’eux lorsqu’un événement se produit, sans savoir ce qu’ils en font :

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

Dans main :

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

Sortie :

ACME is now 120
ACME is now 95

En Kotlin, un observateur est souvent une simple fonction. Le sujet stocke des lambdas et appelle chacune d’elles, donc un nouvel observateur n’a besoin d’aucune classe :

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

Dans main :

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

Sortie :

Save clicked
Save clicked
clicks: 2

Un observateur peut vouloir cesser d’écouter. Si subscribe renvoie une fonction qui supprime à nouveau l’observateur, l’appelant peut annuler sans avoir accès à la liste du sujet :

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

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

Sortie :

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

La bibliothèque standard possède une propriété observable. Delegates.observable(initial) { property, old, new -> ... } exécute le lambda après chaque affectation ; le chapitre 10 explique le mot-clé by qu’il utilise :

import kotlin.properties.Delegates

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

À l’intérieur de main :

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

Sortie :

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

Défi

Facile

Un WeatherStation notifie les observateurs qui implémentent le TemperatureObserver fourni. Écrivez attach(observer), detach(observer) et record(temp), qui notifient chaque observateur attaché, mais uniquement lorsque la température diffère de la lecture précédente (la première lecture notifie toujours). Écrivez trois observateurs : Display conserve les lignes display: 21 dans lines, Alarm(threshold) conserve dans alerts les lignes alarm: 31 > 30 pour les températures supérieures au seuil, et Stats renvoie min 18, max 31, updates 4 depuis summary() (ou no data).

Le code fourni lit le seuil de l’alarme, puis des températures ou detach alarm / attach alarm, attache les trois observateurs au début et affiche finalement les lignes de l’affichage, les alertes et le résumé.

Votre code doit être placé dans WeatherStation.kt, Display.kt, Alarm.kt, Stats.kt et TemperatureObserver.kt. Main.kt contient le code d’entrée/sortie fourni et ne peut pas être modifié.

Essayez vous-même

fun main() {
    // Code d'entrée/sortie fourni : le garder tel quel
    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 iconTestez-vous

Cette leçon comprend un petit quiz. Commencez la leçon pour y répondre et suivre votre progression.

Toutes les leçons de Programmation orientée objet

Entraînez-vous par vous-même : Playground Kotlin