סינגלטון – Singleton Design Pattern

התחנה הבאה שלנו ברחבי ה-design patterns
היא האופציה ליצור אובייקט יחיד ומיוחד, שיש לו רק מופע אחד, לא משנה מה.
הרי הוא הסינגלטון – Singleton Design Pattern
כמו שאר הפוסטים בסדרה הזאת, גם הפוסט הזה מובסס על הספר הנהדר Head First Design Patterns

במקור זה היה אחד הפוסטים הכי מעוררי מחלוקת בדף בפייסבוק
כי סינגלטון מעורר מחלוקות מאז שחשבו עליו.
יש שיגידו שהוא anti pattern, יש שיגידו שהוא חיוני.
לרובנו יש דעה עליו, כי אנחנו פוגשים אותו כבר בלימודים או בראיונות עבודה.
אני אנסה היום להרחיב קצת על מה זה בכלל, ומה הבעיות בו

חזרה להתחלה

לפני שנגיע ממש לסיגלטון, בואו נחזור קצת על הבסיס
איך אנחנו יוצרים אובייקט חדש בקוד?

new MyObject();

ומה אם אובייקט אחר רוצה ליצור את MyObject?
הוא יכול לקרוא לאותה שורה בדיוק.
בעצם, כאשר יש לנו מחלקה, אנחנו תמיד נוכל לאתחל אותה לפחות פעם אחת
בהנחה ויש לה בנאי פומבי (public)

אבל מה קורה אם למחלקה אין בנאי פומבי?
ובכן במקרה הזה, רק מחלקות שיושבות באותה חבילה (package) יכולים לאתחל אותה

אבל מה יקרה אם יהיה לנו משהו כזה:

public MyClass {

  private MyClass() {}
}

ובכן במקרה כזה אי אפשר לאתחל את המחלקה MyClass
כי אף אחד לא יכול לגשת לבנאי שלה.
חוץ מאשר MyClass עצמה

אבל אין בזה הגיון, איך אני יכול לגשת לבנאי
בלי ליצור מופע של המחלקה קודם?
ובכן יש לנו פתרון גם לזה

public MyClass {

  public static MyClass getInstance() {}
}

אם נשתמש במתודות סטטיות, אז אנחנו יכולים לקרוא לMyClass בלי לאתחל אותה
בצורה הזאת:

MyClass.getInstance()

ואם נאחד את שני הדברים נקבל קוד כזה

public MyClass {

  private MyClass() {}

  public static MyClass getInstance() {
    return new MyClass();
  }
}

ובכך מימשנו את הסיגנלטון הראשון שלנו

סינגלטון קלאסי

אז בואו נסתכל רגע על המימוש הקלאסי של סיגלטון, וננסה להבין את כל הניו אנסים שבו

קודם כל נשים לב ששינינו את שם המחלקה שלנו להיות Singleton.
ויש לנו שדה סטטי של המחלקה, שנקרא uniqueInstance
שהוא בעצם המצביע למופע היחידי של המחלקה Singleton.
הבנאי שלנו הוא פרטי, private
ויש לנו את המתודה getInstance שהיא אחראית ליצור מופע רק פעם אחת
ולהחזיר את המופע הזה החוצה

אם נתמקד רגע רק במתודה getInstance
אז נראה שכאשר קוראים למתודה בפעם הראשונה, getInstance יהיה null
כי אף אחד עוד לא שם לו ערך
ובמקרה הזה אנחנו ניכנס לתוך התנאי, וניצור מופע חדש של Singleton
שיוחזק על ידי ה-uniqueInstance.
ובסוף נחזיר אותו
בפעם הבאה שהפונקציה הזאת תיקרא, uniqueInstance כבר לא null ולכן ישר נחזיר את הערך

מפעל שוקולד

כמנהגנו בקודש, נסתכל על דוגמה קצת יותר מורכבת כדי להבין דברים נוספים לגבי ה-Singleton
אבל אני ממליץ בחום, לפני שממשיכים, שתבינו רגע איך סינגלטון עובד בצורה הבסיסית

