الفرق بين المراجعتين ل"Design Patterns/singleton"

من موسوعة حسوب
اذهب إلى التنقل اذهب إلى البحث
(2.0 محتوى)
(2.1 إضافة محتوى)
سطر 12: سطر 12:
  
 
== الحل ==
 
== الحل ==
 +
تشترك كل استخدامات نمط المفردة في هاتين الخطوتين:
 +
 +
جعل المنشئ الافتراضي خاصًا لمنع الكائنات الأخرى من استخدام معامل new مع فئة المفردة.
 +
 +
إنشاء أسلوب إنشاء ساكن (static creation method) يتصرف مثل المنشئ (Constructor)، ويستدعي هذا الأسلوبُ المنشئَ الخاص لإنشاء كائن ما ثم يحفظه بعد ذلك في حقل ساكن (static field)، وكل الاستدعاءات التالية لهذا الأسلوب تعيد الكائن المحفوظ مسبقًا (Cached).
 +
 +
وإن كانت شيفرتك لها صلاحية وصول لفئة المفردة فإنها تستطيع استدعاء أسلوب المفردة الساكن، وهكذا يعاد نفس الكائن دائمًا في كل مرة يُستدعى هذا الأسلوب.
 +
 +
== مثال من الواقع ==
 +
تعد الحكومات مثالًا ممتازًا على نمط المفردة، فلا يمكن للدولة أن يكون لها أكثر من حكومة رسمية، ويمثل اسم تلك الحكومة "حكومة كذا" نقطة وصول عامة تعرِّف مجموعة الأشخاص المسؤولين فيها بغض النظر عن هوياتهم الفردية.
 +
 +
== البُنية ==
 +
# تصرح فئة Singleton عن الأسلوب الساكن <code>getInstance</code> الذي يعيد نفس النسخة من فئته. وينبغي أن يكون منشئ المفردة (Singleton's Constructor) مخفيًا من شيفرة العميل، وكذلك يجب أن يكون أسلوب <code>getInstance</code> هو الطريقة الوحيدة للحصول على كائن مفردة.
 +
 +
== مثال توضيحي ==
 +
تتصرف فئة وصلة قاعدة البيانات في هذا المثال كمفردة، وليس لتلك الفئة منشئ عام، لذا فإن الطريقة الوحيدة للخصول على كائنها هو استدعاء أسلوب <code>getInstance</code> ، إذ يحفظ أول كائن تم إنشاؤه ثم يعيده في كل الاستدعاءات التالية.<syntaxhighlight lang="java">
 +
// الذي يسمح للعملاء بالوصول getInstance تعرِّف فئة قاعدة البيانات أسلوب
 +
// إلى نفس النسخة من وصلة قاعدة البيانات في البرنامج كله.
 +
class Database is
 +
    private field instance: Database
 +
 +
    // يجب أن يكون منشئ المفردة خاصًا دائمًا لمنع استدعاءات الإنشاء المباشرة
 +
    // new باستخدام معامل
 +
    private constructor Database() is
 +
        // شيفرة بدء مثل الوصلة الفعلية لخادم قاعدة بيانات
 +
 +
    // الأسلوب الساكن الذي يتحكم في الوصول إلى نسخة المفردة.
 +
    static method getInstance() is
 +
        if (this.instance == null) then
 +
            acquireThreadLock() and then
 +
                // (thread) تأكد أن النسخة لم تُبدأ بعد بواسطة أي خيط
 +
                // آخر إن كان هذا الخيط بانتظار تحرير القفل.
 +
                if (this.instance == null) then
 +
                    this.instance = new Database()
 +
        return this.instance
 +
 +
    // أخيرًا، يجب أن تعرِّف أي مفردة بعض المنطق التجاري الذي يمكن
 +
    // تنفيذه في هذه النسخة.
 +
    public method query(sql) is
 +
        // فمثلًا، ستمر كل استعلامات تطبيق ما على هذه الأسلوب، ومن ثم
 +
        //  هنا(caching) أو حفظ مؤقت (throttling) يمكنك وضع منطق خنق.
 +
 +
class Application is
 +
    method main() is
 +
        Database foo = Database.getInstance()
 +
        foo.query("SELECT ...")
 +
        // ...
 +
        Database bar = Database.getInstance()
 +
        bar.query("SELECT ...")
 +
        // foo على نفس الكائن الذي يحتويه متغير bar سيحتوي متغير.
 +
</syntaxhighlight>
 +
 +
== قابلية الاستخدام ==
 +
استخدم نمط المفردة في حالة وجوب توفر نسخة واحدة فقط من فئة ما لكل العملاء، ككائن قاعدة بيانات وحيد مشترك بين أجزاء مختلفة في البرنامج.
 +
 +
يعطل نمط المفردة كل أساليب إنشاء الكائنات من فئة ما باستثناء أسلوب الإنشاء الخاص، وهذا الأسلوب إما ينشئ كائنًا جديدًا أو يعيد كائنًا موجودًا إن تم إنشاؤه من قبل.
 +
 +
استخدم نمط المفردة حين تريد تحكمًا أكثر في متغيراتك العامة.
 +
 +
على عكس المتغيرات العامة (Global Variables)، فإن نمط المفردة يضمن وجود نسخة واحدة فقط من فئة ما، ولا شيء يمكنه استبدال تلك النسخة المحفوظة باستثناء فئة المفردة نفسها. لاحظ أنك تستطيع تعديل هذا التقييد في أي وقت لتسمح بإنشاء أي عدد من نسخ المفردة، والجزء الذي تحتاج إلى تعديله في الشيفرة حينها هو المتن الرئيسي (body) في أسلوب <code>()getInstance</code>.
 +
 +
== كيفية الاستخدام ==
 +
# أضف حقلًا ساكنًا خاصًا إلى الفئة لتخزين نسخة المفردة.
 +
# صرِّح عن أسلوب إنشاءٍ ساكنٍ عام للحصول على نسخة المفردة.
 +
# استخدم البدء الكسول (lazy initialization) داخل الأسلوب الساكن، إذ يجب أن تنشئ كائنًا جديدًا في الاستدعاء الأول لها وتضعه في الحقل الساكن. كذلك يجب أن يعيد الأسلوب تلك النسخة دومًا في كل الاستدعاءات التالية.
 +
# اجعل منشئ الفئة خاصًا (private)، وسيظل أسلوب الفئة الساكن قادرًا على استدعاء المنشئ لكن لا يستطيع استدعاء الكائنات الأخرى.
 +
# اذهب لشيفرة العميل واستبدل كل الاستدعاءات المباشرة لمنشئ المفردة باستدعاءات إلى أسلوب الإنشاء الساكن الخاص بها.
 +
 +
== المزايا والعيوب ==
 +
 +
=== المزايا ===
 +
* تستطيع ضمان وجود نسخة واحدة فقط من فئة ما.
 +
* تحصل على نقطة وصول عامة إلى تلك النسخة.
 +
* لا يُبدأ كائن المفردة إلا حين يستدعى لأول مرة.
 +
 +
=== العيوب ===
 +
* يخرق نمط المفردة مبدأ المسؤولية الواحدة، إذ يحل النمط مشكلتين في نفس الوقت.
 +
