نمط المفرد (Singleton)
نمط المفرد (Singleton)
يضمن نمط المفرد أن يكون للصنف كائن واحد فقط طوال دورة حياة التطبيق، مع توفير نقطة وصول عالمية إلى ذلك الكائن. يُعدّ من أكثر أنماط الإنشاء استخدامًا — وفي الوقت ذاته من أكثرها إساءةً في التوظيف — ضمن مجموعة أنماط GoF.
متى يكون استخدام المفرد مناسبًا؟
يجب أن يكون الصنف من نوع مفرد حين تتحقّق الشرطان معًا:
- يجب أن يوجد كائن واحد بالضبط — مثل سجل الإعدادات، أو مجمّع الاتصالات، أو الذاكرة المؤقتة الآمنة، أو واجهة تعامل مع أجهزة مادية.
- يجب الوصول إلى ذلك الكائن من أماكن متعددة دون الحاجة إلى تمريره عبر كل مُنشئ أو دالة.
التهيئة الفورية (Eager Initialization)
أبسط صورة: يُنشأ الكائن فور تحميل الصنف بواسطة JVM. ولأن تحميل الأصناف مضمون خيطيًا بحسب مواصفة JVM، لا حاجة إلى أي تزامن.
المقايضة: يُنشأ الكائن حتى لو لم يُستخدم قط. بالنسبة للكائنات خفيفة الوزن هذا مقبول. أمّا للموارد المكلفة (مثل مجمّع اتصالات قاعدة البيانات) التي قد لا تكون مطلوبة دائمًا، فالتهيئة الكسولة أفضل.
التهيئة الكسولة — اصطلاح القفل ذي الفحص المزدوج
تؤجّل التهيئة الكسولة إنشاء الكائن حتى أول استدعاء لـ getInstance(). في بيئة متزامنة، فحص if (instance == null) البسيط ليس آمنًا — يمكن لخيطين أن يريا null ويُنشئ كل منهما كائنًا مستقلًا. الحل الكلاسيكي هو القفل ذو الفحص المزدوج مقرونًا بالكلمة المفتاحية volatile:
volatile إلزامية هنا؟ بدونها يُجيز JVM إعادة ترتيب الكتابة على INSTANCE بحيث قد يرى خيط آخر كائنًا مُنشأ جزئيًا. تفرض volatile علاقة "يحدث قبل": يكتمل البناء الكامل قبل نشر المرجع. إغفالها يُحدث سباق بيانات خفيًا يصعب إعادة إنتاجه على المعالجات متعددة الأنوية.
اصطلاح الحامل — التهيئة عند الطلب
بديل كسول أكثر أناقة يتجنّب التزامن الصريح كليًا، مستغلًا ضمان JVM بأن الصنف لا يُهيَّأ إلا مرة واحدة وعند أول وصول إليه فقط:
المفرد بصيغة Enum — المعيار الذهبي
يوصي Joshua Bloch في كتابه Effective Java (البند 3) بتنفيذ المفرد على شكل مُعدَّد (enum) ذي عنصر واحد. هذا هو الأسلوب الأكثر إيجازًا وأمانًا خيطيًا وتسلسلًا في Java:
يُبطل أسلوب enum ثلاث هجمات كلاسيكية تكسر المفرد:
- الانعكاس (Reflection): يُطلق
Constructor.setAccessible(true)استثناءIllegalArgumentExceptionلمُنشئات enum — يفرض ذلك JVM نفسه. - التسلسل (Serialization): عادةً يُنتج إلغاء التسلسل كائنًا جديدًا مما يُخلّ بالضمان. أما تسلسل enum فيُديره JVM ويعيد دائمًا الكائن الوحيد الأصلي — دون الحاجة لتجاوز
readResolve(). - النسخ (Cloning): لا يُنفّذ
EnumواجهةCloneable، لذا يُطلقclone()استثناءCloneNotSupportedException.
java.lang.Enum). إن كان مفردك بحاجة للوراثة من صنف مجرد، أو لتهيئة كسولة بمنطق معقد، فاستخدم اصطلاح الحامل عوضًا عن ذلك. لكن في الحالة الشائعة — مفرد عديم الحالة أو بحالة بسيطة — صيغة enum هي الأمثل.
مقارنة سلامة الخيوط
- التهيئة الفورية: آمنة خيطيًا دون جهد إضافي. يُنشأ الكائن دائمًا.
- القفل ذو الفحص المزدوج: آمن مع
volatile؛ كسول. شائع في الكود القديم. - اصطلاح الحامل: آمن عبر دلالات تهيئة الصنف في JVM؛ كسول. موصى به للمفردات القائمة على الصنف.
- مفرد enum: آمن خيطيًا؛ آمن تسلسليًا؛ محصّن ضد الانعكاس. موصى به في Effective Java.
المفردات وحقن التبعيات
النمط الاحترافي الشائع هو الجمع بين نطاق المفرد وحقن التبعيات. تُدير أطر العمل مثل Spring كائنات المفرد (beans) نيابةً عنك — تُعلن صنفًا بـ @Service أو @Component وتضمن الحاوية وجود كائن واحد. تحصل على كامل مزايا قابلية الاختبار دون الحاجة لكتابة قفل مزدوج يدويًا.
حين تكتب مفردات خاملة (خارج حاوية DI)، برمج بحسب الواجهة لا الصنف المفرد الملموس. هذا يحفظ قابلية الاختبار:
الخلاصة
استخدم التهيئة الفورية حين تكون تكلفة إنشاء الكائن منخفضة. استخدم اصطلاح الحامل حين تحتاج لتهيئة كسولة لمفرد قائم على صنف. استخدم مفرد enum كخيار افتراضي لأي مفرد جديد لا يحتاج للوراثة — فهو موجز وآمن خيطيًا وتسلسليًا ومحصّن ضد الانعكاس. تعلّم القفل ذا الفحص المزدوج للتعامل مع الكود القديم بأمان.