Menu
Coddy logo textTech
flag Ar iconالعربيةdown icon

نمط Singleton

جزء من قسم البرمجة كائنية التوجه في رحلة Kotlin على Coddy. الدرس 42 من 57.

إن نمط التصميم هو حل مُثبت لمشكلة تصميم تظهر مرارًا وتكرارًا. يضمن نمط singleton أن يكون للصنف نسخة واحدة بالضبط يمكن للبرنامج بأكمله الوصول إليها. في 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

يحتفظ النمط Singleton بالحالة التي تشترك فيها أجزاء مختلفة من البرنامج. أبقِ هذه الحالة خاصة وغيّرها فقط من خلال الأساليب، بحيث يمر كل تغيير عبر القواعد نفسها:

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اختبر نفسك

يتضمن هذا الدرس اختبارًا قصيرًا. ابدأ الدرس للإجابة عليه وتتبّع تقدمك.

جميع دروس البرمجة كائنية التوجه

تدرّب بنفسك: Playground لـ Kotlin