תבנית Singleton
חלק מהיחידה תכנות מונחה עצמים במסלול ה-Kotlin של Coddy. שיעור 42 מתוך 57.
תבנית עיצוב היא פתרון מוכח לבעיית עיצוב שחוזרת שוב ושוב. תבנית הסינגלטון מבטיחה שלמחלקה יש מופע אחד בדיוק, שכל התוכנית יכולה לגשת אליו. ב-Kotlin, הצהרת object היא סינגלטון: היא נוצרת פעם אחת, בשימוש הראשון, ובבטחה גם כשכמה שרשורים מתחילים בו-זמנית:
object Settings {
var theme = "light"
var fontSize = 14
}
fun openEditor() = "editor in ${Settings.theme} theme"בתוך main:
Settings.theme = "dark"
println(openEditor())
println(Settings.fontSize)פלט:
editor in dark theme
14הסינגלטון מחזיק מצב שחלקים שונים בתוכנית חולקים. שמרו על המצב הזה פרטי ושנו אותו רק באמצעות מתודות, כך שכל שינוי יתבצע לפי אותם כללים:
object Metrics {
private val hits = mutableMapOf<String, Int>()
fun hit(page: String) { hits[page] = (hits[page] ?: 0) + 1 }
fun report() = hits.entries.joinToString { "${it.key}=${it.value}" }
}
fun home() = Metrics.hit("home")
fun about() = Metrics.hit("about")בתוך main:
home(); about(); home()
println(Metrics.report())פלט:
home=2, about=1כאשר מופע יחיד זקוק לנתוני אתחול, מחלקה עם private constructor יכולה ליצור אותו באובייקט הנלווה שלה בפנייה הראשונה ולהחזיר את אותו מופע לאחר מכן:
class Database private constructor(val url: String) {
companion object {
private var instance: Database? = null
fun connect(url: String): Database =
instance ?: Database(url).also { instance = it }
}
}
בתוך main:
val a = Database.connect("db://main")
val b = Database.connect("db://other")
println(a === b)
println(b.url)פלט:
true
db://mainהשתמשו בסינגלטונים במשורה. סינגלטון הוא מצב גלובלי: כל קוד יכול לשנות אותו, מה שמסתיר מי תלוי בו, ובדיקות לא יכולות להתחיל מעותק חדש. הם מתאימים לדברים שיש מהם באמת רק אחד לכל תוכנית, כמו תצורה או רשם אירועים. כשחלק אחד בלבד בתוכנית זקוק לאובייקט, העבירו אותו כפרמטר, כשהטיפוס שלו הוא ממשק, כדי שבדיקה תוכל להעביר תחליף מזויף במקום:
interface Clock { fun now(): Long }
object SystemClock : Clock { override fun now() = System.currentTimeMillis() }
class Session(private val clock: Clock) { // התלות גלויה
val started = clock.now()
}
val session = Session(SystemClock) // בדיקה מעבירה Clock מדומהאתגר
קלהשלימו את היחידון Logger. יש לו minLevel שניתן לשנות (debug, info, warn או error, בסדר הזה). log(level, message) שומר את [WARN] disk low כשהרמה שווה ל-minLevel או גבוהה ממנה, ואחרת סופר את ההודעה ב-dropped, שאפשר לקרוא מבחוץ לקוד אך לא לשנות. entries היא רשימה לקריאה בלבד של השורות שנשמרו, ו-count(level) מחזירה כמה מהשורות שנשמרו הן ברמה הזאת.
פונקציות העזר שב-Functions.kt מסופקות: הן ניגשות לאותו Logger מקובץ אחר. הקוד שסופק קורא את הרמה המינימלית ולאחר מכן שורות במבנה level,message, ומדפיס כל ערך ואת errors: 1, dropped: 2.
הקוד שלכם ייכתב ב-Logger.kt וב-Functions.kt. Main.kt מכיל את קוד הקלט/פלט שסופק, ואי אפשר לערוך אותו.
נסו בעצמכם
fun main() {
// קוד הקלט/פלט שסופק: יש להשאיר אותו כפי שהוא
val input = generateSequence(::readLine).toList()
Logger.minLevel = input[0]
for (line in input.drop(1)) {
val (level, message) = line.split(",", limit = 2)
when (level) {
"debug" -> debug(message)
"info" -> info(message)
"warn" -> warn(message)
else -> error(message)
}
}
for (e in Logger.entries) println(e)
println("errors: ${Logger.count("error")}, dropped: ${Logger.dropped}")
}
השיעור הזה כולל חידון קצר. התחילו את השיעור כדי לענות עליו ולעקוב אחרי ההתקדמות.
כל השיעורים ביחידה תכנות מונחה עצמים
1מחלקות ואובייקטים
עבודה עם קבציםהגדרת מחלקותבנאים ראשייםבלוקי init ואימות נתוניםבנאים משנייםסיכום – כרטיס ספרייהתרגלו בעצמכם: Kotlin אונליין