Singleton Pattern

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

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

مقدمة

في المقالة السابقة من سلسلة Design Patterns، قدمنا ​​الفكرة العامة لأنماط التصميم وشرحنا كيف تساعد المطورين على حل مشكلات تصميم البرامج الشائعة. ناقشنا أيضًا classes الرئيسية لأنماط التصميم: الأنماط الإبداعية والهيكلية والسلوكية.

ينتمي Singleton Pattern إلى أنماط التصميم الإبداعي فئة. تركز الأنماط الإبداعية على كيفية إنشاء الكائنات وكيف يمكن التحكم في إنشاء الكائنات أو تبسيطها.

يتم استخدام Singleton بشكل شائع للتحكم في الوصول إلى الموارد المشتركة مثل كائنات التكوين، والمسجلين، ومديري ذاكرة التخزين المؤقت، ومديري اتصال قاعدة البيانات، والخدمات على مستوى التطبيق. ومع ذلك، غالبًا ما يفضل تصميم البرامج الحديثة Dependency Injection على الاستخدام المباشر لـ Singleton، خاصة في التطبيقات الكبيرة والقابلة للاختبار.

ما هو Singleton Pattern؟

Singleton Pattern هو نمط تصميم يقيد class بمثيل كائن واحد فقط ويوفر طريقة للوصول إلى نفس المثيل من أجزاء مختلفة من التطبيق.

بعبارات بسيطة، يجيب سينجلتون على هذا السؤال: كيف يمكننا التأكد من وجود كائن واحد فقط من class؟

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

الفكرة الرئيسية لسينجلتون

تعتمد الفكرة الرئيسية لـ Singleton على ثلاث قواعد:

  • يقوم الclass بتخزين مثيله الفردي داخليًا.

  • يكون constructor خاصًا أو محميًا لمنع إنشاء الكائن المباشر من الخارج.

  • يوفر الclass طريقة ثابتة عامة تقوم بإرجاع مثيل واحد.

نظرًا لأن constructor لا يمكن الوصول إليه بشكل عام، فلا يمكن للتعليمات البرمجية الخارجية إنشاء كائنات جديدة باستخدام الكلمة الأساسية الجديدة. بدلاً من ذلك، يجب عليه استدعاء أسلوب مثل getInstance.

لماذا يتم استخدام سينجلتون

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

تتضمن الأسباب الشائعة لاستخدام Singleton ما يلي:

  • التحكم في الوصول إلى مورد مشترك.

  • ضمان مثيل واحد لمدير التكوين.

  • توفير مثيل مسجل واحد.

  • إدارة كائن ذاكرة التخزين المؤقت المشتركة.

  • تجنب التهيئة المتكررة للأشياء باهظة الثمن.

ومع ذلك، يجب على المطورين توخي الحذر. فقط لأن الclass يمكن أن يكون Singleton لا يعني أنه يجب أن يكون Singleton. يجب أن يحل النمط مشكلة التصميم الحقيقية.

هيكل المفردة الأساسية

تحتوي فئة Singleton الأساسية عادةً على خاصية ثابتة خاصة وconstructor خاص وطريقة ثابتة عامة للوصول إلى المثيل.

class Singleton
{
    private static ?Singleton $instance = null;

    private function __construct()
    {
    }

    public static function getInstance(): Singleton
    {
        if (self::$instance === null) {
            self::$instance = new Singleton();
        }

        return self::$instance;
    }
}

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

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

مثال مفرد في PHP

يوضح المثال التالي تطبيق Singleton الأكثر عملية لمدير تكوين التطبيق:

class ConfigManager
{
    private static ?ConfigManager $instance = null;

    private array $settings = [];

    private function __construct()
    {
        $this->settings = [
            'app_name' => 'My Application',
            'environment' => 'production',
            'debug' => false,
        ];
    }

    public static function getInstance(): ConfigManager
    {
        if (self::$instance === null) {
            self::$instance = new ConfigManager();
        }

        return self::$instance;
    }

    public function get(string $key): mixed
    {
        return $this->settings[$key] ?? null;
    }
}

$config = ConfigManager::getInstance();
echo $config->get('app_name');

In this example, the ConfigManager class can only be created once. Any part of the application that calls ConfigManager::getInstance will receive the same instance.

منع الاستنساخ في Singleton

في PHP، يمكن استنساخ كائن باستخدام الكلمة الأساسية clone. لحماية Singleton من التكرار، عادةً ما يمنع المطورون الاستنساخ عن طريق جعل طريقة __clone خاصة.

