تعلّم البرمجة بلغة كوتلن (71): الكائنات المرافقة Companion Objects

تعلّم البرمجة بلغة كوتلن (71): الكائنات المرافقة Companion Objects
أستمع الى المقال

شرحنا في درس الكائن Object، كيف أنه تم تطبيق نمط التصميم المُفرد Singleton في لغة كوتلن، عبر استخدام الكلمة المفتاحية object. في هذا الدرس، سنتعلم كيف يمكننا أن نعلن عن هذا الكائن، داخل كائنات أو أصناف أخرى، أي ككائنات مُتداخلة. وكيف يمكن ربط الكائن بالصنف ليصبح جزءًا منه، عبر إضافة الكلمة companion بطريقة قريبة لما فعلناه مع الأصناف المُتداخلة Nested Classes والأصناف الداخلية Inner Classes. ولكن، قبل ذلك، وكتمهيد لهذا الدرس، دعونا نتعرف على بعض المفاهيم المهمة، والتي ستساعدنا في فهمه.

أحيانًا نحتاج إلى إجراء تعديلات طفيفة على صنفٍ ما، دون الاضطرار للوراثة منه، وإنشاء صنف مُشتق جديد. في كوتلن يمكن القيام بهذا الأمر، عبر استخدام مفهومي: تعبير الكائن Object Expression والتصريح أو الإعلان عن الكائن Object Declaration.

تعبير الكائن Object Expression:

باستخدام تعبير الكائن، يمكن إنشاء كائنات من أصناف مجهولة، أي الأصناف التي لم يتم الإعلان عنها صراحةً باستخدام الكلمة المفتاحية class. تكمن الفائدة من هذه الأصناف، في أنها تستخدم مرة واحدة فقط. يمكن الإعلان عن كائنات مجهولة قائمة بذاتها، أو عبر الوراثة من صنف عادي موجود مسبقًا، أو عبر تطبيق واجهة Interface.

تعبير كائن مجهول قائم بذاته:

يكون الإعلان عنه في أبسط صوره، كالتالي:

fun main() {

    val helloWorld = object {
        
    }

}

داخل الدالة ()main أعلاه لدينا متغير helloWorld أسندنا إليه تعبير كائن مجهول. يمكننا استخدام المتغير، في الإشارة إلى هذا الكائن المجهول، أو طباعة القيمة العائدة إذا وجدت. وبما أنه لا توجد شفرة بين القوسين المعقوفين لجسم الكائن المجهول، يمكننا أن نضع داخله خاصيّات. كمثال:

fun main() {

    val helloWorld = object {
        val hello = "Hello"
        val world = "World"

    }

}

ولكن حتى مع وجود الخاصيّات سينتج السطر التالي عند وضع المتغير في دالة الطباعة:

oop.companionObject.MainKt$main$helloWorld$1@30f39991

 هذا الجزء:

oop.companionObject.MainKt

هو رابط الحزمتين والملف الذي يتواجد به الكائن في جهاز الحاسب الخاص بي. أمّا هذا الجزء:

$main$helloWorld$1@30f39991

فهو اسم الدالة التي يتواجد بها واسم الكائن وأخيرًا عنوانه في الجزء المُخصص لهذا الكائن في ذاكرة حاسوبي. بالطبع ليست هذه هي القيمة المُرادة عندما أنشأنا الكائن المجهول. لذا، يمكن أن نطبّق أو نعيد تعريف دالة ()toString المتواجدة في الصنف الأب لكل الأصناف في كوتلن، الصنف Any. وهو أمر شرحناه تفصيليًا في درس أصناف البيانات Data Classes:

fun main() {
    val helloWorld = object {
        val hello = "Hello"
        val world = "World"
	// Any لأن تعبير الكائن المجهول يرث من الصنف override نستخدم الكلمة
        override fun toString() = "$hello $world"
    }

    println(helloWorld)
}

عند وضع المتغير helloWorld والذي يُمثل الكائن المجهول في دالة الطباعة، ستكون نتيجة تنفيذ الشفرة هي:

Hello World

وهي القيمة التي تعيدها دالة ()toString.

تعبير كائن مجهول عبر الوراثة من صنف مفتوح:

في بعض الأحيان، قد نحتاج إلى تعديل صنف متواجد مسبقًا لغرضٍ ما. في هذه الحالة يمكن أن نُنشئ صنف جديد ومن ثم الوراثة من الصنف القديم وإجراء التعديلات اللازمة. أو إذا كنا نحتاج التعديلات في مكان واحد فقط، ولا نحتاج إلى إنشاء كائنات عديدة من هذا الصنف الجديد، حينها يمكن أن نستخدم تعبير الكائن المجهول، ونجعله يرث من الصنف القديم.

