أستمع الى المقال

شرحنا في قسم مقدمة في الكائنات، ما هي الكائنات Objects وكيفية تمثيلها باستخدام الأصناف Classes التي تملك بدورها خاصيّات Properties ودوال تنتمي إليها Member Functions. أو تجميع هذه الكائنات في تجميعات Collections، أو في مصفوفات Arrays. وكتمهيد لفهم المفاهيم الـ 4 الأساسية للبرمجة الكائنية، درسنا كيفية تغليف Encapsulation الخاصيّات والدوال وغيرها، للتحكم في كيفية الوصول إليها وتغيير قيمها. أمّا المفاهيم الـ 3 الأخرى: التجريد Abstraction و الوراثة Inheritance و تعددية الأشكال Polymorphism، فهي ما سنستعرضها بداية من هذا الدرس.

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

بيل غيتس

عندما ننشئ صنف، يمكننا أن نضع به خاصيّات تصف خواصه ودوال تؤدي وظائف خاصة به. ولكن في بعض الأحيان، يكون لدينا أصناف عديدة ذات خواص ووظائف مشتركة. في هذه الحالة، يمكننا التخلص من الشفرات المتكررة، بوضعها كلها في نوع نموذجي prototype يصف فقط ما هي هذه الخواص والوظائف. ثم بعد ذلك، يتم تطبيقها أو تنفيذها في كل صنف على حدا. هذا المفهوم التجريدي يمكن تطبيقه عبر ما يعرف بـ الواجهة Interface.

ما هي الواجهة Interface:

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

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

واجهة برمجة التطبيقات (Application Programming Interface – API) هي عبارة عن مجموعة من مسارات اتصال محددة بين مكونات البرامج المختلفة. أمّا في البرمجة الكائنية، الواجهة البرمجية API لأي كائن، هي مجموعة من أعضائه العامين الذين يستخدمهم للتفاعل مع الكائنات الأخرى.

ستتضح هذه التعريفات النظرية عندما نستخدم الواجهة عمليًا في الفقرات أدناه.

إنشاء واجهة Interface:

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

interface InterfaceName {
    
    val property: Datatype

    fun function1(): ReturnType

    fun function2()
}

نلاحظ أيضاً، أن الخاصية property ليس بها قيمة معينة، ولكن تم تحديد ما هو نوع بيانات هذه القيمة التي ستسند إليها عند التطبيق لاحقًا. بالنسبة للدوال، هي أيضًا مجرد وصف لما يجب أن يكون عليه النوع الذي يجب أن تعيده ReturnType. فدالة ()function1 ستعيد نفس النوع المحدد أعلاه، ودالة ()function2 لا تعيد نوع معين، وهذا يعني النوع Unit في كوتلن.

تطبيق الواجهة:

لتطبيق هذه الواجهة في أي صنف، نستخدم النقطتين المتعامدتين (:) بعد اسم الصنف ثم نكتب اسم الواجهة المُراد تطبيقها:

class ClassName: InterfaceName {

    override val property: DataType = something

    override fun function1(): ReturnType {
        return something
    }

    override fun function2() {
        doSomething
    }
}

عندما يطبق الصنف ClassName الواجهة InterfaceName، سيكون إجباريًا أن يطبق هذا الصنف الخاصيّات والدوال المعلنة في الواجهة (إلّا في حالات خاصة سنذكرها في الفقرات أدناه). ويتم تطبيق أعضاء الواجهة هؤلاء، باستخدام الكلمة المفتاحية override، وهي تعني إعادة تطبيق أو تعريف لأعضاء الواجهة. لأن أعضاء الواجهة هي أعضاء عامة، لذا عندما نستخدمها في صنفٍ ما، فنحن سنعيد كتابتها overwriting أو نعيد تعريفها overriding بما يناسب هذا الصنف.