class ConfigManager
{
    private static ?ConfigManager $instance = null;

    private function __construct()
    {
    }

    private function __clone()
    {
    }

    public static function getInstance(): ConfigManager
    {
        if (self::$instance === null) {
            self::$instance = new ConfigManager();
        }

        return self::$instance;
    }
}

بجعل __clone خاصًا، لا يمكن للتعليمات البرمجية الخارجية استنساخ كائن Singleton.

منع إلغاء التسلسل في Singleton

هناك طريقة أخرى لإعادة إنشاء الكائن وهي من خلال التسلسل وإلغاء التسلسل. في إصدارات PHP الحديثة، يمكن للمطورين منع إلغاء التسلسل عن طريق تحديد طريقة __wakeup التي تطرح استثناءً.

class ConfigManager
{
    private static ?ConfigManager $instance = null;

    private function __construct()
    {
    }

    private function __clone()
    {
    }

    public function __wakeup(): void
    {
        throw new Exception('Cannot unserialize a singleton.');
    }

    public static function getInstance(): ConfigManager
    {
        if (self::$instance === null) {
            self::$instance = new ConfigManager();
        }

        return self::$instance;
    }
}

يساعد هذا في حماية Singleton من إعادة إنشائه من خلال إلغاء التسلسل.

التهيئة البطيئة

تعني التهيئة البطيئة أنه يتم إنشاء مثيل Singleton فقط عند طلبه لأول مرة. يمكن أن يكون هذا مفيدًا عندما يكون إنشاء الكائن مكلفًا أو عندما لا تكون هناك حاجة للكائن دائمًا أثناء الطلب.

في تطبيق Singleton الأساسي، تحدث التهيئة البطيئة داخل طريقة getInstance. لا يتم إنشاء الكائن حتى يتم استدعاء getInstance.

يمكن أن يؤدي هذا إلى تقليل الاستخدام غير الضروري للموارد، ولكنه يعني أيضًا أن منطق التهيئة مخفي داخل الclass. في التطبيقات الأكبر حجمًا، قد يوفر Dependency Injection تحكمًا أوضح في وقت وكيفية إنشاء الكائنات.

تهيئة حريصة

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

مع التهيئة المتلهفة، يكون الكائن موجودًا على الفور. قد يؤدي ذلك إلى تسهيل الوصول، ولكنه قد يؤدي إلى إنشاء كائنات غير مستخدمة وزيادة تكلفة بدء التشغيل.

عادةً ما تكون التهيئة البطيئة أكثر شيوعًا في أمثلة PHP Singleton.

حالات الاستخدام الحقيقي لـ Singleton

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

تشمل حالات الاستخدام المحتملة ما يلي:

  • مدير التكوين: كائن مشترك يقرأ إعدادات التطبيق ويوفرها.

  • المسجل: كائن تسجيل مركزي يستخدم عبر التطبيق.

  • مدير ذاكرة التخزين المؤقت: كائن الوصول إلى ذاكرة التخزين المؤقت المشتركة.

  • تسجيل التطبيق: مكان مركزي للقيم المشتركة على مستوى التطبيق.

  • مدير الموارد: فئة تتحكم في الوصول إلى مورد محدود.

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

Singleton لمثال المسجل

يمكن تنفيذ المُسجل البسيط باعتباره Singleton عندما يحتاج التطبيق إلى مثيل تسجيل مشترك واحد.

class Logger
{
    private static ?Logger $instance = null;

    private function __construct()
    {
    }

    public static function getInstance(): Logger
    {
        if (self::$instance === null) {
            self::$instance = new Logger();
        }

        return self::$instance;
    }

    public function log(string $message): void
    {
        echo date('Y-m-d H:i:s') . ' - ' . $message;
    }
}

$logger = Logger::getInstance();
$logger->log('Application started.');

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

فوائد Singleton Pattern

يتمتع Singleton Pattern بالعديد من الفوائد عند استخدامه بشكل صحيح. فهو يمنح المطورين التحكم في إنشاء الكائنات ويضمن وجود مثيل واحد فقط.

تشمل الفوائد الرئيسية ما يلي:

  • يضمن مثيل واحد مشترك للفئة.

  • يوفر نقطة وصول عالمية لهذا المثيل.

  • يمكن أن يقلل من إنشاء الكائن المتكرر.

  • يمكن مركزية الوصول إلى الموارد المشتركة.

  • يمكن أن يكون سهل التنفيذ في التطبيقات الصغيرة.