وهو أمر قمنا به فعليًا في الدرس السابق. لذا نستعير منه الشفرة مع بعض التعديلات التي تناسب درسنا هذا:

open class Pet {
    open fun speak() = Unit
}

fun parrot(): Pet {
    return object : Pet() {
        override fun speak() = println("talk")
    }
}

الصنف Pet مفتوح open للوراثة منه. ولدينا دالة ()parrot والتي تعيد قيمة من النوع Pet. لذلك، يمكننا أن نجعل الدالة تعيد كائن مُشتق من النوع Pet. سواء كان هذا الكائن صنف تم الإعلان عنه عبر الكلمة class، أو كائن مجهول كما في شفرتنا أعلاه. المهم هنا، هو أن يكون الصنف أو الكائن، يرث من الصنف Pet عبر استخدام النقطتين المتعامدتين (:) ثم اسم الصنف الأب Pet.

نحن هنا اخترنا استخدام الكائن المجهول، لأنه لن نحتاج لاسم الصنف المُشتق لإنشاء كائنات منه. كل ما نحتاجه، هو أن يتم تطبيق الدالة ()speak، ثم إعادة الكائن، كقيمة للدالة ()parrot.

وهذا ما سيحدث عندما نستدعي الدالة ()parrot، فستكون القيمة العائدة منها هي كائن من النوع Pet. (ستجري عملية تحويل نوع لأعلى upcasting تلقائيًا). ثم عبر القيمة العائدة، يمكننا استدعاء دالة ()speak:

fun main() {
    val petObj = parrot()
    petObj.speak()
}

المتغير petObj سيكون هو المرجع reference الذي يُمثل الكائن من النوع Pet العائد من الدالة ()parrot. لذا يمكننا استخدامه لاستدعاء أعضاء الصنف المتواجدين في الصنف Pet، وهو هنا عضو واحد فقط، الدالة ()speak. وستكون نتيجة استدعاء الدالة هي:

talk

وهي القيمة التي سيتم طباعتها عبر دالة الطباعة المتواجدة في الكائن المجهول المتواجد داخل ()parrot. وبالطبع، لأنه تجري عملية تحويل النوع المُشتق (الكائن المجهول) لأعلى (الصنف الأب Pet)، يمكن عبر الكائن العائد من الدالة ()parrot، استدعاء أي أعضاء آخرين يتواجدون في الصنف الأب Pet.

فمثلًا، إذا عدّلنا على الشفرة السابقة وأضفنا دالة جديدة للصنف الأب:

open class Pet {
    open fun speak() = Unit
    fun eat() = println("Pet is eating!")
}

أضفنا دالة ()eat للصنف الأب، وهي غير قابلة لإعادة التعريف في الصنف المُشتق، لعدم احتوائها على الكلمة المفتاحية open، كما في دالة ()speak. ولكن رغم ذلك، يمكن استدعاء هذه الدالة الجديدة في دالة ()main:

fun main() {
    val petObj = parrot()

    petObj.eat()
}

وستكون نتيجة التنفيذ هي:

Pet is eating!

هذا حدث، لأنه بكل بساطة تم تحويل نوع الكائن المجهول لنوعه الأعلى Pet، قبل إعادته من قبل دالة ()parrot.

تعبير كائن مجهول عبر تطبيق واجهة interface:

يمكن الإعلان عن تعبير كائن مجهول، والذي بدوره يطبّق واجهة Interface. فمثلًا، إذا كان لدينا الواجهتين التاليتين:

interface A {
    fun funFromA() {}
}

interface B

يمكن تطبيق الواجهتين عبر تعبير كائن مجهول كالتالي:

class C {

    val getObject = object: A {
        val x: String = "x"
    }


    val getObjectA = object: A {
        override fun funFromA() = Unit
    }


    fun getObjectB(): B = object: A, B {

        override fun funFromA() {}
        val x: String = "x"
    }
}

داخل الصنف C، لدينا الخاصيّة getObject والتي نسند إليها تعبير كائن مجهول والذي بدوره يطبّق الواجهة A. ولأن الدالة المتواجدة داخل الواجهة A لديها تطبيق افتراضي (الأقواس المعقوفة الفارغة تعتبر تطبيق أيضًا)، يصبح تطبيق الدالة ()funFromA اختياريًا بين أقواس تعبير الكائن المجهول. ويمكننا أيضًا أن نعيد تعريف الدالة كما فعلنا في تعبير الكائن المجهول الذي نسنده للخاصيّة getObjectA.