* يغطي على عيوب التصميم، كأن يكون لدى مكونات البرنامج بيانات أكثر من المطلوب عن بعضها البعض.
 +
* يتطلب معالجة خاصة في بيئة متعددة الخيوط (multi-threaded) كي لا تنشئ الخيوط المتعددة كائن مفردة أكثر من مرة.
 +
* قد يكون من الصعب اختبار شيفرة العميل جزءًا جزءًا - أو على مستوى الوحدة (unit test)- لنمط المفردة بسبب أن العديد من إطارات العمل للاختبارات تعتمد على الاكتساب (inheritance) عند إنتاج كائنات مقلدة (mock objects). وبما أن منشئ فئة المفردةِ خاصًا، ومن المستحيل تخطي الأساليب الساكنة في أغلب اللغات، فإنك في حاجة إلى التفكير في طريقة مبدعة لتقليد المفردة، وإلا فلا تكتب تلك الاختبارات أو تستخدم نمط المفردة على الإطلاق.
 +
 +
== العلاقات مع الأنماط الأخرى ==
 +
* فئة نمط الواجهة قد تتحول إلى مفردة بما أن كائن الواجهة الواحد يكون كافيًا في أغلب الأحوال.
 +
* قد يتشابه نمط وزن الذبابة (Flyweight) مع نمط المفردة إن استطعت تقليل كل الحالات المشتركة للكائنات إلى كائن واحد من نمط وزن الذبابة، لكن هناك اختلافان رئيسيان بين هذين النمطين:
 +
# يجب أن تكون هناك نسخة واحدة من المفردة، بينما يمكن أن يكون لفئة وزن الذبابة أكثر من نسخة  بحالات فعلية مختلفة.
 +
# يمكن لكائن المفردة أن يكون متقلبًا، أما كائنات نمط وزن الذبابة فتكون ثابتة وغير قابلة للتغيير.
 +
* يمكن استخدام أنماط المصنع المجرد والباني والنموذج الأولي كمفردات.
 +
 +
== الاستخدام في لغة جافا ==
 +
'''المستوى:''' ★ ☆ ☆
 +
 +
'''الانتشار:'''  ★ ★ ☆
 +
 +
'''أمثلة الاستخدام:''' ينظر كثير من المطورين إلى نمط المفردة على أنه نمط عكسي (antipattern) لهذا يتراجع استخدامه مع الوقت في شيفرة جافا، لكن رغم ذلك فهناك الكثير من الأمثلة لنمط المفردة في مكتبات جافا:
 +
