علّم البرمجة بلغة كوتلن (59): الأصناف المجردة Abstract Classes

علّم البرمجة بلغة كوتلن (59): الأصناف المجردة Abstract Classes
أستمع الى المقال

واحد من أهم ركائز البرمجة الكائنية هو مبدأ أو مفهوم التجريد. فبعد أن تعرّفنا على ما هي الواجهة Interface، وكيفية إنشائها واستخدامها، نستعرض في درس اليوم طريقة لجعل الأصناف مجرّدة abstract، والفرق بينها وبين الواجهة.

ما هو الصنف المجرّد Abstract Class:

الصنف المجرّد يشبه الصنف العادي باستثناء أن به دالة أو خاصيّة واحدة أو أكثر غير مكتملة التعريف. ودالة غير مكتملة التعريف يعني، مجرد دالة بها الكلمة fun واسم الدالة وأقواس معاملاتها وليس بها جسم. قد يكون بها معاملات أو لا، وقد نذكر نوع بيانات القيمة التي تعيدها الدالة كنتيجة بعد إنتهاء عملها أو نترك كوتلن تضع لها النوع Unit. والمقصود بخاصيّة غير مكتملة، الإعلان عن خاصية بدون تهيئة أي دون أن نسند لها قيمة من نوع بيانات معين.

الصنف المجرّد يشبه الواجهة Interface كثيرًا مع الفرق أن الواجهة لا يمكنها أن تحتفظ بحالة state، أي لا يمكن أن يتم إسناد قيم للخاصيّات مباشرةً. أمّا عن الفروقات بين الاثنين، هذا ما سنشرحه تفصيليًا في الفقرات أدناه، ولكن دعونا أولًا نتعرف على الصنف المجرّد نفسه وكيفية إنشائه وما هي المميزات التي يقدمها لنا.

إنشاء الصنف المجرّد:

لإنشاء صنف مجرّد، نستخدم الكلمة المفتاحية abstract قبل الكلمة المفتاحية لإنشاء صنف class، ثم اسم الصنف:

abstract class AbstractClassName {

    abstract val abstractPropertyName: Int

    abstract fun abstractFunName(i: Double)

    abstract fun abstractFunName() : Int
    
}

ويجب أيضًا، أن نضع الكلمة abstract قبل الخاصيّات والدوال التي ليست مكتملة التعريف. فمثلًا، الخاصيّة abstractPropertyName أعلنا عنها كخاصيّة من النوع Int، ولكن ليس بها قيمة من هذا النوع. في هذه الحالة، يجب أن نضع لها الكلمة abstract أو نسند لها قيمة من هذا النوع صراحةَ، وإلّا سينتج خطأ.

بالنسبة للدوال في الصنف، عندما يكون الصنف مجرّد، يمكننا أيضًا أن نختار عدم وضع جسم لها. فإذا نظرنا إلى دالة ()abstractFunName، نجد أن بها معامل من النوع Double وليس بها جسم نضع به الشفرة التي ستنفذها. كما نلاحظ، أنه ليس بها نوع القيمة العائدة منها ReturnType، وفي هذه الحالة ستضع لها كوتلن النوع Unit.

أمّا بالنسبة لدالة ()abstractFunName، هي تحمل نفس اسم الدالة التي تسبقها، وتم السماح بوجودهما معًا في نفس الصنف، لاختلاف توقيعاتهما (اختلاف المعاملات أو عدمها في حالتنا هذه). نلاحظ أننا في هذه الدالة، ذكرنا صراحةً نوع بيانات القيمة العائدة منها وهي Int. وأيضًا لوجودها في صنف مجرّد، أمكننا عدم وضع جسم لها.

هذه هي بعض الأوضاع المختلفة التي يمكننا أن نعلن بها الدوال والخاصيّات المجرّدة داخل الصنف المجرّد. ولكن بالطبع، يمكننا أيضًا أن نعلن عن خاصيّات ودوال غير مجرّدة داخله. وما يجب أن نعرفه أيضًا، هو في حالة وجود خاصيّة مجرّدة أو دالة مجرّدة في أي صنف، يصبح حينها إلزاميًا أن نجعل هذا الصنف مجرّد بوضع الكلمة abstract.

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

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

وفي ذات الوقت، يمكنه أن يختار إعادة تعريف overriding الخاصيّات والدوال غير المجرّدة إذا كانت متواجدة في الصنف الأب المجرّد وتحمل الكلمة open، أو استخدام نفس تعريفها في الصنف الأب لأنها ستكون متوفرة للصنف الابن على كل حال.

لفهم أفضل للنقاط الواردة في الفقرات أعلاه، يمكننا أن نرى ما هي التقييدات والمميزات التي يتميز بها الصنف المجرّد.

مميزات الصنف المجرّد:

لدى الأصناف المجرّدة الميزات الخاصة التالية:

  • من المستحيل إنشاء كائن منها.
  • يمكن أن يحتوي الصنف المجرّد، على خاصيّات ودوال مجرّدة والتي يجب وضع قيم لها أو جسم بالنسبة للدوال، في الأصناف الفرعية subclasses غير المجرّدة التي ترث منه.
  • يحتوي على دوال وخاصيّات غير مجرّدة.
  • يمكن لصنف مجرّد أن يرث من صنف آخر (إذا كان الصنف الآخر قابل للوراثة)، بما في ذلك الأصناف المجرّدة الأخرى.
  • يمكن للأصناف المجرّدة، أن تمتلك باني Constructor.

وتشير هذه الميزات، إلى أن الاختلاف بين الأصناف المجرّدة والعادية، يتمثّل في شيئين: لا يمكن إنشاء كائن من الأصناف المجرّدة، والأصناف العادية لا يمكنها أن تحتوي على خاصيّات ودوال مجرّدة.

