تعلّم البرمجة بلغة كوتلن (61): تعدد الأشكال Polymorphism

تعلّم البرمجة بلغة كوتلن (61): تعدد الأشكال Polymorphism
أستمع الى المقال

عند استخدام نمط البرمجة الكائنية، يبدو الأمر كما لو كنا نعمل مع كائنات حقيقية من الحياة اليومية، عبر تمثيلها في التعليمات البرمجية الخاصة بنا. في لغة كوتلن، يمكننا إنشاء الأصناف Classes لتمثيل الكائنات في برامجنا، وإنشاء كائنات من هذه الأصناف. بعدها يمكننا إسناد هذه الكائنات إلى متغيرات (أو مراجع References) للتعامل معها لاحقًا في شفرتنا، أو تجميعها في تجميعات Collections.

وهناك مفاهيم ومبادئ تم تطويرها مع الوقت، لنتمكن من استغلال النمط الكائني هذا، الاستغلال الأمثل. من ضمن هذه المفاهيم: التغليف Encapsulation والوراثة Inheritance والتجريد Abstraction كما في الواجهة Interface والصنف المجرّد Abstract Class.

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

ما هو تعدد الأشكال Polymorphism:

الـ Polymorphism هو مصطلح يوناني قديم يعني “العديد من الأشكال”. ولتوضيح هذا عبر مثال من حياتنا الواقعية، نجد أنه يمكن أن يكون للشخص الواحد أشكال مختلفة في الوقت نفسه. مثل: أن يكون الشخص أب/أم، زوج/زوجة، موظف/موظفة …الخ.

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

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

قد تبدو هذه التعريفات غامضة إلى حد ما، ولكن سيزول هذا الغموض، عندما نضرب أمثلة عملية في الفقرات أدناه.

أنواع تعدد الأشكال:

اعتمادًا على كيفية استدعاء الدالة، يمكن تقسيم تعدد الأشكال إلى قسمين:

  • تعدد الأشكال في وقت الترجمة Compile-Time Polymorphism.
  • تعدد الأشكال أثناء عمل البرنامج Runtime Polymorphism.

تعدد الأشكال في وقت الترجمة Compile-Time:

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

ونظرًا لأن المترجم سيعرف أي دالة يجب أن يستدعي في وقت الترجمة، يُطلق عليه تعدد الأشكال في وقت التجميع أو الترجمة. ويُعرف أيضًا باسم تعدد الأشكال الثابت Static Polymorphism أو الربط المبكر Early Binding (سيأتي شرحه في الفقرة التالية) أو التحميل الزائد Overloading (تم شرحه في درس منفصل).

تعدد الأشكال أثناء تشغيل البرنامج Runtime:

عندما نُنشئ دالة ومن ثم نستدعيها، تتم عملية الربط binding بين الاستدعاء وجسم الدالة، بشكل ثابت static، أي أثناء عملية الترجمة. وهذا الأمر، سيحدث أيضًا في تعدد الأشكال الثابت المذكور في الفقرة السابقة. لأنه في نهاية المطاف، سيعرف المترجم ما هي الدالة التي يجب أن يستدعيها وينفذها قبل اكتمال عملية ترجمة وتجميع شفرة البرنامج. وهذا ما يعرف بـ الربط المبكر Early Binding.

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

فإذا كان الصنف المُشتق يطبِّق override دالة من الصنف الأعلى الذي يرث منه، ثم نستدعي هذه الدالة عبر استخدام متغير نوعه هو الصنف الأعلى، يتم استدعاء الدالة المناسبة، بعد معرفة نوع الكائن الذي يستدعيها أثناء عمل البرنامج Runtime. وهذا يحدث، لأن كلًا من الصنف الأعلى (الأب) والصنف المُشتق (الابن)، لديهما نفس الدالة. ويعرف هذا بـ الربط المتأخر Late Binding.

مثال عملي:

كمثال، إذا كان لدينا الشفرة التالية:

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

class Dog : Pet() {
    override fun speak() = "Bark!"
}

class Cat : Pet() {
    override fun speak() = "Meow"
}

fun talk(pet: Pet) = pet.speak()