نلاحظ أن الشفرات أعلاه للشرح فقط وليست شفرات كوتلن حقيقية. لذا دعونا نقوم بكتابة مثال عملي حقيقي عبر كوتلن لفهم طريقة عمل الواجهة.

مثال عملي:

ولفهم كيفية استخدام الواجهة عمليًا، دعونا نعيد استخدام نفس الشفرة من درس البرمجة المُعَمَمّة Generic Programming:

class Animal<T>(private val obj: T) {
    fun <T> getAnimalInfo(obj: T) = obj
}

data class Cat(val sound: String)
data class Lion(val sound: String)
data class Wolf(val sound: String)

في الشفرة أعلاه، استخدمنا صنف مُعَمَم لتمثيل كل أصناف الحيوانات. ولإستخدام الخاصيّة sound والدالة ()getAnimalInfo، كان علينا إنشاء كائن Animal من النوع الخاص بالصنف الذي نريده:

val cat: Animal<Cat> = Animal(Cat("Meow, Meow"))
cat.getAnimalInfo(cat)

في هذه الحالة، سيكون نوع بيانات الكائن cat هو: <Animal<Cat وليس كائن من النوع Cat. وهذا لا يبدو مناسبًا، فكل كائن cat يفضل أن يكون من النوع Cat. لفعل ذلك، يمكننا جعل Animal يصف خاصيّات ووظائف أي صنف من النوع حيوان فقط، ويترك تطبيقها للصنف الذي يمثل هذا الحيوان ينفذها كما يريد.

الخاصيَات والدوال التي يمكننا وضعها في صنف Animal، هي المشتركة بين كل الحيوانات، مثل: اللون color وإصدار صوت noise والحركة move وغيرها:

interface Animal {

    val color: String

    fun move(): String

    fun makeNoise(): String

}

الآن Animal هو وصف لما يجب أن يكون عليه أي صنف حيوان يطبقه. وعند تطبيقه من أي صنف، يصبح من الإجباري تطبيق كل أعضائه في الصنف:

class Cat : Animal {

    override val color = "Red"

    override fun move()= "Walking..."

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

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

وعند إنشاء كائن من Cat، سيكون نوع بياناته هو Cat. بالتالي، يمكننا استخدامه في الوصول إلى الخاصيّات والدوال الخاصة به مباشرةً عبر الكائن:

fun main() {
    val cat: Cat = Cat()

    println(cat.color)
    println(cat.move())
    println(cat.makeNoise())
}

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

Red
Walking...
Meow, Meow

تطبيق الخاصيّات في باني الصنف:

نلاحظ أننا نضع قيمة color المحددة بـ “Red” في الصنف Cat مباشرةً. قد تصدر القطط نفس الصوت وتتحرك بنفس الطريقة، ولكنه سيكون لديها ألوان مختلفة في كثير من الأحيان. لذلك، يبدو من الأفضل جعل خاصيّة الـ color مرنة باستقبال قيمتها عبر باني الصنف Cat. وبما أن الخاصيّة color هي مجرد تطبيق لخاصية color في الواجهة Animal، إذًا يجب أن نضيف الكلمة override أيضًا:

class Cat(override val color: String) : Animal {

