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

يستخدم الصنف Class في كوتلن، لوصف بنية أو هيكل عام يحدد الشكل الذي يجب أن تكون عليه الكائنات المُنشأة من هذا الصنف. باستخدام هذه البنية المحددة الشكل، يمكننا إنشاء نسخ instances (كائنات) لمرات عديدة وبطرق مختلفة، من نفس الصنف. ولكن، في بعض الأحيان نحتاج إلى نسخة واحدة فقط (كائن واحد) من صنفٍ معين طوال فترة عمل البرنامج. لفعل ذلك، نحتاج إلى استخدام نمط تصميم يعرف بـ النمط المُفرد Singleton.

في هذا الدرس، سنتعرف على نمط التصميم هذا، ولماذا نستخدمه في برامجنا، وكيف تم تطبيقه في لغة كوتلن.

النمط المُفرد Singleton:

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

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

  1. يعد الاحتفاظ بنسخة واحدة فقط من الكائن مفيدًا، عندما نريد التحكم في الوصول إلى بعض الشفرات المشتركة، بأن نضعها في هذا الكائن. لأنه عند إنشاء كائن من صنف يستخدم نمط التصميم المُفرد، سيكون هناك كائنًا واحداً فقط في ذاكرة الحاسب. وعند محاولة إنشاء كائن آخر من نفس الصنف، سيتم استخدام نفس الكائن السابق بنفس بياناته المتواجدة في الذاكرة مُسبقًا.
  2. توفير وصول عام، إذ يمكن الوصول لهذه النسخة من الكائن بجميع مكوناته كما هي، من أي مكان في شفرة البرنامج (إذا لم يتم منع ذلك عبر محدد الوصول private). ويمكن حينها وضع كل الشفرة التي نريد مشاركتها واستخدامها كما هي في كل مكان، داخل هذا الكائن.

لمزيد من الفهم، فلنلقي نظرة على الصورة التالية:

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

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

هذه كانت نبذة سريعة عن نمط التصميم هذا، والذي يستخدم في لغات برمجة أخرى غير كوتلن. إذًا، كيف تم تطبيقه في كوتلن؟ هذا ما سنعرفه نظريًا وعمليًا في الفقرات أدناه من هذا الدرس.

الكلمة المفتاحية object:

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

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

object JustOne

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

object JustOne {
  val number = 2
  fun multiply() = number * 10
  fun multiplyAgain() = multiply() * 20
}

الآن الصنف JustOne، يحتوي على خاصيّة number ودالة ()multiply التي عملها هو إعادة نتيجة ضرب قيمة الخاصيّة number في العدد 10 ودالة ()multiplyAgain والتي ستعيد ضرب قيمة دالة ()multiply في العدد 20. لا شئ جديد هنا عما درسناه في درس الأصناف ما عدا وجود الكلمة object.

استخدام الصنف JustOne:

في العادة نستخدم أقواس الباني () لإنشاء كائن من أي صنف كالتالي:

val justOne = JustOne()

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

fun main() {
    JustOne.number
    JustOne.multiply()
    JustOne.multiplyAgain()
}

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

fun main() {
    println(
        """
            ${JustOne.number}
            ${JustOne.multiply()}
            ${JustOne.multiplyAgain()}
        """.trimIndent()
    )
}

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

2
20
400

العدد 2 هو قيمة الخاصيّة number لذا تمت طباعته كما هو. العدد 20 هو نتيجة ضرب قيمة الخاصيّة في العدد 10. والعدد 400 هو نتيجة ضرب القيمة العائدة من دالة ()multiply في العدد 20. ما نلاحظه هنا، هو على الرغم من أن الصنف يتم استدعاؤه  كل مرة في سطر جديد، ولكن تبقى البيانات هي نفسها لا تتغير. فمثلًا، السطر التالي:

JustOne.multiplyAgain()

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

استدعاء الكائن object في حزمة أخرى:

يحتفظ صنف الـ Singleton، بكل التغييرات التي تجري على اعضائه (الخاصيّات والدوال وغيرها)، حتى لو تم إجراء هذه التغييرات في حزم Packages مختلفة في مشروع البرنامج أو التطبيق. فمثلًا، إذا كان لدينا صنف Singleton في حزمة وملف لوحده، ثم أجرينا تغييرات في حزم وملفات مختلفة، وطبعنا النتيجة في دالة ()main والتي تتواجد بدورها في حزمة وملف مختلف كالتالي:

