جدول Singleton
جزء من قسم Object Oriented Programming في رحلة Lua على Coddy — الدرس 63 من 70.
أحياناً تحتاج إلى نسخة واحدة فقط من شيء ما في برنامجك بالكامل—مدير إعدادات، أو نظام تسجيل، أو إعدادات اللعبة. إن إنشاء نسخ متعددة قد يسبب مشاكل: أي نسخة منها تحتوي على البيانات الصحيحة؟ يضمن نمط Singleton وجود نسخة واحدة فقط.
في لغة Lua، يعد تنفيذ نمط Singleton بسيطاً بشكل مدهش بفضل طريقة عمل الوحدات (modules). فعندما تستخدم require لاستدعاء وحدة ما، تقوم Lua بتخزين النتيجة مؤقتاً. وبالتالي، فإن الاستدعاءات اللاحقة لـ require تعيد الجدول المخزن مؤقتاً، وليس جدولاً جديداً.
-- GameConfig.lua
local GameConfig = {
volume = 80,
difficulty = "normal"
}
function GameConfig:setVolume(v)
self.volume = v
end
return GameConfig
الآن، في أي مكان في برنامجك:
local config1 = require("GameConfig")
local config2 = require("GameConfig")
config1:setVolume(50)
print(config2.volume) -- 50 (نفس الجدول!)
كلا من config1 و config2 يشيران إلى نفس الجدول تماماً. التغييرات التي يتم إجراؤها من خلال متغير واحد تكون مرئية من خلال الآخر. يحدث هذا تلقائياً لأن require الخاصة بـ Lua تخزن القيمة المرجعة في package.loaded وتعيد استخدامها.
على عكس نمط المصنع (Factory pattern) الذي ينشئ كائنات جديدة، يضمن نمط المفرد (Singleton) حالة مشتركة عبر تطبيقك بالكامل. وهذا مثالي للإعدادات العامة التي تحتاج أجزاء متعددة من الكود الخاص بك إلى الوصول إليها وتعديلها بشكل متسق.
التحدي
سهللنقم ببناء مدير إعدادات اللعبة باستخدام نمط Singleton! ستقوم بإنشاء وحدة تكوين (configuration module) تضمن وصول كل جزء من برنامجك إلى نفس الإعدادات تماماً—بغض النظر عن عدد مرات استدعائها.
ستقوم بتنظيم الكود الخاص بك عبر ملفين:
GameSettings.lua: قم بإنشاء وحدة Singleton تخزن تكوين اللعبة. يجب أن يتضمن جدول الإعدادات الخاص بكmusicVolume(يبدأ بـ 70)، وsfxVolume(يبدأ بـ 100)، وdifficulty(يبدأ بـ"normal"). أضف ثلاث دوال::setMusicVolume(value)، و:setSfxVolume(value)، و:setDifficulty(level)لتعديل هذه الإعدادات. تذكر إرجاع الجدول في نهاية الوحدة!main.lua: أثبت أن Singleton يعمل عن طريق استدعاء وحدة GameSettings مرتين في متغيرين مختلفين. استخدم المتغير الأول لتغيير مستوى صوت الموسيقى والصعوبة. ثم استخدم المتغير الثاني لطباعة الإعدادات الثلاثة جميعاً—إذا كان Singleton يعمل بشكل صحيح، فسيشير كلا المتغيرين إلى نفس الجدول، وبالتالي فإن التغييرات التي يتم إجراؤها من خلال أحدهما تكون مرئية من خلال الآخر.
ستتلقى مدخلين:
- مستوى صوت الموسيقى الجديد (رقم)
- مستوى الصعوبة الجديد (سلسلة نصية مثل
"easy"أو"hard")
بعد تعديل الإعدادات من خلال المتغير الأول، اطبع الإعدادات الثلاثة باستخدام المتغير الثاني، كل منها في سطر خاص به بهذا الترتيب: مستوى صوت الموسيقى، مستوى صوت المؤثرات الصوتية (sfx)، ثم الصعوبة.
على سبيل المثال، إذا كانت المدخلات هي 50 و hard، يجب أن يكون الناتج:
50
100
hardإذا كانت المدخلات هي 25 و easy، يجب أن يكون الناتج:
25
100
easyلاحظ أن مستوى صوت المؤثرات الصوتية (sfx) يظل عند قيمته الافتراضية 100 لأننا لم نقم بتغييره أبداً—لكن مستوى صوت الموسيقى والصعوبة يعكسان التغييرات التي تم إجراؤها من خلال المتغير الأول، مما يثبت أن كلا المتغيرين يشيران إلى نفس نسخة Singleton!
ورقة مرجعية
يضمن نمط Singleton وجود نسخة واحدة فقط من الكائن طوال البرنامج بأكمله. هذا مفيد لمديري التكوين (configuration managers)، أو أنظمة التسجيل (logging systems)، أو إعدادات الألعاب حيث قد تؤدي النسخ المتعددة إلى عدم اتساق البيانات.
في لغة Lua، يتم تنفيذ نمط Singleton بشكل طبيعي من خلال نظام الوحدات (module system). عندما تستخدم require لاستدعاء وحدة معينة، تقوم Lua بتخزين النتيجة في package.loaded. الاستدعاءات اللاحقة لـ require تعيد نفس الجدول المخزن مؤقتاً، وليس جدولاً جديداً.
إنشاء وحدة Singleton:
-- GameConfig.lua
local GameConfig = {
volume = 80,
difficulty = "normal"
}
function GameConfig:setVolume(v)
self.volume = v
end
return GameConfig
استخدام الـ Singleton:
local config1 = require("GameConfig")
local config2 = require("GameConfig")
config1:setVolume(50)
print(config2.volume) -- 50 (نفس الجدول!)
يشير كل من config1 و config2 إلى نفس الجدول تماماً. التغييرات التي يتم إجراؤها من خلال متغير واحد تظهر فوراً من خلال الآخر، مما يضمن حالة مشتركة (shared state) عبر تطبيقك بالكامل.
يختلف نمط Singleton عن نمط Factory: فبينما يقوم Factory بإنشاء كائنات جديدة، يضمن Singleton نسخة مشتركة واحدة بحالة متسقة.
جرّب بنفسك
-- main.lua - توضيح نمط Singleton
-- قراءة المدخلات
local newMusicVolume = tonumber(io.read())
local newDifficulty = io.read()
-- TODO: استدعاء وحدة GameSettings في متغير (مثلاً، settings1)
-- TODO: استدعاء وحدة GameSettings مرة أخرى في متغير مختلف (مثلاً، settings2)
-- (إذا كان Singleton يعمل بشكل صحيح، فسيشير كلا المتغيرين إلى نفس الـ table!)
-- TODO: استخدم المتغير الأول لـ:
-- - تعيين مستوى صوت الموسيقى إلى newMusicVolume
-- - تعيين الصعوبة إلى newDifficulty
-- TODO: استخدم المتغير الثاني لطباعة الإعدادات
-- طباعة كل منها في سطر مستقل: musicVolume، sfxVolume، difficulty
يتضمن هذا الدرس اختبارًا قصيرًا. ابدأ الدرس للإجابة عليه وتتبّع تقدمك.
جميع دروس Object Oriented Programming
4مشروع: بنك رقمي
إعداد المشروعطريقة الإيداع7تعدد الأشكال وإعادة التعريف
إعادة تعريف الدوالاستدعاء دوال الفئة الأبDuck Typingواجهة مشتركةالتحقق من النوعمراجعة - أدوار الموظفين2نمط النموذج الأولي للفئات
مفهوم النموذج الأوليالربط باستخدام __indexالمنشئ :new()تهيئة الخصائصنسخ مستقلةملخص - مصنع السيارات5تحميل المعاملات في OOP
جمع الكائناتطرح الكائناتدمج الكائناتمقارنة الكائنات (<, >)مراجعة - رياضيات المحفظة8التغليف
اصطلاحات التسميةاستخدام Closures للخصوصيةالوصول عبر Closuresجداول للقراءة فقطمنطق التحقق من الصحةمراجعة - الخزنة الآمنة11أنماط التصميم (مبسط)
دوال المصنعجدول Singletonنمط الـ Iteratorالمراقب (المستمع)مراجعة - مصنع الـ Logger3حالة الكائن وسلوكه
متغيرات النسخةدالات الجلب (Getter Methods)دالات التعيين (Setter Methods)الخصائص المحسوبةتنسيق النصوصالتحقق من المساواةمراجعة - درجة الطالب6أساسيات الوراثة
إعداد الوراثةوراثة الـ Methodsتوسيع الـ Constructorإضافة Methods للابنالمشترك مقابل الفريدملخص - التسلسل الهرمي للأشكال