مثال عملي:

ولفهم الفرق بين الوراثة من الأصناف العادية (التي تحمل الكلمة open) والأصناف المجرّدة، يمكننا تحويل الصنف الأعلى Animal والذي استخدمناه في درس الوراثة، لترث منه كائنات من النوع “حيوان” خاصيًاته ووظائفه، إلى صنف مجرّد:

abstract class Animal {

    val limbs: Int = 0

    abstract val color: String

    abstract fun makeNoise(): String

    fun move(): String {
        return "Running or jumping"
    }
}

الخاصيّة limbs بها قيمة افتراضية وهي 0. والدالة ()move هي دالة مكتملة التعريف وليست مجرّدة. ولكن لدينا خاصيّة مجرّدة وهي color ودالة مجرّدة وهي ()makeNoise. لذا توجب أن نحوّل هذا الصنف إلى صنف مجرّد، بإضافة الكلمة abstract إليه. (كان من الممكن أن نستخدم Interface هنا ولكن عندها لا يمكننا إضافة قيمة للخاصيّة limbs ولن يحتوي الصنف على باني، وسنعرف لماذا لاحقًا في هذا الدرس).

يمكن لأي صنف عادي أو مجرّد، الوراثة من الصنف Animal. ولكن الصنف الابن، سيكون مجبرًا لأن يضع تعريف للخاصيّات والدوال غير مكتملة التعريف في الصنف الأب المجرّد Animal:

class Cat : Animal() {
    override val color = "Blue"
    override fun makeNoise(): String {
        return "Meow, Meow"
    }
}
class Dog : Animal() {
    override val color = "Black"
    override fun makeNoise(): String {
        return " hau, haw"
    }
}

الصنفين Cat و Dog، يرثان من الصنف المجرّد Animal بنفس طريقة الوراثة من الأصناف العادية. الفرق أن الأصناف المجرّدة لا تحتاج ان نضع بها الكلمة open كالأصناف العادية. 

وعند إنشاء كائنات من الصنفين  Cat و Dog، سيكونان مجبرين على إعادة تعريف color و ()makeNoise، وستتوفر لهما كل الدوال والخاصيّات غير المجرّدة الأخرى بتعريفاتها الافتراضية:

fun main() {

    val cat = Cat()
    val dog = Dog()

    println(cat.color)
    println(cat.limbs)
    println(cat.move())

    println(dog.color)
    println(dog.limbs)
    println(dog.move())

}

وستكون نتيجة الطباعة:

Blue
0
Running or jumping
Black
0
Running or jumping

لتغيير القيم الافتراضية لـ limbs و ()move، يجب أن نجرّدهما من تعريفاتهما، أو نجعلهما مفتوحين باستخدام الكلمة open:

abstract class Animal {

    open val limbs: Int = 0

    abstract val color: String

    abstract fun makeNoise(): String

    open fun move(): String {
        return "Running or jumping"
    }
}

الآن يمكننا إعادة تعريفهما في الأصناف المشتقة من الصنف Animal:

class Cat : Animal() {
    override val color = "Blue"
    override val limbs = 4
    override fun makeNoise(): String {
        return "Meow, Meow"
    }

    override fun move(): String {
        return super.move() + ", but sometimes flies from one wall to another"
    }
}
class Dog : Animal() {
    override val limbs = 4
    override val color = "Black"

    override fun makeNoise(): String {
        return " hau, haw"
    }

    override fun move(): String {
        return super.move()
    }
}

لتكون نتيجة تنفيذ الشفرة أعلاه، هي:

Blue
4
Running or jumping, but sometimes flies from one wall to another
Black
4
Running or jumping

لماذا الصنف المجرّد:

إذا كنا لا نريد تعريف الخاصيّة color في الصنف الأعلى ونريد أن يتم تعريفها في كل صنف يرث منه. ولا نريد وضع جسم لدالة ()makeNoise لأننا نريد من كل صنف مشتق من هذا الصنف الأعلى، أن يضع لها تعريف خاص به. ولا نريد أن يتم إنشاء كائن من هذا الصنف الأعلى. في هذه الحالة، سنجعل الصنف مجرّد، والذي سيقبل وجود خاصيّات ودوال مجرّدة به، ولديه باني ولكن لا يمكن إنشاء كائن منه عكس الصنف العادي.

بعض الفروق بين الصنف المجرّد والواجهة:

كلا من الصنف المجرّد abstract class والواجهة interface، هما مفهومان قويان يحققان مبدأ التجريد بسماحهما بوضع خاصيّات ودوال مجرّدة داخلهما. كلاهما لا يمكن إنشاء كائن منهما مباشرةً. وعلى الرغم من أنهما يبدوان متشابهان كثيرًا، ولكن هناك بعض الفروق بينهما.

من ضمن هذه الفروق:

  • يمكن لصنف مجرّد أن يرث من صنف مجرّد أو صنف عادي. بينما الواجهة يمكنها فقط الوراثة من واجهة أخرى.
  • يمكن لصنف مجرّد أن يرث من صنف مجرّد أو صنف عادي واحد فقط. بينما الواجهة يمكنها أن ترث من عدة واجهات.
  • يمكن للأصناف المجرّدة إمتلاك باني Constructor وهو ما لا تملكه الواجهة.
  • في الصنف المجرّد، يجب أن نضع الكلمة abstract قبل الخاصيّات والدوال غير مكتملة التعريف، بينما في الواجهة هي اختيارية.
  • يمكن إسناد قيم لخاصيّات في الصنف المجرّد، بينما في الواجهة لا يمكننا فعل ذلك مباشرةً.

وعمومًا، استخدام كلا المفهومين (واجهات وأصناف مجرّدة) يجعل الشفرة الخاص بنا أكثر مرونة. 

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

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