Menu
Coddy logo textTech
flag Ar iconالعربيةdown icon

نمط 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) في حدوث مشكلات.

challenge icon

التحدي

سهل

لنقم ببناء نمط 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 قيد التشغيل.

    اقرأ مدخلين:

    1. اسم الإعداد المسبق (سلسلة نصية)
    2. مستوى صوت مخصص (عدد صحيح)

    أولاً، اعرض الإعدادات الافتراضية الأولية عن طريق استدعاء 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;
}
quiz iconاختبر نفسك

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

جميع دروس Object Oriented Programming