لدينا صنف أعلى Pet مفتوح open للوراثة، وصنفان يرثان منه: Dog و Cat. وبما أن الصنف الأعلى به دالة مفتوحة open اسمها ()speak، استطعنا تطبيقها في الأصناف الفرعية المُشتقة منه. 

كل دالة ()speak في الأصناف الثلاث، تعيد قيمة نصية تناسب الصنف الذي تتواجد به. وفي آخر سطر في الشفرة، لدينا دالة أخرى اسمها ()talk وبها معامل واحد من النوع Pet. مهمة هذه الدالة، هي أن تعيد دالة الـ ()speak المناسبة، للكائن الذي يأتي عبر معاملها عند استدعائها:

fun main() {
    println(
        talk(Dog())
    )

    println(
        talk(Cat())
    )
}

عند استدعاء دالة ()talk وإرسال كائنات من الأنواع المُشتقة من النوع الأعلى إلى معاملها pet والذي نوعه هو الصنف الأعلى Pet، يتم تحويل هذه الكائنات المُشتقة إلى الصنف الأعلى Pet. وفي حالتنا هذه، سيتم تحويل الكائن ()Dog والكائن ()Cat، إلى النوع Pet، ثم لاحقًا يتم استدعاء دالة ()speak المناسبة لكل كائن.

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

Bark!
Meow

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

الربط الديناميكي (الحركي) المتأخر:

كما ذكرنا في فقرة سابقة، عملية الربط binding بين استدعاء وجسم الدالة، يحدث بشكل ثابت static. ولكن في حالة دالة ()talk، لا يعرف المترجم ما هو كائن الـ Pet الذي سيتم إرساله كقيمة لمعامل الدالة. لأنه قد يكون أي صنف من الأصناف التي تشتق من الصنف Pet.

بالتالي، لا يعرف المترجم كيفية ربط جسم الدالة:

pet.speak()

بـ استدعائها:

talk(Dog())
talk(Cat())

لذلك، ستجري عملية الربط binding، بطريقة ديناميكية أثناء عمل البرنامج Runtime. وهذا يعرف بـ الربط الديناميكي dynamic binding أو الربط المتأخر late binding.

الفروقات بين overloading و overriding:

يحقق مفهوم التحميل الزائد للدوال overloading ومفهوم التجاوز أو تطبيق الدالة في صنف مُشتق overriding، مبدأ تعدد الأشكال. ولكن هناك فروقات أساسية بين كلا المفهومين، كما في الجدول التالي:

مفهوم الـ overloading:

وهو المفهوم الذي يمكننا من إنشاء دوال بنفس الاسم بشرط اختلاف تواقيع معاملاتها:

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

مفهوم الـ overriding:

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

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

الخلاصة:

يحقق كلًا من مفهومي: الـ overloading و الـ overriding، نوع مختلف من تعدد الأشكال. وكما هو الحال في هذين المفهومين، هناك فروقات أساسية بين نوعي تعدد الأشكال، والتي يمكن إيجازها في النقاط التالية:

تعدد الأشكال في وقت الترجمة Compile-Time:

  • يتم اختيار الدالة التي سيتم تنفيذها أثناء وقت الترجمة.
  • لا يحتاج للوراثة.
  • يكون تنفيذه أسرع نتيجة لمعرفة الدالة التي يجب استدعاؤها أثناء الترجمة.
  • يمكن تحقيق هذا النوع من تعدد الأشكال، عبر التحميل الزائد للدوال.
  • يُعرف أيضًا بـ تعدد الأشكال الثابت Static Polymorphism أو الربط المبكر Early Binding.

تعدد الأشكال أثناء تشغيل البرنامج Runtime:

  • يتم اختيار الدالة أثناء عمل البرنامج Runtime.
  • لا بد من وجود وراثة.
  • يكون تنفيذه أبطأ من النوع السابق، نتيجة لمعرفة الدالة التي يجب استدعاؤها أثناء عمل البرنامج.
  • يمكن تحقيق هذا النوع من تعدد الأشكال، عبر تطبيق الدوال Overriding.
  • يُعرف أيضًا بـ تعدد الأشكال الديناميكي Dynamic Polymorphism أو الربط المتأخر Late Binding.

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

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