هذه المزايا تجعل Singleton جذابة للمبتدئين، ولكن النمط له أيضًا عيوب مهمة.

عيوب Singleton Pattern

يمكن أن يتسبب Singleton Pattern في حدوث مشكلات إذا تم استخدامه كثيرًا أو في المكان الخطأ. واحدة من أكبر المشكلات هي أنها تقدم حالة عالمية في التطبيق.

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

Singleton can also make testing harder. If a class directly calls ConfigManager::getInstance or Logger::getInstance, it becomes difficult to replace that dependency with a fake object during unit tests.

مشكلة أخرى هي اقتران ضيق. الكود التي تعتمد بشكل مباشر على فئة Singleton تصبح مرتبطة بقوة بهذا التنفيذ المحدد.

مشاكل المفردة والاختبار

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

إذا كان الclass يستخدم Dependency Injection، فيمكن أن يوفر الاختبار تبعية زائفة. ولكن إذا قام الclass باستدعاء Singleton مباشرة، فإن استبدال تلك التبعية يصبح أكثر صعوبة.

For example, a service that calls Logger::getInstance directly is tied to the Logger class. A better design may be to depend on a LoggerInterface and inject the logger from outside.

سينجلتون ضد Dependency Injection

غالبًا ما يكون Dependency Injection بديلاً أفضل لـ Singleton في تصميم البرامج الحديثة. بدلاً من مطالبة الclass بجلب تبعيته من نقطة وصول عامة، يتم تمرير التبعية إلى الclass من الخارج.

على سبيل المثال، بدلاً من هذا:

class OrderService
{
    public function completeOrder(): void
    {
        $logger = Logger::getInstance();
        $logger->log('Order completed.');
    }
}

سيبدو نهج Dependency Injection كما يلي:

class OrderService
{
    public function __construct(
        private LoggerInterface $logger
    ) {
    }

    public function completeOrder(): void
    {
        $this->logger->log('Order completed.');
    }
}

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

سينجلتون مقابل الطبقة الثابتة

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

لا يزال Singleton كائنًا. يمكنه تنفيذ interfaces واستخدام الوراثة والاحتفاظ بحالة الكائن. يتم الوصول إلى class الثابتة مباشرة من خلال الأساليب الثابتة وعادةً لا تتطلب مثيلًا للكائن.

غالبًا ما تُستخدم classes الثابتة لطرق مساعدة بسيطة. يتم استخدام Singleton عند وجود مثيل كائن واحد. يجب استخدام كلا الطريقتين بعناية لأنهما يمكن أن يزيدا من الاقتران إذا تم الإفراط في استخدامه.

Singleton في الأطر الحديثة

غالبًا ما تستخدم أطر العمل الحديثة مثل Laravel وSymfony حاويات الخدمة لإدارة عمر الكائنات. يمكن لحاوية الخدمة تسجيل فئة كخدمة مشتركة، مما يعني إعادة استخدام نفس المثيل عند الطلب.

يوفر هذا سلوكًا يشبه سلوك Singleton دون إجبار الclass نفسه على تنفيذ Singleton Pattern.

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

سينجلتون في لارافيل

في Laravel، يمكن للمطورين ربط فئة كصنف مفرد داخل مزود الخدمة باستخدام حاوية الخدمة:

$this->app->singleton(LoggerInterface::class, FileLogger::class);

هذا يخبر Laravel بإنشاء نسخة مشتركة واحدة فقط لهذا الربط. يمكن أن تعتمد classes الأخرى على LoggerInterface، وسيقوم Laravel بإدخال نفس مثيل FileLogger.

وهذا يختلف عن كتابة فئة Singleton اليدوية. لا يحتاج الclass نفسه إلى constructor خاص أو طريقة getInstance. تتحكم الحاوية في عمر الكائن.

متى تستخدم سينجلتون

استخدم Singleton Pattern فقط عندما يكون هناك سبب قوي لضمان وجود مثيل واحد بالضبط للفئة وعندما لا يضر الوصول الشامل بالتصميم.

قد يكون Singleton مقبولاً عندما:

  • يمثل الكائن موردًا مشتركًا على مستوى التطبيق حقًا.

  • يجب أن يكون هناك مثيل واحد فقط بشكل منطقي.

  • لا يحتاج الكائن إلى الاستبدال كثيرًا في الاختبارات.

  • التطبيق صغير ولا يتطلب إدارة التبعية المعقدة.

  • حاوية خدمة إطار العمل غير متوفرة.

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

