Menu
Coddy logo textTech

Patrón Observer

Parte de la sección Programación orientada a objetos del Journey de Kotlin de Coddy. Lección 44 de 57.

El patrón observer permite que los objetos se suscriban a los eventos de otro objeto, el subject. El subject mantiene una lista de observadores y notifica a cada uno cuando ocurre algo, sin saber qué hacen con ello:

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

Dentro de main:

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

Salida:

ACME is now 120
ACME is now 95

En Kotlin, un observador suele ser una función simple. El sujeto almacena lambdas y llama a cada una, por lo que un observador nuevo no necesita una clase:

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

Dentro de main:

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

Salida:

Save clicked
Save clicked
clicks: 2

Un observador puede querer dejar de escuchar. Si subscribe devuelve una función que elimina de nuevo al observador, quien llama puede cancelar sin tener acceso a la lista del sujeto:

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

Dentro de 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")

Salida:

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

La biblioteca estándar tiene una propiedad observable. Delegates.observable(initial) { property, old, new -> ... } ejecuta la lambda después de cada asignación; el capítulo 10 explica la palabra clave by que utiliza:

import kotlin.properties.Delegates

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

Dentro de main:

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

Salida:

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

Desafío

Fácil

WeatherStation notifica a los observadores que implementan el TemperatureObserver proporcionado. Escribe attach(observer), detach(observer) y record(temp), que notifiquen a cada observador asociado, pero solo cuando la temperatura difiera de la lectura anterior (la primera lectura siempre notifica). Escribe tres observadores: Display conserva líneas display: 21 en lines, Alarm(threshold) conserva líneas alarm: 31 > 30 en alerts para temperaturas superiores al umbral, y Stats devuelve min 18, max 31, updates 4 desde summary() (o no data).

El código proporcionado lee el umbral de la alarma y después las temperaturas o detach alarm / attach alarm, asocia los tres observadores al inicio y finalmente imprime las líneas de la pantalla, las alertas y el resumen.

Tu código va en WeatherStation.kt, Display.kt, Alarm.kt, Stats.kt y TemperatureObserver.kt. Main.kt contiene el código proporcionado de entrada/salida y no se puede editar.

Pruébalo tú mismo

fun main() {
    // Código de entrada/salida suministrado: déjalo tal como está
    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 iconPonte a prueba

Esta lección incluye un breve cuestionario. Empieza la lección para responderlo y registrar tu progreso.

Todas las lecciones de Programación orientada a objetos

Practica por tu cuenta: Playground de Kotlin