Menu
Coddy logo textTech

옵서버 패턴

Coddy Kotlin 여정의 객체 지향 프로그래밍 섹션에 포함된 레슨. 57개 중 44번째.

observer 패턴을 사용하면 객체가 다른 객체인 subject의 이벤트를 구독할 수 있습니다. subject는 observer 목록을 유지하고, 각 observer가 이벤트를 어떻게 처리하는지 알지 못한 채 무언가 발생하면 각각에 알립니다:

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

챌린지

쉬움

WeatherStation은 제공된 TemperatureObserver를 구현하는 옵저버에게 알림을 보냅니다. 연결된 모든 옵저버에게 알림을 보내되 온도가 이전 측정값과 다를 때만 알림을 보내는 attach(observer), detach(observer) 및 record(temp)를 작성하세요(첫 번째 측정값은 항상 알림을 보냅니다). 세 개의 옵저버를 작성하세요. Display는 display: 21 행을 lines에 저장하고, Alarm(threshold)은 임계값보다 높은 온도에 대해 alarm: 31 > 30 행을 alerts에 저장하며, Stats는 summary()에서 min 18, max 31, updates 4를 반환합니다(또는 no data를 반환합니다).

제공된 코드는 알람 임계값을 읽은 다음 온도 또는 detach alarm / attach alarm을 읽고, 시작할 때 세 옵저버를 모두 연결한 후 마지막에 디스플레이 행, 알림 및 요약을 출력합니다.

코드는 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 플레이그라운드