متى يجب تجنب المفردة

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

يجب عادةً تجنب Singleton عندما:

  • يعتمد الclass على الخدمات الخارجية.

  • يجب استبدال الclass بصنف وهمي أو مزيف أثناء الاختبار.

  • يستخدم التطبيق Dependency Injection أو حاوية الخدمة.

  • يخلق Singleton حالة عالمية مخفية.

  • قد تكون هناك حاجة إلى تكوينات متعددة أو مثيلات متعددة لاحقًا.

  • يتم استخدام النمط فقط للراحة.

عادةً ما يكون استخدام Singleton لسهولة الوصول إليه علامة على ضعف التصميم.

أفضل الممارسات لـ Singleton Pattern

إذا تم استخدام Singleton، فيجب تنفيذه بعناية وفي أماكن محدودة فقط.

تتضمن أفضل الممارسات المفيدة ما يلي:

  • استخدم Singleton فقط عندما يكون هناك مثيل واحد مطلوب منطقيًا.

  • اجعل دروس Singleton بسيطة ومركزة.

  • منع البناء المباشر باستخدام constructor خاص.

  • منع الاستنساخ إذا كان المثيلات المكررة غير مسموح بها.

  • منع إلغاء التسلسل عند الحاجة.

  • تجنب تخزين الكثير من الحالة العالمية القابلة للتغيير.

  • تفضل Dependency Injection في التطبيقات الكبيرة.

  • استخدم حاويات خدمة إطار العمل عندما تكون متاحة.

تساعد هذه الممارسات في تقليل مخاطر Singleton والحفاظ على نظافة تصميم التطبيق.

الأخطاء الشائعة مع سينجلتون

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

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

الخطأ الثالث هو تخزين البيانات الخاصة بالطلب داخل Singleton. يجب ألا تحتفظ المثيلات المشتركة عن طريق الخطأ بالبيانات التي تنتمي إلى طلب مستخدم واحد فقط أو عملية واحدة.

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

سلامة المفردة والخيط

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

في تطبيقات الويب PHP النموذجية، عادةً ما يتم تشغيل كل طلب بشكل منclass، لذا فإن سلامة سلسلة الرسائل تكون أقل أهمية مما هي عليه في لغات مثل Java أو C#. ومع ذلك، يجب على المطورين الذين يعملون مع عمليات PHP طويلة الأمد أو العمال أو البيئات غير المتزامنة أن يفهموا عمر الكائن والحالة المشتركة بعناية.

Singleton في المشاريع الصغيرة مقابل المشاريع الكبيرة

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

في المشاريع الكبيرة، غالبًا ما يكون Singleton أقل ملاءمة لأنه يمكن أن يخفي التبعيات ويجعل الاختبار أكثر صعوبة. عادةً ما تستفيد التطبيقات الكبيرة بشكل أكبر من Dependency Injection، interfaces، وحاويات الخدمة، وعمر الكائن الواضح.

حجم وتعقيد المشروع يجب أن يؤثر على القرار. قد يؤدي النمط المقبول في برنامج نصي صغير إلى إنشاء مشكلات في تطبيق مؤسسة كبيرة.

قائمة مرجعية عملية قبل استخدام Singleton

قبل استخدام Singleton Pattern، يجب على المطورين طرح هذه الأسئلة:

  • هل يحتاج هذا الclass حقًا إلى مثيل واحد فقط؟

  • هل الوصول العالمي آمن لهذا الكائن؟

  • هل هذا سيجعل الاختبار أصعب؟

  • هل يمكن أن يحل Dependency Injection المشكلة بشكل أفضل؟

  • هل سيخزن هذا الكائن الحالة المشتركة القابلة للتغيير؟

  • هل يمكن أن يحتاج التطبيق إلى مثيلات متعددة لاحقًا؟

  • هل يتم استخدام هذا النمط لأسباب التصميم أم فقط للراحة؟

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

خاتمة

Singleton Pattern هو نمط تصميم إبداعي يضمن أن class لديها مثيل واحد فقط وتوفر نقطة وصول عامة إلى هذا المثيل. يمكن أن يكون مفيدًا للموارد المشتركة مثل مديري التكوين، والمسجلين، ومديري ذاكرة التخزين المؤقت في مواقف معينة.

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

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