מה זה singleton ולמה לא כדאי להשתמש בו?


זהו פוסט קצר שמטרתו להציג את תבנית העיצוב singleton ומדוע אינני ממליץ להשתמש בה.
הפוסט הינו חלק מסדרה שמטרתה לדבר על כתיבת קוד רע ו-anti-patterns בתחום עיצוב התוכנה.

מה זה סינגלטון

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


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


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

למה לא להשתמש בו

סינגלטון היא תבנית פשוטה מאוד, אבל האמת היא שאין מתנות חינם.

הנה 4 סיבות (מהקלה לחמורה) לכך שאני חושב שלא כדאי להשתמש בסינגלטון:

בזבזנות בזיכרון
שימוש בסינגלטון הוא אינו יעיל בהיבטי זיכרון כשמדובר על סביבות בהן יש garbage collector. במקרים בהם משתמשים בסינגלטון בשפות כאלו (כמו java ופייתון), השפה מגדירה את האובייקט כחשוב (מכיוון שיש אליו רפרנס סטטי) ולא משחררת אותו מהזיכרון. כשמדובר על אובייקט סינגלטון גדול זה עלול להיות בזבזני.

בעיות multi-threading
סינגלטון הוא בעייתי גם מכיוון שאינו thread safe (לפחות לא בתצורתו הטבעית).
הכוונה היא שכשרצים שני threadים במקביל, יכולים להיווצר שני מופעים של אובייקט הסינגלטון.

דמיינו thread a אשר מגיע ל if של get_instance ורואה שכרגע לא אותחל instance, מכיוון שעוד לא אותחל, הוא נכנס לתוך ה if. רגע לאחר מכן הthread הושהה.

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


בעיות ב Unit Testing
דמיינו את המקרה הבא: יש לנו פונקציה שנקראת sort, במחקלה Users שמביאה בתוכה אובייקט סינגלטון מסוג DB.


כעת נרצה לבדוק אותה בבדיקת יחידה. אבל, מכיוון שמדובר על בדיקת יחידה, איננו מעוניינים לבצע חיבור אמיתי לDB. מכיוון שהמחלקה Users משתמשת ב singleton היא כבולה לשימוש באותו סוג אובייקט שאותחל ולא ניתו יהיה להחליף אותו באובייקט mock (בדרך נורמלית).

singleton משפיע על הסטנדרט של הקוד שלנו
בני האדם עצלנים מטבעם, זה בא לידי ביטוי אפילו עוד יותר כשאנחנו כותבים קוד. פעמים רבות יצא לי לראות (גם ב code base עליו אני עובד) שאנשים עושים שימוש יתר בפעולה get_instance של הסינגלטון ובכך יוצרים תלות חזקה בין המקום בו השתמשו בו לבין האובייקט שאתחלנו.

יש לנו נטייה להיות חוזי עתידות, ולהחליט שלא נצטרך עוד אובייקטים מאותו סוג ושלא נרצה להחליף את האובייקט לצורך בדיקה, אבל whatever can go wrong will go wrong, וסביר מאוד שיום אחד נחליט ליצור מחלקה נוספת שדורסת פעולות של אותה מחלקה (או כל דבר בסגנון) וכשזה יקרה אנחנו נמצא את עצמנו מנסים לפרום תלויות נוראיות שאנחנו יצרנו באותו אובייקט.

מה לעשות במקום

באופן כללי, כעקרון מנחה ולפי עקרון DIP, אני משתדל אף פעם לא לקרוא ליצירת אובייקט מתוך מחלקה או פונקציה אלא תמיד להזריק אותו מבחוץ באמצעות dependency injection. אם נחזור לדוגמה המדוברת של המחלקה Users, זה יראה כך:


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

סיכום

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



סינגלטון כמו שאר ה design pattern הוא פתרון לבעיית דיזיין. כשאנחנו שוקלים להשתמש בו עלינו לחשוב איזו בעיה בדיוק אנחנו מנסים לפתור ולנסות לפתור אותה בדרך שיכולה להיות אולי מעט קשה יותר אך כנראה תהיה גם נכונה יותר (שימוש ב mutex לדוגמה)

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

נתראה בפוסט הבא :)

תגובות

פוסטים פופולריים מהבלוג הזה

מדריך: כיצד לבחור ואיך להתקין Visual Studio 2017

בדיקות לתשתית האוטומציה שלנו | סוגי הבדיקות

5 סיבות להיות מפתח אוטומציה