المقدمة
نمط الـ Singleton في جافا هو واحد من أنماط تصميم مجموعة الأربعة ويأتي في فئة أنماط تصميم الإنشاء. من خلال التعريف، يبدو أنه نمط تصميم بسيط، ولكن عندما يتعلق الأمر بالتنفيذ، يأتي مع العديد من الاعتبارات.
في هذا المقال، سنتعلم مبادئ نمط تصميم الـ Singleton، نستكشف طرقًا مختلفة لتنفيذ هذا النمط، وبعض أفضل الممارسات لاستخدامه.
مبادئ نمط الـ Singleton
- يقيد نمط الـ Singleton إنشاء كائن من الفئة ويضمن أن يكون هناك فقط مثيل واحد من الفئة في الآلة الافتراضية لجافا.
- يجب أن توفر الفئة Singleton نقطة وصول عالمية للحصول على مثيل من الفئة.
- يُستخدم نمط الـ Singleton لأغراض مثل تسجيل الأحداث، كائنات السائقين، التخزين المؤقت، و حوض الخيوط.
- نمط التصميم الفردي يستخدم أيضًا في أنماط تصميم أخرى مثل المصنع النموذجي، البنّاء، النموذج الأصلي، الواجهة، إلخ.
- يُستخدم نمط التصميم الفردي أيضًا في فئات جافا الأساسية (على سبيل المثال،
java.lang.Runtime
،java.awt.Desktop
).
تنفيذ نمط التصميم الفردي في جافا
لتنفيذ نمط الفردي، لدينا نهج مختلفة، ولكن لديها جميعًا المفاهيم الشائعة التالية.
- البناء الخاص لتقييد التوصيل للفئة من فئات أخرى.
- المتغير الثابت الخاص لنفس الفئة الذي هو النسخة الوحيدة من الفئة.
- الطريقة الثابتة العامة التي تُرجع مثيل الفئة، وهذه هي نقطة الوصول العالمية للعالم الخارجي للحصول على مثيل الفئة الفردية.
في الأقسام اللاحقة، سنتعلم الطرق المختلفة لتنفيذ نمط الفردي والمخاوف التصميمية مع التنفيذ.
1. التهيئة الحثية
في التهيئة الحثية، يتم إنشاء مثيل الفئة الفردية في وقت تحميل الفئة. العيب في التهيئة الحثية هو أن الطريقة تُنشأ حتى لو لم تكن تطبيق العميل يستخدمها. هذا هو تنفيذ فئة الفردية بالتهيئة الثابتة:
package com.journaldev.singleton;
public class EagerInitializedSingleton {
private static final EagerInitializedSingleton instance = new EagerInitializedSingleton();
// البناء الفردي لتجنب استخدام تطبيقات العميل للبناء
private EagerInitializedSingleton(){}
public static EagerInitializedSingleton getInstance() {
return instance;
}
}
إذا لم تكن فئة الفردية تستخدم الكثير من الموارد، فهذا هو النهج المناسب للاستخدام. ولكن في معظم الحالات، تُنشأ فئات الفردية للموارد مثل نظام الملفات، اتصالات قواعد البيانات، إلخ. يجب علينا تجنب الاستنساخ ما لم يُطلب العميل بطريقة getInstance
. أيضًا، هذه الطريقة لا توفر أي خيارات لمعالجة الاستثناءات.
2. التهيئة بالكتلة الثابتة
تهيئة كتلة تنفيذ التهيئة تكون مشابهة لتهيئة eager، باستثناء أن نسخة من الفئة يتم إنشاؤها في كتلة السكون التي توفر الخيار لـ معالجة الاستثناء.
package com.journaldev.singleton;
public class StaticBlockSingleton {
private static StaticBlockSingleton instance;
private StaticBlockSingleton(){}
// تهيئة الكتلة السكونة لمعالجة الاستثناء
static {
try {
instance = new StaticBlockSingleton();
} catch (Exception e) {
throw new RuntimeException("Exception occurred in creating singleton instance");
}
}
public static StaticBlockSingleton getInstance() {
return instance;
}
}
كل من التهيئة الفورية وتهيئة الكتلة السكونة تقوم بإنشاء النسخة حتى قبل استخدامها، وهذا ليس أفضل ممارسة للاستخدام.
3. تأخير التهيئة
طريقة التهيئة الكسولة لتنفيذ نمط الفردي تقوم بإنشاء النسخة في طريقة الوصول العامة. فيما يلي رمز عينة لإنشاء فئة الفردي بهذا النهج:
package com.journaldev.singleton;
public class LazyInitializedSingleton {
private static LazyInitializedSingleton instance;
private LazyInitializedSingleton(){}
public static LazyInitializedSingleton getInstance() {
if (instance == null) {
instance = new LazyInitializedSingleton();
}
return instance;
}
}
التنفيذ السابق يعمل بشكل جيد في حالة البيئة ذات الخيط الواحد، ولكن عندما يتعلق الأمر بالأنظمة متعددة الخيوط، يمكن أن يسبب مشاكل إذا كانت عدة خيوط داخل الشرط if
في نفس الوقت. سيؤدي ذلك إلى تدمير نمط الفردي وستحصل الخيوط على نسخ مختلفة من فئة الفردي. في القسم التالي، سنرى وسائل مختلفة لإنشاء فئة فرديه آمنة للخيوط.
4. وحيد الخيط الآمن
A simple way to create a thread-safe singleton class is to make the global access method synchronized so that only one thread can execute this method at a time. Here is a general implementation of this approach:
package com.journaldev.singleton;
public class ThreadSafeSingleton {
private static ThreadSafeSingleton instance;
private ThreadSafeSingleton(){}
public static synchronized ThreadSafeSingleton getInstance() {
if (instance == null) {
instance = new ThreadSafeSingleton();
}
return instance;
}
}
التنفيذ السابق يعمل بشكل جيد ويوفر أمان الخيط، ولكنه يقلل من الأداء بسبب التكلفة المرتبطة بالطريقة المتزامنة، على الرغم من أننا بحاجة إليها فقط للخيوط الأولى التي قد تنشئ مثيلات منفصلة. لتجنب هذه السمة الإضافية في كل مرة، يتم استخدام مبدأ القفل المزدوج. في هذا النهج، يُستخدم الكتلة المتزامنة داخل شرط if
مع فحص إضافي للتأكد من أنه تم إنشاء مثيل وحيد فقط من فئة وحيد. تقدم مقتطف الكود التالي تنفيذ القفل المزدوج:
public static ThreadSafeSingleton getInstanceUsingDoubleLocking() {
if (instance == null) {
synchronized (ThreadSafeSingleton.class) {
if (instance == null) {
instance = new ThreadSafeSingleton();
}
}
}
return instance;
}
استمر في التعلم مع فئة وحيد الخيط الآمن.
5. تنفيذ بيل بيوغ لوحيد
قبل جافا 5، كانت نموذج الذاكرة في جافا يعاني من العديد من المشاكل، وكانت الطرق السابقة تفشل في سيناريوهات معينة حيث يحاول العديد من الخيوط الحصول على مثيل من فئة البيان الوحيد بشكل متزامن. لذلك، جاء بيل بيو بنهج مختلف لإنشاء فئة البيان الوحيد باستخدام فئة مساعدة داخلية ثابتة. فيما يلي مثال على تنفيذ فئة البيان الوحيد بواسطة بيل بيو:
package com.journaldev.singleton;
public class BillPughSingleton {
private BillPughSingleton(){}
private static class SingletonHelper {
private static final BillPughSingleton INSTANCE = new BillPughSingleton();
}
public static BillPughSingleton getInstance() {
return SingletonHelper.INSTANCE;
}
}
لاحظ ال فئة الداخلية الثابتة الخاصة التي تحتوي على مثيل من فئة البيان الوحيد. عندما يتم تحميل فئة البيان الوحيد، لا تتم تحميل فئة SingletonHelper
في الذاكرة، وفقط عندما يقوم شخص ما باستدعاء طريقة getInstance()
، تتم تحميل هذه الفئة وتنشئ مثيل فئة البيان الوحيد. هذا هو النهج الأكثر استخداماً لفئة البيان الوحيد حيث لا يتطلب التزامن.
6. استخدام الانعكاس لتدمير نمط البيان الوحيد
يمكن استخدام الانعكاس لتدمير جميع الطرق السابقة لتنفيذ فئة البيان الوحيد. فيما يلي مثال على فئة:
package com.journaldev.singleton;
import java.lang.reflect.Constructor;
public class ReflectionSingletonTest {
public static void main(String[] args) {
EagerInitializedSingleton instanceOne = EagerInitializedSingleton.getInstance();
EagerInitializedSingleton instanceTwo = null;
try {
Constructor[] constructors = EagerInitializedSingleton.class.getDeclaredConstructors();
for (Constructor constructor : constructors) {
// سيدمر هذا الكود نمط البيان الوحيد
constructor.setAccessible(true);
instanceTwo = (EagerInitializedSingleton) constructor.newInstance();
break;
}
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(instanceOne.hashCode());
System.out.println(instanceTwo.hashCode());
}
}
عند تشغيل فئة الاختبار السابقة، ستلاحظ أن `hashCode` لكل من الحالتين ليسا نفس الذي يدمر نمط النمط الفردي. الانعكاس (Reflection) قوي جدًا ويستخدم في العديد من الأطر مثل Spring و Hibernate. استمر في تعلمك مع دليل تعليمي حول انعكاس جافا.
7. النمط الفردي كالتعداد
للتغلب على هذا الوضع مع الانعكاس، يقترح جوشوا بلوخ (Joshua Bloch) استخدام `enum` لتنفيذ نمط التصميم الفردي حيث يضمن جافا أن أي قيمة `enum` تُنشأ مرة واحدة فقط في برنامج جافا. نظرًا لأن قيم `Java Enum` متاحة على نطاق عالمي، فإن الفردي كذلك. العيب هو أن نوع `enum` مرن إلى حد ما (على سبيل المثال، لا يسمح بالتهيئة الكسلية).
package com.journaldev.singleton;
public enum EnumSingleton {
INSTANCE;
public static void doSomething() {
// قم بعمل ما
}
}
8. التسلسل والفردي
أحيانًا في الأنظمة الموزعة ، نحتاج إلى تنفيذ واجهة Serializable
في الفئة الفردية حتى نتمكن من تخزين حالتها في نظام الملفات واستردادها في وقت لاحق. هنا فئة فردية صغيرة تنفذ أيضًا واجهة Serializable
:
package com.journaldev.singleton;
import java.io.Serializable;
public class SerializedSingleton implements Serializable {
private static final long serialVersionUID = -7604766932017737115L;
private SerializedSingleton(){}
private static class SingletonHelper {
private static final SerializedSingleton instance = new SerializedSingleton();
}
public static SerializedSingleton getInstance() {
return SingletonHelper.instance;
}
}
المشكلة مع الفئة الفردية المُسلسلة هي أنه في كل مرة نقوم فيها بتسلسلها ، سيتم إنشاء مثيل جديد من الفئة. إليك مثالًا:
package com.journaldev.singleton;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectInputStream;
import java.io.ObjectOutput;
import java.io.ObjectOutputStream;
public class SingletonSerializedTest {
public static void main(String[] args) throws FileNotFoundException, IOException, ClassNotFoundException {
SerializedSingleton instanceOne = SerializedSingleton.getInstance();
ObjectOutput out = new ObjectOutputStream(new FileOutputStream(
"filename.ser"));
out.writeObject(instanceOne);
out.close();
// فك التسلسل من الملف إلى كائن
ObjectInput in = new ObjectInputStream(new FileInputStream(
"filename.ser"));
SerializedSingleton instanceTwo = (SerializedSingleton) in.readObject();
in.close();
System.out.println("instanceOne hashCode="+instanceOne.hashCode());
System.out.println("instanceTwo hashCode="+instanceTwo.hashCode());
}
}
ينتج هذا الكود هذا الإخراج:
OutputinstanceOne hashCode=2011117821
instanceTwo hashCode=109647522
لذا فإنه يدمر نمط الفئة الفردية. للتغلب على هذ scenarion ، كل ما علينا القيام به هو توفير تنفيذ لأسلوب readResolve()
.
protected Object readResolve() {
return getInstance();
}
بعد هذا ، ستلاحظ أن hashCode
لكلا الكائنين هو نفسه في برنامج الاختبار.
اقرأ عن تسلسل Java و فك تسلسل Java.
الاستنتاج
تناول هذا المقال نمط تصميم الفئة الفردية.
واصل تعلمك مع المزيد من دروس Java.