نمط Singleton
جزء من قسم Object Oriented Programming في رحلة C++ على Coddy — الدرس 91 من 104.
يضمن نمط Singleton أن يكون للفئة (class) مثيل واحد فقط طوال البرنامج، ويوفر نقطة وصول عامة إليه. يُعد هذا مفيداً للموارد التي يجب مشاركتها، مثل مدير الإعدادات (configuration manager)، أو مسجل الأحداث (logger)، أو مجمع اتصالات قاعدة البيانات (database connection pool).
الفكرة الأساسية هي جعل المنشئ (constructor) خاصاً (private) بحيث لا يمكن لأي شخص إنشاء كائنات (instances) بشكل مباشر، ثم توفير دالة ثابتة (static method) تعيد الكائن الوحيد:
#include <iostream>
#include <string>
class Logger {
private:
std::string logFile;
// منشئ خاص - يمنع إنشاء الكائنات بشكل مباشر
Logger() : logFile("app.log") {}
// حذف منشئ النسخ وعامل التعيين
Logger(const Logger&) = delete;
Logger& operator=(const Logger&) = delete;
public:
// دالة ثابتة للوصول إلى الكائن الوحيد
static Logger& getInstance() {
static Logger instance; // يتم إنشاؤه مرة واحدة، ويظل موجوداً حتى ينتهي البرنامج
return instance;
}
void log(const std::string& message) {
std::cout << "[LOG] " << message << "\n";
}
};
int main() {
Logger::getInstance().log("Application started");
Logger& logger = Logger::getInstance();
logger.log("Processing data");
}إن static Logger instance الموجود داخل getInstance() هو ميزة في C++11 تُعرف باسم "المتغيرات الساكنة السحرية" (magic statics) - وهي آمنة للخيوط (thread-safe) ومضمونة التهيئة لمرة واحدة فقط. من خلال حذف منشئ النسخ (copy constructor) وعامل التعيين (assignment operator)، نمنع أي شخص من إنشاء نسخ من الكائن الوحيد (singleton) الخاص بنا.
استخدم نمط Singleton باعتدال - فهو يقدم حالة عامة (global state) مما قد يجعل الاختبار أكثر صعوبة. وهو الأنسب للموارد المشتركة حقاً حيث قد يتسبب وجود عدة مثيلات (instances) في حدوث مشكلات.
التحدي
سهللنقم ببناء نمط Singleton لـ GameSettings يدير تكوينات تطبيق لعبة. هذه حالة استخدام مثالية لنمط Singleton — فأنت تريد مدير إعدادات واحدًا بالضبط يمكن لجميع أجزاء لعبتك الوصول إليه، مما يضمن تكويناً متسقاً في جميع الأنحاء.
ستقوم بتنظيم الكود الخاص بك عبر ثلاثة ملفات:
GameSettings.h: قم بتعريف فئة Singleton التي تدير تكوين اللعبة.يجب أن تخزن فئة
GameSettingsثلاثة إعدادات كأعضاء خاصين (private members):difficulty(سلسلة نصية)،volume(عدد صحيح من 0-100)، وfullscreen(قيمة منطقية). قم بتهيئتها بالقيم الافتراضية:"Normal"، و50، وfalse.قم بتنفيذ نمط Singleton بشكل صحيح:
- اجعل المنشئ (constructor) خاصاً (private)
- احذف منشئ النسخ (copy constructor) وعامل التعيين (assignment operator)
- وفر دالة
getInstance()ثابتة (static) تعيد مرجعاً (reference) إلى المثيل الوحيد
أضف هذه الدوال العامة (public methods) للتفاعل مع الإعدادات:
setDifficulty(const std::string& diff)— لتحديث الصعوبةsetVolume(int vol)— لتحديث مستوى الصوتsetFullscreen(bool fs)— لتحديث وضع ملء الشاشةdisplaySettings()— لطباعة جميع الإعدادات الحالية
GameModule.h: قم بإنشاء وحدة منفصلة تصل إلى الـ singleton.قم بتعريف دالة تسمى
applyGamePresetتأخذ اسم الإعداد المسبق (سلسلة نصية). يجب أن تصل هذه الدالة إلى singleton الخاص بـGameSettingsوتقوم بتكوينه بناءً على الإعداد المسبق:"casual"— يضبط الصعوبة على"Easy"، ومستوى الصوت على70، وملء الشاشة علىfalse"competitive"— يضبط الصعوبة على"Hard"، ومستوى الصوت على30، وملء الشاشة علىtrue- أي إعداد مسبق آخر — يبقي الإعدادات الافتراضية دون تغيير
يوضح هذا كيف يمكن لأجزاء مختلفة من البرنامج الوصول إلى نفس مثيل singleton وتعديله.
main.cpp: اجمع كل شيء معاً لإظهار الـ singleton قيد التشغيل.اقرأ مدخلين:
- اسم الإعداد المسبق (سلسلة نصية)
- مستوى صوت مخصص (عدد صحيح)
أولاً، اعرض الإعدادات الافتراضية الأولية عن طريق استدعاء
displaySettings()على الـ singleton. اطبع سطراً فارغاً بعد ذلك.ثم قم بتطبيق الإعداد المسبق باستخدام دالة
applyGamePresetواعرض الإعدادات مرة أخرى. اطبع سطراً فارغاً بعد ذلك.أخيراً، قم بتجاوز مستوى الصوت فقط باستخدام قيمة المدخل المخصصة واعرض الإعدادات النهائية.
يجب أن تقوم دالة displaySettings() بالإخراج بهذا التنسيق الدقيق:
Difficulty: [value]
Volume: [value]
Fullscreen: [yes/no]على سبيل المثال، مع المدخلات casual و 85:
Difficulty: Normal
Volume: 50
Fullscreen: no
Difficulty: Easy
Volume: 70
Fullscreen: no
Difficulty: Easy
Volume: 85
Fullscreen: noمع المدخلات competitive و 20:
Difficulty: Normal
Volume: 50
Fullscreen: no
Difficulty: Hard
Volume: 30
Fullscreen: yes
Difficulty: Hard
Volume: 20
Fullscreen: yesلاحظ كيف يصل كل من main.cpp و GameModule.h إلى نفس مثيل singleton — التغييرات التي يتم إجراؤها في مكان واحد تكون مرئية في كل مكان. هذه هي قوة نمط Singleton للموارد المشتركة مثل إعدادات اللعبة.
ورقة مرجعية
يضمن نمط Singleton أن يكون للفئة (class) مثيل واحد فقط طوال البرنامج ويوفر وصولاً عاماً إليه. وهو مفيد للموارد المشتركة مثل مديري التكوين (configuration managers)، أو المسجلات (loggers)، أو تجمعات اتصالات قاعدة البيانات (database connection pools).
خطوات التنفيذ الرئيسية:
- اجعل المنشئ (constructor) خاصاً (private) لمنع إنشاء مثيل مباشر
- احذف منشئ النسخ (copy constructor) وعامل التعيين (assignment operator) لمنع النسخ
- وفر طريقة ثابتة (static method) تعيد مرجعاً إلى المثيل الوحيد
class Logger {
private:
std::string logFile;
// منشئ خاص
Logger() : logFile("app.log") {}
// حذف منشئ النسخ وعامل التعيين
Logger(const Logger&) = delete;
Logger& operator=(const Logger&) = delete;
public:
// طريقة ثابتة للوصول إلى المثيل الوحيد
static Logger& getInstance() {
static Logger instance; // يتم إنشاؤه مرة واحدة، ويعيش حتى ينتهي البرنامج
return instance;
}
void log(const std::string& message) {
std::cout << "[LOG] " << message << "\n";
}
};
// الاستخدام
Logger::getInstance().log("Application started");
Logger& logger = Logger::getInstance();
logger.log("Processing data");يستخدم static Logger instance داخل getInstance() ما يسمى بـ "magic statics" في C++11 - وهو آمن للخيوط (thread-safe) ومضمون التهيئة مرة واحدة فقط.
استخدم Singleton بحذر لأنه يقدم حالة عامة (global state) قد تجعل الاختبار أكثر صعوبة. هو الأنسب للموارد المشتركة حقاً حيث قد تسبب المثيلات المتعددة مشاكل.
جرّب بنفسك
#include <iostream>
#include <string>
#include "GameSettings.h"
#include "GameModule.h"
using namespace std;
int main() {
// قراءة المدخلات
string presetName;
int customVolume;
cin >> presetName;
cin >> customVolume;
// TODO: الحصول على مثيل singleton لـ GameSettings
// TODO: عرض الإعدادات الافتراضية الأولية
// TODO: طباعة سطر فارغ
// TODO: تطبيق الإعداد المسبق باستخدام دالة applyGamePreset
// TODO: عرض الإعدادات بعد تطبيق الإعداد المسبق
// TODO: طباعة سطر فارغ
// TODO: تجاوز مستوى الصوت بـ customVolume
// TODO: عرض الإعدادات النهائية
return 0;
}
يتضمن هذا الدرس اختبارًا قصيرًا. ابدأ الدرس للإجابة عليه وتتبّع تقدمك.
جميع دروس Object Oriented Programming
1أساسيات الـ OOP
الملفات الخارجيةبناء وتجميع (Compilation) لغة C++ملفات الـ Header وملفات الـ Sourceالـ Namespaces والـ Scopeمقدمة في الـ OOP في C++الـ Classes مقابل الـ Objectsالمؤشر 'this'الـ Methods (الدوال الأعضاء)الـ Attributes (بيانات الأعضاء)أساسيات الـ Ctors والـ Dtorsمراجعة - آلة حاسبة بسيطة4خصائص الـ Class
أعضاء الـ Instance مقابل الـ Staticدوال الـ Getters والـ Settersدوال الأعضاء الثابتة (Const)الكلمة المفتاحية Mutableالدوال والمتغيرات الساكنة (Static)الدوال والفئات الصديقة (Friend)مراجعة - مدير الحساب البنكي7الوراثة
الوراثة الأساسيةمستويات الوصول في الوراثةترتيب استدعاء الـ Ctor والـ Dtorإعادة تعريف الدوال (Method Overriding)الدوال الافتراضية والـ VTableالوراثة المتعددةالوراثة الافتراضيةمراجعة - هيكلية الموظفين10نظرة عامة على STL
نظرة عامة وفلسفة STLحاويات STLالـ Iteratorsخوارزميات STLالـ Functors وتعبيرات Lambdaمراجعة - تكرار الكلمات13أنماط التصميم - الجزء الأول
مقدمة في أنماط التصميمنمط Singletonنمط Factory و Abstract Factoryنمط Builderنمط Observerنمط Strategy2إدارة الذاكرة
ذاكرة Stack مقابل Heapالمؤشرات والمراجعالذاكرة الديناميكية (new/delete)المؤشرات الذكية في C++RAII في C++مراجعة - مدير المصفوفات الديناميكية5التغليف (Encapsulation)
محددات الوصول في C++محددات الوصول بشكل متعمقإخفاء المعلوماتStruct مقابل Classالأصناف المتداخلة والداخليةمراجعة - نظام سجلات الطلاب8تعدد الأشكال (Polymorphism)
تعدد الأشكال: وقت التجميع مقابل وقت التشغيلالتحميل الزائد للدوال (Function Overloading)مراجعة الدوال الافتراضية (Virtual Functions)الدوال الافتراضية البحتة (Pure Virtual Functions)الأصناف المجردة (Abstract Classes)تصميم الواجهات (Interface) في C++التحويل الديناميكي (Dynamic Casting) و RTTIملخص - حاسبة الأشكال11مفاهيم OOP المتقدمة
التركيب مقابل الوراثةالـ Mixins عبر CRTPنمط Pimplمحو النوع (Type Erasure)أصناف Enum والأنواع القويةمعالجة الاستثناءات في OOPتسلسلات الاستثناءات المخصصة14أنماط التصميم - الجزء الثاني
نمط الأمرنمط المحولنمط المزيننمط قالب الطريقةنمط الحالةنمط التركيبRAII كنمط3المنشئات والموادم
المنشئ الافتراضيالمنشئ ذو المعاملاتمنشئ النسخمنشئ النقلقوائم تهيئة المنشئالمنشئات المفوضةتعمق في الموادمقاعدة الثلاثة / الخمسة / الصفرمراجعة - فئة String6تحميل العوامل (Operator Overloading)
مقدمة في تحميل العواملتحميل العوامل الحسابيةتحميل عوامل المقارنةعوامل Streamتحميل عامل التعيينتحميل العوامل [] و ()عوامل تحويل النوعمراجعة - Matrix Class9القوالب
قوالب الدوالقوالب الأصنافتخصيص القوالبالقوالب المتغيرةأساسيات SFINAE و Type Traitsمراجعة - الحاويات العامة