أمّا عن تعبير الكائن المجهول الذي ستعيده الدالة ()getObjectB، فهو يطبّق واجهتين وليس واحدة فقط. وفي هذه الحالة، يصبح إلزاميًا، أن نكتب إحدى الواجهتين صراحًة لتكون نوع بيانات القيمة التي تعيدها الدالة، لذا اخترنا أن يكون نوع الإرجاع في الدالة، الصنف B، وكان يمكن أن نختار A لافرق.

الإعلان عن الكائن Object Declaration:

كما شرحنا في درس الكائن object، يتم تطبيق نمط تصميم البرمجيات النمط المُفرد Singleton على الأصناف، عبر استخدام الكلمة المفتاحية object ثم اسم الصنف المُراد. ثم يمكننا أن نضع داخل هذا الكائن، دوال وخاصيّات. هذا الفعل، هو ما يعرف بـ الإعلان عن الكائن Object Declaration. ولابد أن يكون هناك اسم لهذا الكائن نكتبه بعد الكلمة المفتاحية object. ولا يعتبر الإعلان عن الكائن، تعبير Expression يمكن وضعه يمين علامة الإسناد، وإسناده لمتغير مثلًا. 

فإذا افترضنا أنه احتجنا لصنف اسمه مصنع Factory مهمته هي أن يقوم بإنشاء كائن من صنف ما موجود مسبقًا، وليكن اسمه MyClass، كالتالي:

class MyClass {
    fun myFun(): String = "From MyClass member function: myFun()"
}


object Factory {
    
    fun create(): MyClass = MyClass()
}

لدى الصنف MyClass دالة واحدة وهي ()myFun. الكائن Factory به أيضًا دالة واحدة وهي()create. مهمة الدالة ()create هي إنشاء كائن من الصنف MyClass وإعادته عند استدعائها. ولاستدعاء الدالة نستخدم نفس الطريقة التي شرحناها في درس الكائن، وهي استخدام اسم الكائن ثم نقطة ثم اسم الدالة:

Factory.create()

وبما أن الدالة تعيد كائن من النوع MyClass، يمكن أن نسندها لمتغير ثم استخدام المتغير في استدعاء الدالة ()myFun الخاصة بالصنف MyClass:

fun main() {

    val myClassObj = Factory.create()

    println(myClassObj.myFun())
}

عند تنفيذ الشفرة، ستكون نتيجة الطباعة هي:

From MyClass member function: myFun()

وهو النص الذي تعيده دالة ()myFun.

كائن مُتداخل Nested Object:

نلاحظ أن كل مهمة الكائن هي إعادة كائن من الصنف MyClass. لذا لجعل الشفرة أكثر تنظيمًا، يمكن أن نضعه داخل الصنف MyClass، ككائن مُتداخل:

class MyClass {
    fun myFun(): String = "From MyClass member function: myFun()"

    object Factory {

        fun create(): MyClass = MyClass()
    }
}

وبما أن الكائن Factory أصبح داخل المجال الخاص بالصنف MyClass، يجب علينا استخدام اسم الصنف MyClass، للوصول إليه:

fun main() {

    val myClassObj = MyClass.Factory.create()

    println(myClassObj.myFun())
}

الوصول لأعضاء الكائن Factory:

وعلى الرغم من وجود الكائن Factory في مجال الصنف MyClass، ولكن لن يستطيع الصنف الوصول إلى أعضاء الكائن مباشرةً، ويجب أن يتم استخدام اسم الكائن:

class MyClass {
    fun myFun(): String = "From MyClass member function: myFun()"

    
    val classProperty = Factory.objProperty

    val funFromObj = Factory.create()

    
    object Factory {
        val objProperty = "This is Factory property"
        fun create(): MyClass = MyClass()
    }
}

أضفنا للكائن خاصيّة objProperty. واستخدمنا التعابير التالية:

Factory.objProperty
Factory.create()

للوصول للخاصيّة والدالة الخاصتين بالكائن، ومن ثم إسنادهما إلى الخاصيّات classProperty و funFromObj الخاصة بالصنف MyClass. وبالطبع لن يتم الوصول إليهما، إذا كان يسبق اسميهما محدد الوصول private. هذا يظهر أنه ليس هناك رابط بين الكائن والصنف الذي يحتويه. إذًا، ما الذي يمكننا فعله لجعل الكائن مرتبطًا بالصنف ويتبع له؟ نستخدم الكلمة المفتاحية companion.

