Menu
Coddy logo textTech

Observer Pattern

Part of the Object Oriented Programming section of Coddy's Kotlin journey. Lesson 44 of 57.

The observer pattern lets objects subscribe to the events of another object, the subject. The subject keeps a list of observers and notifies each one when something happens, without knowing what they do with it:

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

Inside main:

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

Output:

ACME is now 120
ACME is now 95

In Kotlin an observer is often a plain function. The subject stores lambdas and calls each one, so a new observer needs no class:

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

Inside main:

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

Output:

Save clicked
Save clicked
clicks: 2

An observer may want to stop listening. If subscribe returns a function that removes the observer again, the caller can cancel without any access to the subject's list:

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

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

Output:

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

The standard library has an observable property. Delegates.observable(initial) { property, old, new -> ... } runs the lambda after every assignment; chapter 10 explains the by keyword it uses:

import kotlin.properties.Delegates

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

Inside main:

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

Output:

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

Challenge

Easy

A WeatherStation notifies observers that implement the supplied TemperatureObserver. Write attach(observer), detach(observer) and record(temp), which notifies every attached observer, but only when the temperature differs from the previous reading (the first reading always notifies). Write three observers: Display keeps display: 21 lines in lines, Alarm(threshold) keeps alarm: 31 > 30 lines in alerts for temperatures above the threshold, and Stats returns min 18, max 31, updates 4 from summary() (or no data).

The supplied code reads the alarm threshold and then temperatures or detach alarm / attach alarm, attaches all three observers at the start and finally prints the display lines, the alerts and the summary.

Your code goes in WeatherStation.kt, Display.kt, Alarm.kt, Stats.kt and TemperatureObserver.kt. Main.kt holds the supplied input/output code and cannot be edited.

Try it yourself

fun main() {
    // Supplied input/output code: keep it as it is
    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 iconTest yourself

This lesson includes a short quiz. Start the lesson to answer it and track your progress.

All lessons in Object Oriented Programming

Practice on your own: Kotlin playground