Menu
Coddy logo textTech

Observerパターン

CoddyのKotlinジャーニー「オブジェクト指向プログラミング」セクションの一部。レッスン 44/57。

observerパターンを使うと、オブジェクトは別のオブジェクトであるsubjectのイベントを購読できます。subjectはオブザーバーの一覧を保持し、何かが起きると、それぞれが何をするかを知らなくても通知します。

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

mainの中で:

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

出力:

ACME is now 120
ACME is now 95

Kotlinでは、オブザーバーは単なる関数であることがよくあります。サブジェクトはラムダを格納し、それぞれを呼び出すため、新しいオブザーバーにクラスは必要ありません。

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

mainの中では次のようにします。

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

出力:

Save clicked
Save clicked
clicks: 2

オブザーバーは、リスニングを停止したい場合があります。subscribeがオブザーバーを再び削除する関数を返す場合、呼び出し側はサブジェクトのリストにアクセスせずにキャンセルできます。

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

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

出力:

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

標準ライブラリには監視可能なプロパティがあります。Delegates.observable(initial) { property, old, new -> ... }は代入のたびにラムダを実行します。第10章では、これが使用するbyキーワードについて説明します。

import kotlin.properties.Delegates

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

mainの中で次のようにします。

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

出力:

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

チャレンジ

簡単

TemperatureObserver を実装する、用意されたオブザーバーに通知する WeatherStation を作成します。attach(observer)、detach(observer)、record(temp) を記述してください。これらは、前回の読み取り値と温度が異なる場合に限り、接続されているすべてのオブザーバーに通知します(最初の読み取りでは常に通知します)。3 つのオブザーバーを作成してください。Display は display: 21 の行を lines に保持し、Alarm(threshold) はしきい値を超える温度について alarm: 31 > 30 の行を alerts に保持し、Stats は summary() から min 18, max 31, updates 4(または no data)を返します。

用意されたコードはアラームのしきい値を読み取り、その後に温度または detach alarm / attach alarm を読み取ります。開始時に 3 つすべてのオブザーバーを接続し、最後に表示行、アラート、サマリーを出力します。

コードは WeatherStation.kt、Display.kt、Alarm.kt、Stats.kt、TemperatureObserver.kt に記述します。Main.kt には用意された入出力コードが含まれており、編集できません。

自分で試してみよう

fun main() {
    // 提供された入出力コード: そのままにしておく
    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 icon腕試し

このレッスンには短いクイズがあります。レッスンを始めて解答し、進捗を記録しましょう。

オブジェクト指向プログラミングのすべてのレッスン

自分で練習してみよう: Kotlinプレイグラウンド