SingletonClass.kt

لدينا في الصورة، ملف اسمه SingletonClass يتواجد داخل الحزمة singleton، والتي يظهر رابطها في أعلى الملف. داخل هذا الملف، لدينا صنف SingletonObject وهو كائن يتبع النمط المُفرد لاستخدامه الكلمة المفتاحية object. داخل الكائن لدينا خاصيّة number والتي أعلناها عبر استخدام var لنتمكن من تغيير قيمتها في أي مكان.

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

ملف file1:

file1.kt

في أعلى الملف يظهر اسم الحزمة التي يتواجد بها وهي package1. ثم لدينا تضمين import لصنف SingletonObject عبر كتابة رابط مباشر إلى مكان تواجده في مشروع البرنامج. وبما أنه تم تعريف ملفنا الحالي على مكان الصنف SingletonObject، يمكننا الآن أن نستخدمه في دالة ()multiply. داخل الدالة نقوم بتغيير قيمة خاصيّة number الخاصة بالكائن SingletonObject، بضربها في العدد 10.

ملف file2:

file2.kt

في هذا الملف الذي يتواجد في حزمة مختلفة أيضًا، نقوم بجلب أو تضمين رابط لمكان تواجد الكائن SingletonObject، حتى نستطيع استخدامه في دالة ()subtract الخاصة بهذا الملف. داخل الدالة نقوم بتغيير قيمة الخاصية number بقسمتها على العدد 26.

في العادة، عندما نُنشئ كائن جديد من صنفٍ ما في ملف أو مكان مختلف في كل مرة، يتم حجز مكان منفصل في ذاكرة الحاسب لهذا الكائن، ويحفظ به البيانات والعمليات التي تجري عليه. فمثلًا، سيتم إنشاء كائن عبر تنفيذ السطر في ملف file1.kt، ثم يتم حفظ قيمة الخاصيّة بعد ضربها في العدد 10، لتكون قيمتها تساوي العدد 690. ثم في ملف file2.kt، سيتم إنشاء كائن مختلف ويتم أخذ قيمة الخاصيّة كما هي في الصنف الأصلي وهي العدد 2، وقسمتها على العدد 26. أي 2/26.

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

ملف file3:

لدينا في هذا الملف، دالة ()main والتي سنستخدمها لتنفيذ التعليمات البرمجية في الحزم والملفات الأخرى، ومن ثم طباعتها لرؤية النتيجة:

file3.kt

في أعلى الملف يظهر اسم الحزمة التي يتواجد بها الملف، تضمين روابط لأماكن تواجد دالة ()multiply ودالة ()subtract والكائن SingletonObject، لنستطيع استخدام الثلاثة في هذا الملف. داخل دالة ()main، نفذنا دالتي ()multiply و ()subtract.

ستقوم دالة ()multiply بتغيير قيمة الخاصيّة number إلى العدد 690. ثم ستقوم دالة ()subtract بقسمة قيمة الخاصيّة على العدد 26. هل ستقوم الدالة بتقسيم القيمة الأصلية للخاصيّة وهي العدد 2، أو القيمة الجديدة والتي تم تغييرها عبر دالة ()multiply وهي العدد 690؟ هذا ما سنعرفه حينما نقوم بطباعة قيمة الخاصيّة وهو ما نفعله في آخر سطر في الصورة أعلاه.

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

26

وهي ناتج ضرب 69 في 10 ويساوي 690. ثم تقسيم 690 على 26 والذي يساوي 26.5384615385. ولكن لأن نوع البيانات Int لا يحتفظ بالكسور، تم تجاهل ما بعد الفاصلة العشرية، لتكون النتيجة هي العدد 26 فقط. (شرحنا هذا تفصيليًا في درس أنواع العدد Number Types).

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

ملحوظة: نلاحظ أنه استطعنا الوصول للكائن داخل حزم وملفات مختلفة، هذا لن يحدث إذا تم وضع محدد وصول private للكائن.

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

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