الكلمة companion:

حينما نعلن عن كائنٍ ما بداخل صنف ونسبقه بالكلمة companion، يصبح الكائن مرتبطًا بالصنف الذي يتواجد به:

class MyClass {
    fun myFun(): String = "From MyClass member function: myFun()"


    val classProperty = objProperty

    val funFromObj = create()
    
    companion object Factory {
        private val objProperty = "This is Factory property"
        fun create(): MyClass = MyClass()
    }
}

نلاحظ أنه بعد إضافة الكلمة companion، يمكن الآن لأعضاء الصنف MyClass، الوصول إلى خاصيّات ودوال الكائن دون استخدام اسمه صراحةً، حتى مع جعل الوصول إليهما خاص عبر private.

وأيضًا يمكن إسقاط اسم الكائن عند استدعاء دالة ()create الخاصة به، في دالة ()main:

fun main() {

    val myClassObj = MyClass.create()

    println(myClassObj.myFun())
}

وستكون نتيجة تنفيذ الشفرة، هي نفسها كالسابق.

تسمية الكائن المُرافق Companion Object:

خلافًا للكائن المُتداخل الذي لا تسبقه الكلمة companion، يمكننا عند استخدام هذه الكلمة، أن نختار ألا نضع له اسم:

class MyClass {
    fun myFun(): String = "From MyClass member function: myFun()"

    companion object {
        fun create(): MyClass = MyClass()
    }
}

وفي هذه الحالة، ستضع له كوتلن اسم افتراضي Companion (الحرف C كبير). يمكننا حينها أن نصل إلى أعضائه عبر اسم الصنف MyClass الذي يحتويه كالسابق، أو استخدام الاسم الافتراضي:

fun main() {

    val myClassObj = MyClass.Companion.create()

    println(myClassObj.myFun())
}

تقييدات استخدام الكائنات المُرافقة:

تسمح لغة كوتلن بكتابة كائن مُرافق واحد فقط لكل صنف. هذا يعني أنه لا يمكن إنشاء كائنات مُرافقة متعددة للصنف، حتى لو كانت تحمل أسماء مختلفة. فإذا حاولنا القيام بذلك:

class MyClass {

    companion object Factory {
        fun function(): MyClass = MyClass()
    }
    companion object Properties {
        fun function() = "Some value"
    }
}

سينتج الخطأ:

Only one companion object is allowed per class

الرسالة تقول، أنه يسمح بإعلان كائن مُرافق واحد فقط داخل الصنف. لحل المشكلة، يمكننا أن نضع كائن مرافق واحد، ونعلن عن باقي الكائنات عبر الكلمة object فقط:

class MyClass {

    companion object Factory {
        fun function(): MyClass = MyClass()
    }
    object Properties {
        fun function() = "Some value"
    }
}

تقييد آخر وهو لا يسمح باستخدام كائن مُرافق داخل كائن. الشفرة التالية:

object Singleton {
    companion object {

    }
}

ستنتج الخطأ:

Modifier 'companion' is not applicable inside 'object'

أي أن المحدد companion غير قابل للتطبيق داخل الكائن المُفرد المعلن عنه بالكلمة object. ولكن بالطبع، يمكننا الإعلان عن كائنات مُتداخلة عديدة، داخل كائن:

object Singleton {
    object InnerSingleton1 {

    }
    object InnerSingleton2 {

    }
}

الخلاصة:

الإعلان عن كائنات داخل الأصناف، هي طريقة جيدة لتنظيم الشفرة وجعلها أكثر قابلية للقراءة. ولجعل كائن ما مرتبطًا بالصنف الذي يتواجد به، نستخدم معه الكلمة companion. وكقاعدة عامة، يُفضل وضع كائنات مُرافقة داخل الصنف، كبديل للأصناف المُتداخلة.

هذا الدرس هو جزء من سلسلة تعليم مبادئ البرمجة بلغة كوتلن. لمُتابعة الدروس منذ البداية ومُشاهدة فهرس المحتويات يمكنك الانتقال إلى الدرس الأول من هنا

هل أعجبك المحتوى وتريد المزيد منه يصل إلى صندوق بريدك الإلكتروني بشكلٍ دوري؟
انضم إلى قائمة من يقدّرون محتوى إكسڤار واشترك بنشرتنا البريدية.