    override fun move() = "Walking..."

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

الآن عند إنشاء كائن من الصنف Cat، يجب أن نرسل قيمة لمعامل الباني (والذي هو خاصيّة في نفس الوقت شرحنا ذلك في درس الباني Constructor):

fun main() {
    val cat1: Cat = Cat("Red")
    val cat2: Cat = Cat("Blue")

    println("The ${cat1.color} cat is making ${cat1.makeNoise()} while ${cat1.move()}")
    println("The ${cat2.color} cat is doing the same as the ${cat1.color} one!!")
}

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

The Red cat is making Meow, Meow while Walking...
The Blue cat is doing the same as the Red one!!

تطبيق الواجهة Animal في أصناف أخرى:

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

class Lion : Animal

class Wolf : Animal

ولكن عند وضعها بهذا الشكل، سنلاحظ أن هناك خط أحمر تحت اسماء الصنف، للدلالة على هناك خطأ والشفرة بكاملها لن يتم تنفيذها إذا لم معالجته. وإذا وضعنا مؤشر الفأرة على هذا الخطأ في برنامج IntelliJ، ستظهر رسالة تخبرنا بالمشكلة وفي الغالب سيوفر البرنامج رابط لحل المشكلة:

يظهر في الرسالة، أن الخطأ ناتج عن الصنف Lion غير المجرّد non-abstract (سنتحدث عن الأصناف المجرّدة في درس لاحق)، لا ينفذ الخاصيّة المجرّدة abstract property المعلن عنها في النوع Interface، المسمى Animal. وهذا يعني، طالما صنف Lion يطبق واجهة Interface والتي بها خاصيّات مجرّدة أي لم يتم تهيئتها أو إضافة قيمة لها، يجب أن يتم تطبيق أو تنفيذ هذه الخاصيّة هنا في صنف Lion.

لحل المشكلة، يمكننا كتابة الخاصيّة مسبوقة بـ override، كما فعلنا في صنف Cat أعلاه، أو يمكننا فعل ذلك بمساعدة البرنامج بالضغط على:

Implement as constructor parameters

بعدها وفي النافذة التي ستظهر، نتأكد من أن الخاصيّة المختارة هي التي نريدها، ثم نضغط OK. سيقوم البرنامج بفعل نفس الأمر كالتالي:

ولكن كما نرى في الصورة المتحركة، الخطأ لم يختفي بعد. وهذا لأن هناك أعضاء آخرين لم يطبقهم الصنف Lion. وهما الدالتين المجرّدتين ()move و ()makeNoise (لا تملكان جسم body به عمليات كالدوال العادية، لذلك هما مجرّدتين abstract). 

يمكننا فعل ذلك بمساعدة بيئة التطوير IntelliJ مرة أخرى بالضغط على Implement Members هذه المرة:

وبعد أن يطبق Intellij الدوال، نحذف السطر (“TODO(“Not yet implemented ونضع الشفرة الخاصة بنا:

class Lion(override val color: String) : Animal {
    override fun move(): String {
        return “Running…”
    }

    override fun makeNoise(): String {
        return “grrr, grrr”
    }
}

يمكننا فعل المثل مع صنف Wolf. بالطبع يمكننا فعل كل ذلك يدويًا كما فعلنا مع الصنف Cat، ولكن استخدام هذه الميزة في برنامج IntelliJ، ستكون مفيدة بشكل خاص عند تطبيق واجهة Interface لا نعرف ما هي اسماء الدوال والخاصيّات المتوفرة بها.

تطبيق قيم افتراضية:

أغلب الحيوانات لديها أطراف Limbs (أرجل)، لذا سنضيفها كخاصيّة للواجهة Animal:

interface Animal {

    val limbs: Int

    val color: String

    fun move(): String

    fun makeNoise(): String
}

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

ولأن الواجهة ليست صنف class عادي، والهدف منها أساسًا هو وصف عام لأصناف متعددة، لا يمكننا أن نضع قيم لخواصها التي من المفترض أن يطبقها كل صنف بما يناسبه:

interface Animal {

    val limbs: Int = 4

    val color: String

    fun move(): String

    fun makeNoise(): String
}

الشفرة أعلاه، ستنتج الخطأ التالي:

Property initializers are not allowed in interfaces

ولكن في كوتلن، يمكننا فعل ذلك باستخدام الـ getters (لا يمكن استخدام الـ setters لأنه لا يمكن إنشاء كائن من الواجهة Interface لتغيير قيمة الخاصيّة عبر دالة set. بالتالي، إعلانها بالكلمة var لن يشكل فرق وقد يسبب مشكلة):

interface Animal {

    val limbs: Int
        get() = 4

    val color: String

    fun move(): String

    fun makeNoise(): String
}

الآن أصبح تطبيق الخاصيّة limbs، اختياريًا. وهكذا نكون قد تخلصنا من الخطأ في كل الأصناف التي تطبق الواجهة Animal.

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

interface Animal {

    val limbs: Int
        get() = 4

    val color: String

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

    fun makeNoise(): String
}

في هذه الحالة، أصبح تطبيق دالة ()move داخل الأصناف التي تطبق الواجهة Animal أيضًا اختياري. وفي حالة لدينا صنف جديد لا تطابق حالته القيم الافتراضية في الواجهة، نحتاج إلى تطبيق أعضاء الواجهة بما يناسب الصنف الجديد:

class Parrot(override val color: String) : Animal {
    
    override val limbs: Int = 2

    override fun move(): String {
        return "Flying..."
    }
    override fun makeNoise(): String {
        return "Speaking..."
    }
}

الصنف ببغاء Parrot، لديه طرفين فقط وليس 4، وهو يميل إلى الطيران أكثر من المشي. لذا طبّقنا الخاصيّة limbs والدالة ()move وعدّلنا عليهما بما يناسب الببغاء.

أمّا الصنف Cat مثلًا، فيمكننا كتابته كالتالي:

class Cat(override val color: String) : Animal {

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

وعلى الرغم من الصنف Cat لا يطبق الخاصيّة limbs ولا الدالة ()move، ولكنهما سيكونان متوفران لاستخدامها مع الصنف، نتيجة لتطبيقه الواجهة Animal التي يتواجدان بها:

fun main() {
    val cat: Cat = Cat("Red")
    val parrot: Parrot = Parrot("Blue")

    println("The ${cat.color} cat is making ${cat.makeNoise()} while ${cat.move()} with it's ${cat.limbs} Limbs")
    println("The ${parrot.color} parrot is ${parrot.makeNoise()} while ${parrot.move()} with it's ${parrot.limbs} Limbs")

}

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

The Red cat is making Meow, Meow while Running or jumping with it's 4 Limbs
The Blue parrot is speaking while flying with it's 2 Limbs

واجهة SAM:

يأتي الاسم واجهات SAM أو  Single Abstract Method (SAM) Interfaces، من لغة البرمجة جافا. حيث يتم إطلاق الاسم Method على الدوال العضوة المنتمية لصنف ما، والتي تسمى في كوتلن Member Functions. بالتالي، المقصود بـ SAM هو واجهة ذات دالة واحدة مجرّدة ويطلق عليها في كوتلن Fun interface.

يمكننا في كوتلن إنشاء هذه الواجهة ذات الدالة الواحدة، باستخدام الكلمتين fun و interface معًا.كمثال لذلك، لدينا الشفرة التالية:

fun interface ZeroParam {
    fun function(): String
}

fun interface OneParam {
    fun function(n: Int): Int
}

fun interface TwoParams {
    fun function(i: Int, j: Int): Int
}

الشفرة أعلاه، هي عبارة عن ثلاث واجهات دالة SAM أو Fun interface تبدأ بالكلمة fun و interface ولديها دالة مجرّدة (أي دالة بدون جسم body وأقواس معقوفة) واحدة. يمكن أن تحتوي واجهة الدالة Fun interface دوال غير مجرّدة (بها جسم وأقواس معقوفة)، ولكن يجب أن تحتوي على دالة مجرّدة واحدة فقط، وإلا سيحدث الخطأ التالي:

Fun interfaces must have exactly one abstract method

الواجهات الثلاث بها دوال ذات تواقيع signatures مختلفة. فواجهة ZeroParam تحتوي على دالة ليس بها معاملات Parameters وتعيد قيمة من النوع String. وواجهة OneParam فيها دالة لديها معامل واحد من النوع Int وتعيد قيمة من النوع Int أيضًا. أمّا الواجهة الاخيرة TwoParams، فيها دالة لديها معاملين من النوع Int وتعيد قيمة من النوع Int هي الأخرى.

تطبيق واجهة الدالة الواحدة Fun interface:

يمكن تطبيق واجهة الدالة، كما نطبّق واجهة عادية في الصنف:

class Zero : ZeroParam {
    override fun function(): String = "Hello"
}

class One : OneParam {
    override fun function(n: Int) = n + 56
}

class Two : TwoParams {
    override fun function(i: Int, j: Int) = i + j
}

الصنف Zero يطبّق الواجهة ZeroParam، بالتالي عليه أن يطبّق الدالة ()function التي تحتويها الواجهة، وتنفيذها (نلاحظ أن الدالة تعيد النص “Hello” في الصنف). الصنفين One و Two فعلا المثل بالواجهات التي يطبّقونها، ويعيدان بالترتيب ناتج جمع العدد المرسل كقيمة لمعامل الدالة والعدد 56، أو ناتج جمع العددين المرسلين كقيم لمعاملي الدالة.

وبما أن  هذه الأصناف الثلاث: Zero و One و Two، أصبح لديها دوال، يمكننا استدعاء هذه الدوال، باستخدام كائنات مُنشأة من هذه الأصناف في دالة ()main.

استدعاء الدوال العضوة بإنشاء كائن من الصنف:

يمكننا استدعاء هذه الدوال بالطريقة العادية لأنها بالنسبة للصنف الذي تنتمي إليه، عبارة عن دوال عضوة Member Functions. بالتالي، سنستدعيها بإنشاء كائن من الصنف الذي تنتمي إليه. بالطبع عند استدعاء كل دالة، يجب أن نُرسل لها قيمة لمعاملها، إذا كان لديها معامل:

fun main() {
	val zero = Zero()
    println(zero.function())
	
	val one = One()
    println(one.function(44))

    val two = Two()
    println(two.function(44, 56))
}

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

Hello
100
100

لأن الدالة  في صنف Zero بها قيمة افتراضية تم طباعتها هي، لعدم وجود قيمة مرسلة أثناء تنفيذ الدالة. بالنسبة للدالتين في الصنفين الآخرين، تم إجراء العمليات الحسابية على المعاملات المرسلة لها وإعادت النتيجة.

إرسال لامبدا لواجهة الدالة الواحدة:

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

fun main() {

    val lambdaZero = ZeroParam { "Hello" }

    println(lambdaZero.function())

    val lambdaOne = OneParam { it + 56 }

    println(lambdaOne.function(44))

    val lambdaTwo =  TwoParams { i, j -> i + j }

    println(lambdaTwo.function(44, 56))

}

ارسلنا تعبير لامبدا به القيمة نص String إلى واجهة الدالة الواحدة ZeroParam لأنها القيمة التي ستعيدها الدالة. بالنسبة للواجهتين الأخريتين، أرسلنا لهما تعبيري لامبدا مناسبان لمعاملات دوالهما، والإجراء الذي يجب القيام به.

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

Hello
100
100

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

الواجهة أكثر من مجرد نمط:

عند هذه المرحلة، قد يبدو أن الواجهة Interface هو نمط لبناء الأصناف classes. هذا ليس صحيحًا تمامًا في كل الأحوال. لأنه في الغالب تستخدم الواجهة كنموذج للتفاعل مع كائن معين. يمكن مقارنة الواجهات بالعقود contracts لأن كل ما يطبّق أو ينفذ هذه الواجهة، نضمن أنه سيمتلك مجموعة من البنود (الخاصيّات والدوال) المحددة فيها.

وبالتالي، فإننا نعرف ما يمكن توقعه من الصنف، ويمكننا أن نكون على ثقة من أن أي صنف معين ينفذ واجهة معينة، سيحتوي على دالة 1 ودالة 2 …الخ. بالإضافة إلى ذلك، تحدد الواجهات أيضًا الطريقة التي يمكننا بها التفاعل مع الأصناف التي تطبقها.

الخلاصة:

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

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

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