תבנית Observer
חלק מהיחידה תכנות מונחה עצמים במסלול ה-Kotlin של Coddy. שיעור 44 מתוך 57.
תבנית הצופה מאפשרת לאובייקטים להירשם לאירועים של אובייקט אחר, הנושא. הנושא מחזיק רשימה של צופים ומודיע לכל אחד מהם כשמשהו קורה, בלי לדעת מה הם עושים עם המידע:
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אתגר
קלWeatherStation מודיעה לצופים שמממשים את TemperatureObserver שסופק. כתבו את attach(observer), את detach(observer) ואת record(temp), שמודיעה לכל צופה שמחובר, אך רק כאשר הטמפרטורה שונה מהמדידה הקודמת (המדידה הראשונה תמיד גורמת להודעה). כתבו שלושה צופים: Display שומרת שורות display: 21 ב-lines, Alarm(threshold) שומרת שורות alarm: 31 > 30 ב-alerts עבור טמפרטורות שמעל הסף, ו-Stats מחזירה את min 18, max 31, updates 4 מתוך summary() (או 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())
}
השיעור הזה כולל חידון קצר. התחילו את השיעור כדי לענות עליו ולעקוב אחרי ההתקדמות.
כל השיעורים ביחידה תכנות מונחה עצמים
1מחלקות ואובייקטים
עבודה עם קבציםהגדרת מחלקותבנאים ראשייםבלוקי init ואימות נתוניםבנאים משנייםסיכום – כרטיס ספרייהתרגלו בעצמכם: Kotlin אונליין