הקדמה
תבנית ה- Singleton ב-Java היא אחת מתבניות העיצוב של קבוצת ארבעת התבניות ונכנסת לקטגוריה של תבניות יצירה. מההגדרה, נראה כי היא תבנית עיצוב ישירה, אך כאשר מדובר ביישום, היא מוצאת חשיבות רבה.
במאמר זה, נלמד עקרונות של תבנית ה- singleton, נחקור לדרכים שונות ליישום של תבנית ה- singleton, ונתרגל כמה מהפרקטיקות הטובות ביותר לשימוש בה.
עקרונות של תבנית ה- Singleton
- תבנית ה- singleton מגבילה את יצירת המחלקה ומבטיחה כי ייצארף רק מופע אחד של המחלקה בכל מכונת ה-Java Virtual Machine.
- המחלקה ה- 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. התחלת בלוק סטטי
בלוק סטטי הפעלת התחלה דומה להפעלת התחלה מוקדמת, עם ההבחנה שהמופע של המחלקה נוצר בתוך הבלוק הסטטי שמספק אפשרות לטיפול בקריסות.
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. מימוש של ביל פיוג' לאובייקט יחיד
לפני Java 5, מודל הזיכרון של Java חוּסַר בשוק. גישות הקודמות נכשלו בתרחישים מסוימים בהם ניסיון של יתר רב של תהליכים לקבל את המופע של המחלקה היחידה בו זמנית. אז ביל פיוג' הציע גישה שונה ליצירת המחלקה היחידה באמצעות מחלקת עזר סטטית פנימית. הנה דוגמה ליישום המחלקה היחידה לפי ביל פיוג':
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
של שני המופעים אינם זהים, מה שמפר את תבנית ה- singleton. הרפלקציה היא כלי עוצמתי מאוד ומשמש בהרבה מסגרות עבור פרימות כמו Spring ו-Hibernate. המשך את למידתך עם מדריך רפלקציה ב-Java.
7. Singleton באמצעות Enum
כדי להתמודד עם המצב הזה בעזרת רפלקציה, Joshua Bloch מציע להשתמש ב-enum
כדי ליישם את תבנית העיצוב singleton כיוון ש-Java מבטיחה שערך כלשהו של enum
מיוצר רק פעם אחת בתוכנית Java. מאחר שערכי Enum ב-Java נגישים בצורה גלובלית, כך גם ה- singleton. החסרון הוא שסוג ה-enum
קצת קשוח (לדוגמה, הוא לא מאפשר את התחלת הפעלה).
package com.journaldev.singleton;
public enum EnumSingleton {
INSTANCE;
public static void doSomething() {
// לעשות משהו
}
}
8. סריאליזציה ו- Singleton
לעיתים במערכות מבוזרות, אנו צריכים ליישם את ממשק ה־`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();
// deserialize מקובץ לאובייקט
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
כך שהוא משחק עם התבנית הסינגלטונית. כדי להתגבר על סצנריו זה, הכל שעלינו לעשות הוא לספק את המימוש של שיטת `readResolve()`.
protected Object readResolve() {
return getInstance();
}
לאחר מכן, תשימו לב שה־`hashCode` של שני המופעים זהה בתוכנית הבדיקה.
קראו עוד על [סדרתיות Java](https://he.wikipedia.org/wiki/Serialization_%D7%91-Java) ו[התאריך של יישות Java](https://he.wikipedia.org/wiki/Java_serialization).
מסקנה
מאמר זה כיסה את תבנית העיצוב של סינגלטון.
המשך את למידתך עם עוד [מדריכי Java](https://he.wikipedia.org/wiki/Java).