כולם יודעים שכל המפעלים המודרנים נשלטים על ידי מחשבים
ומפעלי שוקולד לא שונים במובן הזה.
ספציפית, במפעל שוקולד יש דוד אשר ממיס את השוקולד יחד עם החלב
ואחרי זה הוא מעביר את החומר המומס על בהמשך הפס כדי שייצרו מזה טבלאות שוקולד. יאמי

הנה דוגמה לאיך היינו יכולים למדל בוילר כזה

אנחנו מתחילים את הקוד כאשר הבוילר שלנו הוא ריק, ולא רותח
כאשר אנחנו ממלאים את הבוילר, הוא חייב להיות ריק, ואז אנחנו מעדכנים את הפלאגים שלנו
כדי לרוקן את הבוילר, הוא חייב להיות מלא קודם, ואחרי שהפועלה התבצעה, אנחנו מעדכנים את הפלאג
כדי להרתיח את הבוילר, אנחנו צריכים שהוא יהיה מלא ושהוא לא יהיה רותח כבר.

כפי שאתם יכולים לראות, אנחנו מנסים ששום דבר רע לא יקרה לבוילר שלנו.
אבל מה יקרה אם נצליח ליצור שני אובייקטים של בוילר?
דברים פחות טובים יכולים לקרות. ולכן אנחנו נרצה להפוך את הקלאס הזה להיות סינגלטון


שימו לב שהוספנו משתנה סטסטי, שהוא private שמחזיק את ה-instance שלנו
ובנוסף הוספנו מתודה אשר תחזיר את המופע הזה.

הגדרת סינגלטון

אז עכשיו שיש לנו מושג איך מממשים סינגלטון בסיסי
הגיע הזמן להגדיר אותו בצורה רשמית

הסיגנלטון מוודא שלמחלקה יש מופע יחיד בלבד,
ונותן נקודת גישה גלובלית אל המופע הזה


אין פה שום דבר חדש או מפוצץ, אבל ננסה לפרק את זה קצת:

מה קורה פה באמת? אנחנו לוקחים מחלקה, ונותנים לה לנהל את המופע היחיד שלה.
בנוסף אנחנו מונעים מכל מחלקה אחרת ליצור מופע של המחלקה הזאת.
על מנת לקבל מופע, חייבים לעבור דרך המתודה getInstance

יוסטון יש לנו בעיה

למרות כל ההגנות שרשמנו בקוד, הבוילר אכזב אותנו.
הוספנו סינגלטון, על מנת להגן על הקוד,
מישהו עדיין הצליח לקרוא ל-fill, למרות שהוא היה כבר מלא ורותח.
איך לעזאזל זה קרה?

דבר כזה יכול לקרות אם יש לנו שני threads.
כל אחד מהם מריץ את הקוד הבא:

ChocolateBoiler boiler = ChocolateBoiler.getInstance();
boiler.fill();
boiler.boil();
boiler.drain();

אבל איך זה דופק לנו את הבוילר?
ובכן ככה:

הערך של uniqueInstance
null
null
null
null
<object 1>
<object 2>
ת'רד 2
getInstance()
if (uniqueInstance == null)
uniqueInstance = new ChocolateBoiler();
return uniqueInstance;
ת'רד 1
getInstace()
if (uniqueInstance == null)
uniqueInstance = new ChocolateBoiler();
return uniqueInstance;


במקרה הזה הצלחנו להוציא שני מופעים שונים של המחלקה ChocolateBoiler
למרות שאנחנו משתמשים בסינגלטון.

התמודדות עם מקביליות

יש הרבה דרכים איך להתמודד עם מקביליות.
כל שפה והפתרונות שלה, אבל כיוון שאני כותב את הפוסטים הללו בג'אווה,
אני אציג את הדרך בה ג'אווה פותרת אותם