* [http://docs.oracle.com/javase/8/docs/api/java/lang/Runtime.html#getRuntime-- java.lang.Runtime#getRuntime()]
 +
* [http://docs.oracle.com/javase/8/docs/api/java/awt/Desktop.html#getDesktop-- java.awt.Desktop#getDesktop()]
 +
* [http://docs.oracle.com/javase/8/docs/api/java/lang/System.html#getSecurityManager-- java.lang.System#getSecurityManager()]
 +
يمكن ملاحظة نمط المفردة من خلال أسلوب الإنشاء الساكن الذي يعيد نفس الكائن المحفوظ مسبقًا.
 +
 +
=== مثال: المفردة البسيطة (ذات الخيط الواحد) ===
 +
من السهل استخدام مفردة غامضة، فما عليك سوى إخفا المنشئ واستخدام أسلوب إنشاء ساكن.
 +
 +
==== Singleton.java: المفردة ====
 +
<syntaxhighlight lang="java">
 +
package refactoring_guru.singleton.example.non_thread_safe;
 +
 +
public final class Singleton {
 +
    private static Singleton instance;
 +
    public String value;
 +
 +
    private Singleton(String value) {
 +
        // تحاكي الشيفرة التالية عملية البدء البطيء.
 +
        try {
 +
            Thread.sleep(1000);
 +
        } catch (InterruptedException ex) {
 +
            ex.printStackTrace();
 +
        }
 +
        this.value = value;
 +
    }
 +
 +
    public static Singleton getInstance(String value) {
 +
        if (instance == null) {
 +
            instance = new Singleton(value);
 +
        }
 +
        return instance;
 +
    }
 +
}
 +
 +
</syntaxhighlight>
 +
 +
==== DemoSingleThread.java: شيفرة العميل ====
 +
<syntaxhighlight lang="java">
 +
package refactoring_guru.singleton.example.non_thread_safe;
 +
 +
public class DemoSingleThread {
 +
    public static void main(String[] args) {
 +
        System.out.println("إذا رأيت نفس القيمة فذلك يعني إعادة استخدام للمفردة" + "\n" +
 +
                "إن رأيت قيمًا مختلفة فيعني ذلك إنشاء مفردتين." + "\n\n" +
 +
                "RESULT:" + "\n");
 +
        Singleton singleton = Singleton.getInstance("FOO");
 +
        Singleton anotherSingleton = Singleton.getInstance("BAR");
 +
        System.out.println(singleton.value);
 +
        System.out.println(anotherSingleton.value);
 +
    }
 +
}
 +
</syntaxhighlight>
 +
 +
==== OutputDemoSingleThread.txt: نتائج التنفيذ ====
 +
<syntaxhighlight>
 +
إذا رأيت نفس القيمة فذلك يعني إعادة استخدام للمفردة
 +
إن رأيت قيمًا مختلفة فذلك يعني إنشاء مفردتين
 +
 +
RESULT:
 +
 +
FOO
 +
FOO
 +
</syntaxhighlight>
 +
 +
=== مثال: المفردة البسيطة (متعددة الخيوط) ===
 +
تتصرف نفس الفئة بشكل خاطئ في بيئة متعددة الخيوط (multithreaded)، ذلك أن الخيوط المتعددة يمكنها استدعاء أسلوب إنشائي في نفس الوقت والحصول على نسخ متعددة من فئة المفردة.
 +
 +
==== Singleton.java: المفردة ====
 +
<syntaxhighlight lang="java">
 +
package refactoring_guru.singleton.example.non_thread_safe;
 +
 +
public final class Singleton {
 +
    private static Singleton instance;
 +
    public String value;
 +
 +
    private Singleton(String value) {
 +
        // تحاكي الشيفرة التالية عملية البدء البطيء.
 +
        try {
 +
            Thread.sleep(1000);
 +
        } catch (InterruptedException ex) {
 +
            ex.printStackTrace();
 +
        }
 +
        this.value = value;
 +
    }
 +
 +
    public static Singleton getInstance(String value) {
 +
        if (instance == null) {
 +
            instance = new Singleton(value);
 +
        }
 +
        return instance;
 +
    }
 +
}
 +
</syntaxhighlight>
 +
 +
==== DemoMultiThread.java: شيفرة العميل ====
 +
<syntaxhighlight lang="java">
 +
package refactoring_guru.singleton.example.non_thread_safe;
 +
 +
public class DemoMultiThread {
 +
    public static void main(String[] args) {
 +
        System.out.println("إذا رأيت نفس القيمة فذلك يعني إعادة استخدام نفس المفردة" + "\n" +
 +
                "إذا رأيت قيمًا مختلفة فذلك يعني إنشاء مفردتين" + "\n\n" +
 +
                "RESULT:" + "\n");
 +
        Thread threadFoo = new Thread(new ThreadFoo());
 +
        Thread threadBar = new Thread(new ThreadBar());
 +
        threadFoo.start();
 +
        threadBar.start();
 +
    }
 +
 +
    static class ThreadFoo implements Runnable {
 +
        @Override
 +
        public void run() {
 +
            Singleton singleton = Singleton.getInstance("FOO");
 +
            System.out.println(singleton.value);
 +
        }
 +
    }
 +
 +
    static class ThreadBar implements Runnable {
 +
        @Override
 +
        public void run() {
 +
            Singleton singleton = Singleton.getInstance("BAR");
 +
            System.out.println(singleton.value);
 +
        }
 +
    }
 +
}
 +
</syntaxhighlight>
 +
 +
==== OutputDemoMultiThread.txt: نتائج التنفيذ ====
 +
<syntaxhighlight lang="java">
 +
إذا رأيت نفس القيمة فذلك يعني إعادة استخدام نفس المفردة.
 +
إذا رأيت قيمًا مختلفة فذلك يعني إنشاء مفردتين.
 +
 +
RESULT:
 +
 +
FOO
 +
BAR
 +
</syntaxhighlight>
 +
 +
=== مثال: المفردة الصحيحة (آمنة على الخيوط، تحميل كسول) ===
 +
من أجل إصلاح المشكلة، يجب أن تزامن الخيوط في أول إنشاء لكائن المفردة.
 +
 +
==== Singleton.java: المفردة ====
 +
<syntaxhighlight lang="java">
 +
package refactoring_guru.singleton.example.thread_safe;
 +
 +
public final class Singleton {
 +
    private static volatile Singleton instance;
 +
    public String value;
 +
 +
    private Singleton(String value) {
 +
        this.value = value;
 +
    }
 +
 +
    public static Singleton getInstance(String value) {
 +
        if (instance == null) {
 +
            synchronized (Singleton.class) {
 +
                if (instance == null) {
 +
                    instance = new Singleton(value);
 +
                }
 +
            }
 +
        }
 +
        return instance;
 +
    }
 +
}
 +
</syntaxhighlight>
 +
 +
==== DemoMultiThread.java: شيفرة العميل ====
 +
<syntaxhighlight lang="java">
 +
package refactoring_guru.singleton.example.thread_safe;
 +
 +
public class DemoMultiThread {
 +
    public static void main(String[] args) {
 +
        System.out.println("إذا رأيت نفس القيمة فذلك يعني إعادة استخدام نفس المفردة" + "\n" +
 +
                "إذا رأيت قيمًا مختلفة فذلك يعني إنشاء مفردتين" + "\n\n" +
 +
                "RESULT:" + "\n");
 +
        Thread threadFoo = new Thread(new ThreadFoo());
 +
        Thread threadBar = new Thread(new ThreadBar());
 +
        threadFoo.start();
 +
        threadBar.start();
 +
    }
 +
 +
    static class ThreadFoo implements Runnable {
 +
        @Override
 +
        public void run() {
 +
            Singleton singleton = Singleton.getInstance("FOO");
 +
            System.out.println(singleton.value);
 +
        }
 +
    }
 +
 +
    static class ThreadBar implements Runnable {
 +
        @Override
 +
        public void run() {
 +
            Singleton singleton = Singleton.getInstance("BAR");
 +
            System.out.println(singleton.value);
 +
        }
 +
    }
 +
}
 +
</syntaxhighlight>
 +
 +
==== OutputDemoMultiThread.txt: نتائج الاستخدام ====
 +
<syntaxhighlight>
 +
إذا رأيت نفس القيمة فذلك يعني إعادة استخدام نفس المفردة
 +
إذا رأيت قيمًا مختلفة فذلك يعني إنشاء مفردتين
 +
 +
RESULT:
 +
 +
BAR
 +
BAR
 +
</syntaxhighlight>
 +
 +
== الاستخدام في لغة #C ==
 +
'''المستوى:''' ★ ☆ ☆
 +
 +
'''الانتشار:'''  ★ ★ ☆
 +
 +
'''أمثلة الاستخدام:''' ينظر كثير من المطورين إلى نمط المفردة على أنه نمط عكسي (antipattern) لهذا يتراجع استخدامه مع الوقت في شيفرة #C.
 +
 +
يمكن ملاحظة نمط المفردة من خلال أسلوب الإنشاء الساكن الذي يعيد نفس الكائن المحفوظ مسبقًا.
 +
 +
=== مثال: بنية النمط ===
 +
يوضح هذا المثال بنية نمط '''المفردة'''، ويركز على إجابة الأسئلة التالية:
 +
* ما الفئات التي يتكون منها؟
 +
* ما الأدوار التي تلعبها هذه الفئات؟
 +
* كيف ترتبط عناصر النمط ببعضها؟
 +
 Program.cs: مثال هيكلي<syntaxhighlight lang="c#">
 +
using System;
 +
using System.Collections.Generic;
 +
using System.Linq;
 +
using System.Text;
 +
using System.Threading.Tasks;
 +
 +
namespace Singleton
 +
{
 +
    class Program
 +
    {
 +
        static void Main(string[] args)
 +
        {
 +
            Client client = new Client();
 +
            client.ClientCode();
 +
        }
 +
    }
 +
 +
    class Client
 +
    {
 +
        public void ClientCode()
 +
        {
 +
            Singleton s1 = Singleton.getInstance();
 +
            Singleton s2 = Singleton.getInstance();
 +
 +
            if(s1 == s2)
 +
            {
 +
                Console.WriteLine("نمط المفردة يعمل!. كلاالمتغيران يحتويان نفس النسخة.");
 +
            }
 +
            else
 +
            {
 +
                Console.WriteLine("فشل نمط المفردة!. احتوى المتغيران على نسخ مختلفة.");
 +
            }
 +
        }
 +
    }
 +
 +
    class Singleton
 +
    {
 +
        private static Singleton instance;
 +
 +
        private static object obj = new object();
 +
 +
        private Singleton()
 +
        { }
 +
 +
        public static Singleton getInstance()
 +
        {
 +
            lock(obj)
 +
            {
 +
                if (instance == null)
 +
                    instance = new Singleton();
 +
            }
 +
 +
            return instance;
 +
        }
 +
    }
 +
}
 +
</syntaxhighlight>
 +
 +
==== Output.txt: المخرجات ====
 +
<syntaxhighlight>
 +
نمط المفردة يعمل!. كلا المتغيران يحتويان نفس النسخة.
 +
</syntaxhighlight>
 +
 +
== الاستخدام في لغة PHP ==
 +
'''المستوى:''' ★ ☆ ☆
 +
 +
'''الانتشار:'''  ★ ★ ☆
 +
 +
'''أمثلة الاستخدام:''' ينظر كثير من المطورين إلى نمط المفردة على أنه نمط عكسي (antipattern) لهذا يتراجع استخدامه مع الوقت في شيفرة PHP.
 +
 +
=== مثال: بنية النمط ===
 +
يوضح هذا المثال بنية نمط '''المفردة'''، ويركز على إجابة الأسئلة التالية:
 +
* ما الفئات التي يتكون منها؟
 +
* ما الأدوار التي تلعبها هذه الفئات؟
 +
* كيف ترتبط عناصر النمط ببعضها؟
 +
سيكون من السهل بعد تعلم بنية النمط أن تستوعب المثال التالي بناءً على مثال واقعي لاستخدام لغة PHP.
 +
 +
==== SingletonStructural.php: مثال هيكلي ====
 +
<syntaxhighlight lang="php">
 +
<?php
 +
 +
namespace RefactoringGuru\Singleton\Structural;
 +
 +
/**
 +
* الذي يسمح للعملاء بالوصول إلى نسخة getInstance تعرِّف فئة المفردةأسلوب
 +
* فريدة من المفردة.
 +
*/
 +
class Singleton
 +
{
 +
    private static $instances = [];
 +
 +
    /**
 +
    * يجب أن يكون منشئ المفردة خاصًا دومًا لمنع استدعاءات الإنشاء المباشرة
 +
    * new باستخدام معامل.
 +
    */
 +
    protected function __construct() { }
 +
 +
    /**
 +
    * يجب ألا تكون المفردات قابلة للاستنساخ.
 +
    */
 +
    protected function __clone() { }
 +
 +
    /**
 +
    * (strings) يجب ألا تكون المفردات قابلة للاسترجاع من النصوص.
 +
    */
 +
    public function __wakeup()
 +
    {
 +
        throw new \Exception("Cannot unserialize a singleton.");
 +
    }
 +
 +
    /**
 +
    * الأسلوب الساكن الذي يتحكم في الوصول إلى نسخة المفردة.
 +
    *
 +
    * يسمح لك هذا الاستخدام بتفريع فئة المفردة مع إبقاء نسخة واحدة فقط
 +
    * من كل فئة فرعية.
 +
    */
 +
    public static function getInstance(): Singleton
 +
    {
 +
        $cls = static::class;
 +
        if (! isset(static::$instances[$cls])) {
 +
            static::$instances[$cls] = new static;
 +
        }
 +
 +
        return static::$instances[$cls];
 +
    }
 +
 +
    /**
 +
    * أخيرًا، يجب أن تعرِّف كلُّ مفردة بعض المنطق التجاري الذي يمكن تنفيذه
 +
    * في نسختها.
 +
    */
 +
    public function someBusinessLogic()
 +
    {
 +
        // ...
 +
    }
 +
}
 +
 +
/**
 +
* شيفرة العميل.
 +
*/
 +
function clientCode()
 +
{
 +
    $s1 = Singleton::getInstance();
 +
    $s2 = Singleton::getInstance();
 +
    if ($s1 === $s2) {
 +
        echo "نمط المفردة يعمل. كلا المتغيران يحتويان نفس النسخة.";
 +
    } else {
 +
        echo "فشل نمط المفردة. المتغيران يحتويان نسخًا مختلفة";
 +
    }
 +
}
 +
 +
clientCode();
 +
</syntaxhighlight>
 +
 +
==== Output.txt: المخرجات ====
 +
<syntaxhighlight>
 +
نمط المفردة يعمل. كلا المتغيران يحتويان نفس النسخة.
 +
</syntaxhighlight>
 +
 +
=== مثال: حالة حقيقية ===
 +
يشتهر نمط المفردة أنه يحد من إعادة استخدام الشيفرة وبتعقيد اختبار الوحدات، لكنه لا يزال مفيدًا في بعض الحالات، خاصة حين تريد التحكم في بعض المصادر المشتركة، كحالة كائن تسجيل دخول عام يجب أن يتحكم في الوصول إلى ملف سجل ما، أو سعة تخزين إعدادت وقت تشغيل مشتركة (Shared runtime configuration storage).
 +
 +
==== SingletonRealWorld.php: مثال واقعي ====
 +
<syntaxhighlight lang="php">
 +
<?php
 +
 +
namespace RefactoringGuru\Singleton\RealWorld;
 +
 +
/**
 +
* نمط تصميم المفردة
 +
*
 +
* الهدف: ضمان أن فئة ما لها نسخة واحدة، وتوفير نقطة وصول عامةلها.
 +
*
 +
* يشتهر نمط المفردة أنه يحد من إعادة استخدام الشيفرة وبتعقيد اختبار
 +
* الوحدات، لكنه لا يزال مفيدًا في بعض الحالات، خاصة حين تريد التحكم في بعض
 +
* المصادر المشتركة، كحالة كائن تسجيل دخول عام يجب أن يتحكم في الوصول إلى
 +
* ملف سجل ما، أو سعة تخزين إعدادت وقت تشغيل مشتركة.
 +
*/
 +
 +
/**
 +
* إن احتجت إلى دعم عدة أنواع من المفردات في تطبيقك فيمكنك تعريف المزايا
 +
* الأساسية للمفردة في فئة أساسية في حين تنقل المنطق التجاري الفعلي مثل
 +
* إلى فئات فرعية logging التسجيل
 +
*/
 +
class Singleton
 +
{
 +
    /**
 +
    * توجد النسخة الفعلية للمفردة داخل حقل ساكن طول الوقت تقريبًا، ويكون
 +
    * الحقل الساكن في تلك الحالة عبارة عن مصفوفة تسجِّل فيها كل فئة فرعية من
 +
    * المفردة نسختها الخاصة بها.
 +
    */
 +
    private static $instances = [];
 +
 +
    /**
 +
    * يجب ألا يكون منشئ المفردة عامًا، لكنه في نفس الوقت لا يمكن أن يكون خاصًا
 +
    * كذلك إن كنا نرغب بالسماح بإنشاء فئات فرعية.
 +
    */
 +
    protected function __construct() { }
 +
 +
    /**
 +
    * للمفردات (unserialization) لا يسمح بالاستنساخ أو إلغاء التسلسل.
 +
    */
 +
    protected function __clone() { }
 +
 +
    public function __wakeup()
 +
    {
 +
        throw new \Exception("Cannot unserialize singleton");
 +
    }
 +
 +
    /**
 +
    * الأسلوب الذي تستخدمه للحصول على نسخة المفردة.
 +
    */
 +
    public static function getInstance()
 +
    {
 +
        $subclass = static::class;
 +
        if (!isset(self::$instances[$subclass])) {
 +
            // static لاحظ أننا هنا نستخدم الكلمة المفتاحية
 +
            // بدلًا من الاسم الفعلي للفئة، ويُقصد بهذه الكلمة في هذا السياق
 +
            // اسمَ الفئة الحالية، وذلك التفصيل مهم لأنه عند استدعاء الأسلوب
 +
            // على الفئة الفرعية فإننا نريد إنشاء نسخة من تلك الفئة الفرعية
 +
            // هنا.
 +
         
 +
            self::$instances[$subclass] = new static;
 +
        }
 +
        return self::$instances[$subclass];
 +
    }
 +
}
 +
 +
/**
 +
* فئة تسجيل الدخول هي أكثر استخدام معروف لنمط المفردة، ففي أغلب الحالات
 +
* تحتاج كائن تسجيل دخول وحيد يكتب في ملف سجل واحد، وهذا هو التحكم في مصدر
 +
* مشترك. كما تحتاج أيضًا إلى طريقة مناسبة للوصول إلى تلك النسخة من أي سياق
 +
* في تطبيقك، وتلك هي نقطة الوصول العامة.
 +
*/
 +
class Logger extends Singleton
 +
{
 +
    /**
 +
    * من ملف السجل file pointer resource مصدر مؤشر ملف
 +
    */
 +
    private $fileHandle;
 +
 +
    /**
 +
    * بما أن منشئ المفردة قد استُدعي مرة واحدة فقط، فإن مصدر ملف واحد فقط
 +
    * قد تم فتحه.
 +
    *
 +
    * console stream لاحظ أنه وبداعي التبسيط، سنفتح سيل المنصة
 +
    * بدلًا من الملف الفعلي هنا.
 +
    */
 +
    protected function __construct()
 +
    {
 +
        $this->fileHandle = fopen('php://stdout', 'w');
 +
    }
 +
 +
    /**
 +
    * الملف المفتوح resource اكتب مُدخل سجل لمصدر
 +
    */
 +
    public function writeLog(string $message): void
 +
    {
 +
        $date = date('Y-m-d');
 +
        fwrite($this->fileHandle, "$date: $message\n");
 +
    }
 +
 +
    /**
 +
    * مجرد اختصار بسيط لتقليل الشيفرة المطلوبة لتسجيل الرسائل من شيفرة
 +
    * العميل
 +
    */
 +
    public static function log(string $message): void
 +
    {
 +
        $logger = static::getInstance();
 +
        $logger->writeLog($message);
 +
    }
 +
}
 +
 +
/**
 +
* يشيع أيضًا تطبيق نمط المفردة على إعدادات التخزين، وغالبًا تحتاج إلى الدخول
 +
* لإعدادات التطبيق من أماكن كثيرة من البرنامج، ونمط المفردة ييسر هذه
 +
* العملية.
 +
*/
 +
class Config extends Singleton
 +
{
 +
    private $hashmap = [];
 +
 +
    public function getValue(string $key): string
 +
    {
 +
        return $this->hashmap[$key];
 +
    }
 +
 +
    public function setValue(string $key, string $value): void
 +
    {
 +
        $this->hashmap[$key] = $value;
 +
    }
 +
}
 +
 +
/**
 +
* شيفرة العميل.
 +
*/
 +
Logger::log("Started!");
 +
 +
// logger singleton قارن القيَم من.
 +
$l1 = Logger::getInstance();
 +
$l2 = Logger::getInstance();
 +
if ($l1 === $l2) {
 +
    Logger::log("Logger has a single instance.");
 +
} else {
 +
    Logger::log("Loggers are different.");
 +
}
 +
 +
// على البيانات config singleton انظر كيف يحافظ...
 +
$config1 = Config::getInstance();
 +
$login = "test_login";
 +
$password = "test_password";
 +
$config1->setValue("login", $login);
 +
$config1->setValue("password", $password);
 +
// ...ومن ثم يستعيدها.
 +
$config2 = Config::getInstance();
 +
if ($login == $config2->getValue("login") &&
 +
    $password == $config2->getValue("password")
 +
) {
 +
    Logger::log("Config singleton also works fine.");
 +
}
 +
 +
Logger::log("Finished!");
 +
</syntaxhighlight>
 +
 +
==== Output.txt: المخرجات ====
 +
<syntaxhighlight>
 +
2018-06-04: Started!
 +
2018-06-04: Logger has a single instance.
 +
2018-06-04: Config singleton also works fine.
 +
2018-06-04: Finished!
 +
</syntaxhighlight>
 +
 +
== انظر أيضًا ==
 +
 +
== مصادر ==
 +
* [https://refactoring.guru/design-patterns/singleton توثيق نمط المفردة في موقع refactoring.guru.]

مراجعة 02:09، 17 يناير 2019

نمط المفردة (Singleton) هو نمط تصميم إنشائي يضمن وجود نسخة واحدة فقط من فئة ما في نفس الوقت الذي يوفر فيه نقطة وصول عامة لهذه النسخة.

المشكلة

يحل نمط المفردة مشكلتين في نفس الوقت مخالفًا بذلك مبدأ المسؤولية الواحدة:

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

ثانيًا: توفير نقطة وصول عامة إلى تلك النسخة. ربما تكون المتغيرات العامة التي كنت تستخدمها لتخزين بعض الكائنات الضرورية مفيدة، لكنها غير آمنة بما أنك أي شيفرة قد تغي محتويات تلك المتغيرات وتتسبب في انهيار التطبيق. ويسمح لك نمط المفردة هنا -تمامًا كالمتغير العام (Global Variable)- بالوصول إلى بعض الكائنات من أي مكان في البرنامج، لكنه يحمي تلك النسخة من تغيير محتواها من قِبل الشيفرات الأخرى. وهناك جانب آخر من المشكلة: أنت لا تريد للشيفرة التي تحل مشكلة 1# مثلًا من الانتشار في برنامجك كله، فمن الأفضل وضعها داخل فئة واحدة، خاصة إن كانت بقية شيفرتك تعتمد عليها من قبل.

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

الحل

تشترك كل استخدامات نمط المفردة في هاتين الخطوتين:

جعل المنشئ الافتراضي خاصًا لمنع الكائنات الأخرى من استخدام معامل new مع فئة المفردة.

إنشاء أسلوب إنشاء ساكن (static creation method) يتصرف مثل المنشئ (Constructor)، ويستدعي هذا الأسلوبُ المنشئَ الخاص لإنشاء كائن ما ثم يحفظه بعد ذلك في حقل ساكن (static field)، وكل الاستدعاءات التالية لهذا الأسلوب تعيد الكائن المحفوظ مسبقًا (Cached).

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

مثال من الواقع

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

البُنية

  1. تصرح فئة Singleton عن الأسلوب الساكن getInstance الذي يعيد نفس النسخة من فئته. وينبغي أن يكون منشئ المفردة (Singleton's Constructor) مخفيًا من شيفرة العميل، وكذلك يجب أن يكون أسلوب getInstance هو الطريقة الوحيدة للحصول على كائن مفردة.

مثال توضيحي

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

// الذي يسمح للعملاء بالوصول getInstance تعرِّف فئة قاعدة البيانات أسلوب
// إلى نفس النسخة من وصلة قاعدة البيانات في البرنامج كله.
class Database is
    private field instance: Database

    // يجب أن يكون منشئ المفردة خاصًا دائمًا لمنع استدعاءات الإنشاء المباشرة
    // new باستخدام معامل
    private constructor Database() is
        // شيفرة بدء مثل الوصلة الفعلية لخادم قاعدة بيانات

    // الأسلوب الساكن الذي يتحكم في الوصول إلى نسخة المفردة.
    static method getInstance() is
        if (this.instance == null) then
            acquireThreadLock() and then
                // (thread) تأكد أن النسخة لم تُبدأ بعد بواسطة أي خيط
                // آخر إن كان هذا الخيط بانتظار تحرير القفل.
                if (this.instance == null) then
                    this.instance = new Database()
        return this.instance

    // أخيرًا، يجب أن تعرِّف أي مفردة بعض المنطق التجاري الذي يمكن
    // تنفيذه في هذه النسخة.
    public method query(sql) is
        // فمثلًا، ستمر كل استعلامات تطبيق ما على هذه الأسلوب، ومن ثم
        //  هنا(caching) أو حفظ مؤقت (throttling) يمكنك وضع منطق خنق.

class Application is
    method main() is
        Database foo = Database.getInstance()
        foo.query("SELECT ...")
        // ...
        Database bar = Database.getInstance()
        bar.query("SELECT ...")
        // foo على نفس الكائن الذي يحتويه متغير bar سيحتوي متغير.

قابلية الاستخدام

استخدم نمط المفردة في حالة وجوب توفر نسخة واحدة فقط من فئة ما لكل العملاء، ككائن قاعدة بيانات وحيد مشترك بين أجزاء مختلفة في البرنامج.

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

استخدم نمط المفردة حين تريد تحكمًا أكثر في متغيراتك العامة.

على عكس المتغيرات العامة (Global Variables)، فإن نمط المفردة يضمن وجود نسخة واحدة فقط من فئة ما، ولا شيء يمكنه استبدال تلك النسخة المحفوظة باستثناء فئة المفردة نفسها. لاحظ أنك تستطيع تعديل هذا التقييد في أي وقت لتسمح بإنشاء أي عدد من نسخ المفردة، والجزء الذي تحتاج إلى تعديله في الشيفرة حينها هو المتن الرئيسي (body) في أسلوب ()getInstance.

كيفية الاستخدام

  1. أضف حقلًا ساكنًا خاصًا إلى الفئة لتخزين نسخة المفردة.
  2. صرِّح عن أسلوب إنشاءٍ ساكنٍ عام للحصول على نسخة المفردة.
  3. استخدم البدء الكسول (lazy initialization) داخل الأسلوب الساكن، إذ يجب أن تنشئ كائنًا جديدًا في الاستدعاء الأول لها وتضعه في الحقل الساكن. كذلك يجب أن يعيد الأسلوب تلك النسخة دومًا في كل الاستدعاءات التالية.
  4. اجعل منشئ الفئة خاصًا (private)، وسيظل أسلوب الفئة الساكن قادرًا على استدعاء المنشئ لكن لا يستطيع استدعاء الكائنات الأخرى.
  5. اذهب لشيفرة العميل واستبدل كل الاستدعاءات المباشرة لمنشئ المفردة باستدعاءات إلى أسلوب الإنشاء الساكن الخاص بها.

المزايا والعيوب

المزايا

  • تستطيع ضمان وجود نسخة واحدة فقط من فئة ما.
  • تحصل على نقطة وصول عامة إلى تلك النسخة.
  • لا يُبدأ كائن المفردة إلا حين يستدعى لأول مرة.

العيوب

  • يخرق نمط المفردة مبدأ المسؤولية الواحدة، إذ يحل النمط مشكلتين في نفس الوقت.
  • يغطي على عيوب التصميم، كأن يكون لدى مكونات البرنامج بيانات أكثر من المطلوب عن بعضها البعض.
  • يتطلب معالجة خاصة في بيئة متعددة الخيوط (multi-threaded) كي لا تنشئ الخيوط المتعددة كائن مفردة أكثر من مرة.
  • قد يكون من الصعب اختبار شيفرة العميل جزءًا جزءًا - أو على مستوى الوحدة (unit test)- لنمط المفردة بسبب أن العديد من إطارات العمل للاختبارات تعتمد على الاكتساب (inheritance) عند إنتاج كائنات مقلدة (mock objects). وبما أن منشئ فئة المفردةِ خاصًا، ومن المستحيل تخطي الأساليب الساكنة في أغلب اللغات، فإنك في حاجة إلى التفكير في طريقة مبدعة لتقليد المفردة، وإلا فلا تكتب تلك الاختبارات أو تستخدم نمط المفردة على الإطلاق.

العلاقات مع الأنماط الأخرى

  • فئة نمط الواجهة قد تتحول إلى مفردة بما أن كائن الواجهة الواحد يكون كافيًا في أغلب الأحوال.
  • قد يتشابه نمط وزن الذبابة (Flyweight) مع نمط المفردة إن استطعت تقليل كل الحالات المشتركة للكائنات إلى كائن واحد من نمط وزن الذبابة، لكن هناك اختلافان رئيسيان بين هذين النمطين:
  1. يجب أن تكون هناك نسخة واحدة من المفردة، بينما يمكن أن يكون لفئة وزن الذبابة أكثر من نسخة بحالات فعلية مختلفة.
  2. يمكن لكائن المفردة أن يكون متقلبًا، أما كائنات نمط وزن الذبابة فتكون ثابتة وغير قابلة للتغيير.
  • يمكن استخدام أنماط المصنع المجرد والباني والنموذج الأولي كمفردات.

الاستخدام في لغة جافا

المستوى: ★ ☆ ☆

الانتشار: ★ ★ ☆

أمثلة الاستخدام: ينظر كثير من المطورين إلى نمط المفردة على أنه نمط عكسي (antipattern) لهذا يتراجع استخدامه مع الوقت في شيفرة جافا، لكن رغم ذلك فهناك الكثير من الأمثلة لنمط المفردة في مكتبات جافا:

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

مثال: المفردة البسيطة (ذات الخيط الواحد)

من السهل استخدام مفردة غامضة، فما عليك سوى إخفا المنشئ واستخدام أسلوب إنشاء ساكن.

 Singleton.java: المفردة

package refactoring_guru.singleton.example.non_thread_safe;

public final class Singleton {
    private static Singleton instance;
    public String value;

    private Singleton(String value) {
        // تحاكي الشيفرة التالية عملية البدء البطيء.
        try {
            Thread.sleep(1000);
        } catch (InterruptedException ex) {
            ex.printStackTrace();
        }
        this.value = value;
    }

    public static Singleton getInstance(String value) {
        if (instance == null) {
            instance = new Singleton(value);
        }
        return instance;
    }
}

 DemoSingleThread.java: شيفرة العميل

package refactoring_guru.singleton.example.non_thread_safe;

public class DemoSingleThread {
    public static void main(String[] args) {
        System.out.println("إذا رأيت نفس القيمة فذلك يعني إعادة استخدام للمفردة" + "\n" +
                "إن رأيت قيمًا مختلفة فيعني ذلك إنشاء مفردتين." + "\n\n" +
                "RESULT:" + "\n");
        Singleton singleton = Singleton.getInstance("FOO");
        Singleton anotherSingleton = Singleton.getInstance("BAR");
        System.out.println(singleton.value);
        System.out.println(anotherSingleton.value);
    }
}

 OutputDemoSingleThread.txt: نتائج التنفيذ

إذا رأيت نفس القيمة فذلك يعني إعادة استخدام للمفردة
إن رأيت قيمًا مختلفة فذلك يعني إنشاء مفردتين

RESULT:

FOO
FOO

مثال: المفردة البسيطة (متعددة الخيوط)

تتصرف نفس الفئة بشكل خاطئ في بيئة متعددة الخيوط (multithreaded)، ذلك أن الخيوط المتعددة يمكنها استدعاء أسلوب إنشائي في نفس الوقت والحصول على نسخ متعددة من فئة المفردة.

 Singleton.java: المفردة

package refactoring_guru.singleton.example.non_thread_safe;

public final class Singleton {
    private static Singleton instance;
    public String value;

    private Singleton(String value) {
        // تحاكي الشيفرة التالية عملية البدء البطيء.
        try {
            Thread.sleep(1000);
        } catch (InterruptedException ex) {
            ex.printStackTrace();
        }
        this.value = value;
    }

    public static Singleton getInstance(String value) {
        if (instance == null) {
            instance = new Singleton(value);
        }
        return instance;
    }
}

 DemoMultiThread.java: شيفرة العميل

package refactoring_guru.singleton.example.non_thread_safe;

public class DemoMultiThread {
    public static void main(String[] args) {
        System.out.println("إذا رأيت نفس القيمة فذلك يعني إعادة استخدام نفس المفردة" + "\n" +
                "إذا رأيت قيمًا مختلفة فذلك يعني إنشاء مفردتين" + "\n\n" +
                "RESULT:" + "\n");
        Thread threadFoo = new Thread(new ThreadFoo());
        Thread threadBar = new Thread(new ThreadBar());
        threadFoo.start();
        threadBar.start();
    }

    static class ThreadFoo implements Runnable {
        @Override
        public void run() {
            Singleton singleton = Singleton.getInstance("FOO");
            System.out.println(singleton.value);
        }
    }

    static class ThreadBar implements Runnable {
        @Override
        public void run() {
            Singleton singleton = Singleton.getInstance("BAR");
            System.out.println(singleton.value);
        }
    }
}

 OutputDemoMultiThread.txt: نتائج التنفيذ

إذا رأيت نفس القيمة فذلك يعني إعادة استخدام نفس المفردة.
إذا رأيت قيمًا مختلفة فذلك يعني إنشاء مفردتين.

RESULT:

FOO
BAR

مثال: المفردة الصحيحة (آمنة على الخيوط، تحميل كسول)

من أجل إصلاح المشكلة، يجب أن تزامن الخيوط في أول إنشاء لكائن المفردة.

 Singleton.java: المفردة

package refactoring_guru.singleton.example.thread_safe;

public final class Singleton {
    private static volatile Singleton instance;
    public String value;

    private Singleton(String value) {
        this.value = value;
    }

    public static Singleton getInstance(String value) {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton(value);
                }
            }
        }
        return instance;
    }
}

 DemoMultiThread.java: شيفرة العميل

package refactoring_guru.singleton.example.thread_safe;

public class DemoMultiThread {
    public static void main(String[] args) {
        System.out.println("إذا رأيت نفس القيمة فذلك يعني إعادة استخدام نفس المفردة" + "\n" +
                "إذا رأيت قيمًا مختلفة فذلك يعني إنشاء مفردتين" + "\n\n" +
                "RESULT:" + "\n");
        Thread threadFoo = new Thread(new ThreadFoo());
        Thread threadBar = new Thread(new ThreadBar());
        threadFoo.start();
        threadBar.start();
    }

    static class ThreadFoo implements Runnable {
        @Override
        public void run() {
            Singleton singleton = Singleton.getInstance("FOO");
            System.out.println(singleton.value);
        }
    }

    static class ThreadBar implements Runnable {
        @Override
        public void run() {
            Singleton singleton = Singleton.getInstance("BAR");
            System.out.println(singleton.value);
        }
    }
}

 OutputDemoMultiThread.txt: نتائج الاستخدام

إذا رأيت نفس القيمة فذلك يعني إعادة استخدام نفس المفردة
إذا رأيت قيمًا مختلفة فذلك يعني إنشاء مفردتين

RESULT:

BAR
BAR

الاستخدام في لغة #C

المستوى: ★ ☆ ☆

الانتشار: ★ ★ ☆

أمثلة الاستخدام: ينظر كثير من المطورين إلى نمط المفردة على أنه نمط عكسي (antipattern) لهذا يتراجع استخدامه مع الوقت في شيفرة #C.

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

مثال: بنية النمط

يوضح هذا المثال بنية نمط المفردة، ويركز على إجابة الأسئلة التالية:

  • ما الفئات التي يتكون منها؟
  • ما الأدوار التي تلعبها هذه الفئات؟
  • كيف ترتبط عناصر النمط ببعضها؟

 Program.cs: مثال هيكلي

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Singleton
{
    class Program
    {
        static void Main(string[] args)
        {
            Client client = new Client();
            client.ClientCode();
        }
    }

    class Client
    {
        public void ClientCode()
        {
            Singleton s1 = Singleton.getInstance();
            Singleton s2 = Singleton.getInstance();

            if(s1 == s2)
            {
                Console.WriteLine("نمط المفردة يعمل!. كلاالمتغيران يحتويان نفس النسخة.");
            }
            else
            {
                Console.WriteLine("فشل نمط المفردة!. احتوى المتغيران على نسخ مختلفة.");
            }
        }
    }

    class Singleton
    {
        private static Singleton instance;

        private static object obj = new object();

        private Singleton()
        { }

        public static Singleton getInstance()
        {
            lock(obj)
            {
                if (instance == null)
                    instance = new Singleton();
            }

            return instance;
        }
    }
}

 Output.txt: المخرجات

نمط المفردة يعمل!. كلا المتغيران يحتويان نفس النسخة.

الاستخدام في لغة PHP

المستوى: ★ ☆ ☆

الانتشار: ★ ★ ☆

أمثلة الاستخدام: ينظر كثير من المطورين إلى نمط المفردة على أنه نمط عكسي (antipattern) لهذا يتراجع استخدامه مع الوقت في شيفرة PHP.

مثال: بنية النمط

يوضح هذا المثال بنية نمط المفردة، ويركز على إجابة الأسئلة التالية:

  • ما الفئات التي يتكون منها؟
  • ما الأدوار التي تلعبها هذه الفئات؟
  • كيف ترتبط عناصر النمط ببعضها؟

سيكون من السهل بعد تعلم بنية النمط أن تستوعب المثال التالي بناءً على مثال واقعي لاستخدام لغة PHP.

 SingletonStructural.php: مثال هيكلي

<?php

namespace RefactoringGuru\Singleton\Structural;

/**
 * الذي يسمح للعملاء بالوصول إلى نسخة getInstance تعرِّف فئة المفردةأسلوب
 * فريدة من المفردة.
 */
class Singleton
{
    private static $instances = [];

    /**
     * يجب أن يكون منشئ المفردة خاصًا دومًا لمنع استدعاءات الإنشاء المباشرة
     * new باستخدام معامل.
     */
    protected function __construct() { }

    /**
     * يجب ألا تكون المفردات قابلة للاستنساخ.
     */
    protected function __clone() { }

    /**
     * (strings) يجب ألا تكون المفردات قابلة للاسترجاع من النصوص.
     */
    public function __wakeup()
    {
        throw new \Exception("Cannot unserialize a singleton.");
    }

    /**
     * الأسلوب الساكن الذي يتحكم في الوصول إلى نسخة المفردة.
     *
     * يسمح لك هذا الاستخدام بتفريع فئة المفردة مع إبقاء نسخة واحدة فقط 
     * من كل فئة فرعية.
     */
    public static function getInstance(): Singleton
    {
        $cls = static::class;
        if (! isset(static::$instances[$cls])) {
            static::$instances[$cls] = new static;
        }

        return static::$instances[$cls];
    }

    /**
     * أخيرًا، يجب أن تعرِّف كلُّ مفردة بعض المنطق التجاري الذي يمكن تنفيذه
     * في نسختها.
     */
    public function someBusinessLogic()
    {
        // ...
    }
}

/**
 * شيفرة العميل.
 */
function clientCode()
{
    $s1 = Singleton::getInstance();
    $s2 = Singleton::getInstance();
    if ($s1 === $s2) {
        echo "نمط المفردة يعمل. كلا المتغيران يحتويان نفس النسخة.";
    } else {
        echo "فشل نمط المفردة. المتغيران يحتويان نسخًا مختلفة";
    }
}

clientCode();

 Output.txt: المخرجات

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

مثال: حالة حقيقية

يشتهر نمط المفردة أنه يحد من إعادة استخدام الشيفرة وبتعقيد اختبار الوحدات، لكنه لا يزال مفيدًا في بعض الحالات، خاصة حين تريد التحكم في بعض المصادر المشتركة، كحالة كائن تسجيل دخول عام يجب أن يتحكم في الوصول إلى ملف سجل ما، أو سعة تخزين إعدادت وقت تشغيل مشتركة (Shared runtime configuration storage).

 SingletonRealWorld.php: مثال واقعي

<?php

namespace RefactoringGuru\Singleton\RealWorld;

/**
 * نمط تصميم المفردة
 *
 * الهدف: ضمان أن فئة ما لها نسخة واحدة، وتوفير نقطة وصول عامةلها.
 * 
 * يشتهر نمط المفردة أنه يحد من إعادة استخدام الشيفرة وبتعقيد اختبار 
 * الوحدات، لكنه لا يزال مفيدًا في بعض الحالات، خاصة حين تريد التحكم في بعض 
 * المصادر المشتركة، كحالة كائن تسجيل دخول عام يجب أن يتحكم في الوصول إلى 
 * ملف سجل ما، أو سعة تخزين إعدادت وقت تشغيل مشتركة.
 */

/**
 * إن احتجت إلى دعم عدة أنواع من المفردات في تطبيقك فيمكنك تعريف المزايا
 * الأساسية للمفردة في فئة أساسية في حين تنقل المنطق التجاري الفعلي مثل 
 * إلى فئات فرعية logging التسجيل
 */
class Singleton
{
    /**
     * توجد النسخة الفعلية للمفردة داخل حقل ساكن طول الوقت تقريبًا، ويكون 
     * الحقل الساكن في تلك الحالة عبارة عن مصفوفة تسجِّل فيها كل فئة فرعية من 
     * المفردة نسختها الخاصة بها. 
     */
    private static $instances = [];

    /**
     * يجب ألا يكون منشئ المفردة عامًا، لكنه في نفس الوقت لا يمكن أن يكون خاصًا 
     * كذلك إن كنا نرغب بالسماح بإنشاء فئات فرعية.
     */
    protected function __construct() { }

    /**
     * للمفردات (unserialization) لا يسمح بالاستنساخ أو إلغاء التسلسل.
     */
    protected function __clone() { }

    public function __wakeup()
    {
        throw new \Exception("Cannot unserialize singleton");
    }

    /**
     * الأسلوب الذي تستخدمه للحصول على نسخة المفردة.
     */
    public static function getInstance()
    {
        $subclass = static::class;
        if (!isset(self::$instances[$subclass])) {
            // static لاحظ أننا هنا نستخدم الكلمة المفتاحية
            // بدلًا من الاسم الفعلي للفئة، ويُقصد بهذه الكلمة في هذا السياق 
            // اسمَ الفئة الحالية، وذلك التفصيل مهم لأنه عند استدعاء الأسلوب
            // على الفئة الفرعية فإننا نريد إنشاء نسخة من تلك الفئة الفرعية
            // هنا.
           
            self::$instances[$subclass] = new static;
        }
        return self::$instances[$subclass];
    }
}

/**
 * فئة تسجيل الدخول هي أكثر استخدام معروف لنمط المفردة، ففي أغلب الحالات 
 * تحتاج كائن تسجيل دخول وحيد يكتب في ملف سجل واحد، وهذا هو التحكم في مصدر 
 * مشترك. كما تحتاج أيضًا إلى طريقة مناسبة للوصول إلى تلك النسخة من أي سياق 
 * في تطبيقك، وتلك هي نقطة الوصول العامة.
 */
class Logger extends Singleton
{
    /**
     * من ملف السجل file pointer resource مصدر مؤشر ملف
     */
    private $fileHandle;

    /**
     * بما أن منشئ المفردة قد استُدعي مرة واحدة فقط، فإن مصدر ملف واحد فقط
     * قد تم فتحه.
     *
     * console stream لاحظ أنه وبداعي التبسيط، سنفتح سيل المنصة
     * بدلًا من الملف الفعلي هنا.
     */
    protected function __construct()
    {
        $this->fileHandle = fopen('php://stdout', 'w');
    }

    /**
     * الملف المفتوح resource اكتب مُدخل سجل لمصدر 
     */
    public function writeLog(string $message): void
    {
        $date = date('Y-m-d');
        fwrite($this->fileHandle, "$date: $message\n");
    }

    /**
     * مجرد اختصار بسيط لتقليل الشيفرة المطلوبة لتسجيل الرسائل من شيفرة 
     * العميل
     */
    public static function log(string $message): void
    {
        $logger = static::getInstance();
        $logger->writeLog($message);
    }
}

/**
 * يشيع أيضًا تطبيق نمط المفردة على إعدادات التخزين، وغالبًا تحتاج إلى الدخول
 * لإعدادات التطبيق من أماكن كثيرة من البرنامج، ونمط المفردة ييسر هذه 
 * العملية.
 */
class Config extends Singleton
{
    private $hashmap = [];

    public function getValue(string $key): string
    {
        return $this->hashmap[$key];
    }

    public function setValue(string $key, string $value): void
    {
        $this->hashmap[$key] = $value;
    }
}

/**
 * شيفرة العميل.
 */
Logger::log("Started!");

// logger singleton قارن القيَم من.
$l1 = Logger::getInstance();
$l2 = Logger::getInstance();
if ($l1 === $l2) {
    Logger::log("Logger has a single instance.");
} else {
    Logger::log("Loggers are different.");
}

// على البيانات config singleton انظر كيف يحافظ...
$config1 = Config::getInstance();
$login = "test_login";
$password = "test_password";
$config1->setValue("login", $login);
$config1->setValue("password", $password);
// ...ومن ثم يستعيدها.
$config2 = Config::getInstance();
if ($login == $config2->getValue("login") &&
    $password == $config2->getValue("password")
) {
    Logger::log("Config singleton also works fine.");
}

Logger::log("Finished!");

 Output.txt: المخرجات

2018-06-04: Started!
2018-06-04: Logger has a single instance.
2018-06-04: Config singleton also works fine.
2018-06-04: Finished!

انظر أيضًا

مصادر