Menu
Coddy logo textTech

תבנית 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 מדומה
challenge icon

אתגר

קל

השלימו את היחידון 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}")
}
quiz iconבחנו את עצמכם

השיעור הזה כולל חידון קצר. התחילו את השיעור כדי לענות עליו ולעקוב אחרי ההתקדמות.

כל השיעורים ביחידה תכנות מונחה עצמים

תרגלו בעצמכם: Kotlin אונליין