הפתרון שלנו הוא שימוש ב-synchronized.
אני לא אכנס פה לכל הפרטים, זה אולי יהיה בסדרת פוסטים אחרת
אבל בקצרה, ברגע ששמים את המילה הזאת בחתימה של פונקציה או מתודה
אז ג'אווה לבד ידאג שרק thread אחד יכול להיכנס ולבצע אותה בכל זמן
בצורה הזאת, הבעיה שראינו מקודם תפתר.


חשוב לשים לב, ש-synchronized אכן יפתור לנו את הבעיה
אבל הוא מאוד מאוד יקר.
ואם נתבונן היטב, הבעיה פה היא רק כאשר אנחנו מנסים ליצור אובייקט חדש של ChocolateBoiler.
אחרי שהוא נוצר כבר, לא יכולות להיות עוד בעיות מקביליות שיצליחו ליצור עוד מופעים.
ולכן אנחנו מגבילים את הביצועים שלנו עבור מקרה מאוד ספציפי שיכול לקרות פעם אחת בלבד

האם אפשר לשפר את זה?

עבור רוב האפליקציות בג'אווה שיצא לי לראות,
יש לנו צורך לוודא שכל סינגלטון שאנחנו נממש, יצטרך להיות עמיד למקביליות.
אז איך אנחנו יכולים לעשות

לא נעשה כלום
במקרה הזה, אם העלויות ביצועים של synchronized הן לא משמעותיות
אז אנחנו נשאיר את הקוד ככה.
זה מאוד קריא, ואפקטיבי, אבל אתם צריכים להיות עם האצבע על הדופק


אנחנו יכולים ליצור את המופע שלנו בצורה ישירה:
אם כן יש השפעה על הביצועים, אנחנו יכולים לוותר על יצירת האובייקט בפונקציה getInstance
ובמקום זה, ליצור אותו בצורה ישירה


בצורה הזאת ה-JVM יצור לנו מופע יחיד של Singleton, כאשר הוא טוען את המחלקה.

שימוש ב-volatile יחד עם synchronized
במקרה הזה אנחנו נבדוק אם כבר אתחלנו את המשתנה,
ורק אחרי זה אנחנו נשתמש ב- synchronized על מנת ליצור אותו.
זה נקרא double-checked locking

במקרה הזה, אנחנו משתמשים גם ב-volatile
שהיא מוודאת שכל ה-threads ינהלו את uniqueInstance בצורה הנכונה.
עוד פרטים אפשר לראות כאן

הפתרון המושלם

כשכתבתי את הפוסט המקורי, העירו לי ששכחתי את הפתרון המושלם עבור סינגלטון בג'אווה
ואכן אנשים צדקו, יש פתרון מושלם, שפותר לנו את כל הבעיות:
השימוש ב-Enum.

זה נראה ככה:


בתכלס זה פותר לנו את כל הבעיות.
המחלקה סינלטון במקרה הזה נראה ככה:

public enum Singleton {
  UNIQUE_INSTANCE;
}

אז למה עשינו את כל המסע הזה? והראיתי לכם את הפתרונות המורכבים של מקביליות?
המטרה שלי הייתה שתבינו איך באמת סינגלטון עובד, וגם הרבה אנשים לא כותבים בג'אווה
ולהם אולי אין את הפתרון הזה.

סיכום

בפוסט הזה ראינו שסינגלטון מוודא שיהיה אפשר ליצור רק מופע יחיד של מחלקה
הסינגלטון מספק גישה גלובלית למופע הזה.
בג'אווה, אנחנו מחביאים את הבנאי כ-private, ומשתמשים במתודה סטאטית על מנת לקבל את המופע
ראינו גם שיש לנו בעיות עם מקיבליות בקוד הזה
וראינו כמה שיטות איך לפתור אותן
ובסופו של דבר, ראינו שבג'אווה לפחות, השיטה הטובה ביותר היא להשתמש ב-enum.

את כל הקוד אתם יכולים למצוא כאן

אם הגעתם על לפה, אשמח שתשתפו את הפוסט על מנת לשפר את התפוצה של הדף
וכמו תמיד, אשמח לקבל כל הערה, הארה או שאלה שיש לכם.

השאר תגובה

Scroll to Top