כיצד לנווט בתבנית הסינגלטון הפשוטה באופן מטעה

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

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

דפוס העיצוב של סינגלטון מתייחס לכל החששות הללו. בעזרת דפוס העיצוב של סינגלטון תוכלו:

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

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

עוד על דפוסי עיצוב Java

אתה יכול לקרוא את כל העמודות של תבניות העיצוב של ג'אווה של דייוויד גיר , או להציג רשימה של המאמרים האחרונים של JavaWorld על דפוסי עיצוב Java. ראה " דפוסי עיצוב, התמונה הגדולה " לדיון אודות היתרונות והחסרונות בשימוש בדפוסי כנופיית הארבעה. רוצה יותר? קבל את עלון Enterprise Java שנשלח לתיבת הדואר הנכנס שלך.

דפוס הסינגלטון

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

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

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

כפי שאתה יכול לראות, אין הרבה בדפוס העיצוב של סינגלטון. Singletons שומרים על התייחסות סטטית למופע ה- Singleton היחיד ומחזירים הפניה למופע זה instance()משיטה סטטית .

דוגמה 1 מציגה יישום דפוס עיצוב קלאסי של סינגלטון:

דוגמא 1. הסינגלטון הקלאסי

public class ClassicSingleton { private static ClassicSingleton instance = null; protected ClassicSingleton() { // Exists only to defeat instantiation. } public static ClassicSingleton getInstance() { if(instance == null) { instance = new ClassicSingleton(); } return instance; } }

קל להבין את הסינגלטון המיושם בדוגמה 1. ClassicSingletonהכיתה שומרת הפניה סטטי למופע וחוזר סינגלטון הבודד כי הפניה מן סטטי getInstance()השיטה.

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

שנית, שים לב ClassicSingletonליישום קונסטרוקטור מוגן כדי שלקוחות לא יוכלו להקים מופעים ClassicSingleton; עם זאת, אתה עלול להיות מופתע לגלות שהקוד הבא חוקי לחלוטין:

public class SingletonInstantiator { public SingletonInstantiator() { ClassicSingleton instance = ClassicSingleton.getInstance(); ClassicSingleton anotherInstance =new ClassicSingleton(); ... } }

כיצד המחלקה בשבר הקוד הקודם - שאינו מתרחב - יכולה ClassicSingletonליצור ClassicSingletonמופע אם ClassicSingletonהקונסטרוקטור מוגן? התשובה היא שניתן לקרוא לבנאים מוגנים על ידי מחלקות משנה ועל ידי מחלקות אחרות באותה חבילה . בגלל ClassicSingletonו SingletonInstantiatorנמצאים באותה החבילה (חבילת ברירת המחדל), SingletonInstantiator()שיטות יכולות ליצור ClassicSingletonמקרים. לדילמה זו שני פתרונות: ניתן להפוך את ClassicSingletonהקונסטרוקטור לפרטי כך שרק ClassicSingleton()שיטות יקראו לו; עם זאת, ClassicSingletonלא ניתן לסווג את המשמעות של המשמעות . לפעמים, זה פיתרון רצוי; אם כן, מומלץ להכריז על כיתת הסינגלטון שלךfinal, מה שהופך את הכוונה למפורשת ומאפשר למהדר ליישם מיטבי ביצועים. הפיתרון האחר הוא להכניס את מחלקת הסינגלטון שלך לחבילה מפורשת, כך ששיעורים בחבילות אחרות (כולל חבילת ברירת המחדל) אינם יכולים ליצור מופעים של יחידים.

נקודה מעניינת שלישית בנושא ClassicSingleton: ניתן לקבל מספר מקרים של יחידים אם שיעורים שטעונים על ידי מעמיסי כיתה שונים ניגשים לסינגלטון. תרחיש זה אינו מופרך כל כך; לדוגמא, כמה מכלי servlet משתמשים במעמיסי class שונים עבור כל servlet, כך שאם שני servlets ניגשים לסינגלטון, לכל אחד מהם יהיה מופע משלו.

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

לבסוף, ואולי החשוב ביותר, ClassicSingletonהמחלקה של דוגמה 1 אינה בטוחה בחוטים. אם שני חוטים - נקרא להם חוט 1 וחוט 2 - מתקשרים ClassicSingleton.getInstance()בו זמנית, ClassicSingletonניתן ליצור שני מקרים אם מקדימים את חוט 1 ממש לאחר שהוא נכנס ifלבלוק ובעקבות זאת ניתן שליטה לשרשור 2.

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

בודקים יחידים

במהלך שאר מאמר זה, אני משתמש ב- JUnit יחד עם log4j לבדיקת שיעורי יחיד. אם אינך מכיר את JUnit או log4j, ראה משאבים.

דוגמה 2 מפרטת מקרה מבחן JUnit הבודק את היחיד של דוגמה 1:

דוגמא 2. מקרה מבחן יחיד

import org.apache.log4j.Logger; import junit.framework.Assert; import junit.framework.TestCase; public class SingletonTest extends TestCase { private ClassicSingleton sone = null, stwo = null; private static Logger logger = Logger.getRootLogger(); public SingletonTest(String name) { super(name); } public void setUp() { logger.info("getting singleton..."); sone = ClassicSingleton.getInstance(); logger.info("...got singleton: " + sone); logger.info("getting singleton..."); stwo = ClassicSingleton.getInstance(); logger.info("...got singleton: " + stwo); } public void testUnique() { logger.info("checking singletons for equality"); Assert.assertEquals(true, sone == stwo); } }

מקרה הבדיקה של דוגמא 2 קורא ClassicSingleton.getInstance()פעמיים ומאחסן את ההפניות שהוחזרו במשתנים חברים. testUnique()צ'ק השיטה לראות כי האזכור זהה. דוגמה 3 מראה כי פלט מקרה הבדיקה:

דוגמה 3. פלט מקרה מבחן

Buildfile: build.xml init: [echo] Build 20030414 (14-04-2003 03:08) compile: run-test-text: [java] .INFO main: getting singleton... [java] INFO main: created singleton: [email protected] [java] INFO main: ...got singleton: [email protected] [java] INFO main: getting singleton... [java] INFO main: ...got singleton: [email protected] [java] INFO main: checking singletons for equality [java] Time: 0.032 [java] OK (1 test)

כפי שממחיש הרישום הקודם, המבחן הפשוט של דוגמה 2 עובר בצבעים נאים - שני הפניות היחידות שהתקבלו איתם ClassicSingleton.getInstance()זהות; עם זאת, הפניות הללו הושגו בשרשור אחד. החלק הבא בוחן את שיעורי הסינגלטון שלנו עם מספר אשכולות.

שיקולי רב-הברגה

ClassicSingleton.getInstance()השיטה של דוגמה 1 אינה בטוחה לשרשור בגלל הקוד הבא:

1: if(instance == null) { 2: instance = new Singleton(); 3: }

אם מקדימים חוט בשורה 2 לפני ביצוע ההקצאה, instanceהמשתנה של החבר עדיין יהיה null, ואז חוט אחר יכול להיכנס ifלבלוק. במקרה זה, שני מקרים ייחודיים של יחידים ייווצרו. למרבה הצער, תרחיש זה מתרחש לעיתים נדירות ולכן קשה לייצר אותו במהלך הבדיקה. כדי להמחיש את הרוטה הרוסי הרוסי הזה, הכרחתי את הנושא על ידי הטמעה מחודשת של המחלקה של דוגמה 1 דוגמה 4 מציגה את כיתת היחיד המתוקנת:

דוגמה 4. ערימת הסיפון

import org.apache.log4j.Logger; public class Singleton { private static Singleton singleton = null; private static Logger logger = Logger.getRootLogger(); private static boolean firstThread = true; protected Singleton() { // Exists only to defeat instantiation. } public static Singleton getInstance() { if(singleton == null) { simulateRandomActivity(); singleton = new Singleton(); } logger.info("created singleton: " + singleton); return singleton; } private static void simulateRandomActivity() { try { if(firstThread) { firstThread = false; logger.info("sleeping..."); // This nap should give the second thread enough time // to get by the first thread.Thread.currentThread().sleep(50); } } catch(InterruptedException ex) { logger.warn("Sleep interrupted"); } } }

Example 4's singleton resembles Example 1's class, except the singleton in the preceding listing stacks the deck to force a multithreading error. The first time the getInstance() method is called, the thread that invoked the method sleeps for 50 milliseconds, which gives another thread time to call getInstance() and create a new singleton instance. When the sleeping thread awakes, it also creates a new singleton instance, and we have two singleton instances. Although Example 4's class is contrived, it stimulates the real-world situation where the first thread that calls getInstance() gets preempted.

Example 5 tests Example 4's singleton:

Example 5. A test that fails

import org.apache.log4j.Logger; import junit.framework.Assert; import junit.framework.TestCase; public class SingletonTest extends TestCase { private static Logger logger = Logger.getRootLogger(); private static Singleton singleton = null; public SingletonTest(String name) { super(name); } public void setUp() { singleton = null; } public void testUnique() throws InterruptedException { // Both threads call Singleton.getInstance(). Thread threadOne = new Thread(new SingletonTestRunnable()), threadTwo = new Thread(new SingletonTestRunnable()); threadOne.start();threadTwo.start(); threadOne.join(); threadTwo.join(); } private static class SingletonTestRunnable implements Runnable { public void run() { // Get a reference to the singleton. Singleton s = Singleton.getInstance(); // Protect singleton member variable from // multithreaded access. synchronized(SingletonTest.class) { if(singleton == null) // If local reference is null... singleton = s; // ...set it to the singleton } // Local reference must be equal to the one and // only instance of Singleton; otherwise, we have two // Singleton instances. Assert.assertEquals(true, s == singleton); } } }

Example 5's test case creates two threads, starts each one, and waits for them to finish. The test case maintains a static reference to a singleton instance, and each thread calls Singleton.getInstance(). If the static member variable has not been set, the first thread sets it to the singleton obtained with the call to getInstance(), and the static member variable is compared to the local variable for equality.

הנה מה שקורה כאשר מקרה הבדיקה פועל: החוט הראשון מתקשר getInstance(), נכנס ifלבלוק וישן. בהמשך, החוט השני מתקשר getInstance()ויוצר מופע יחיד. השרשור השני מגדיר את המשתנה חבר סטטי למופע שיצר. החוט השני בודק את המשתנה של האיבר הסטטי ואת העותק המקומי לגבי שוויון, והבדיקה עוברת. כאשר השרשור הראשון מתעורר, הוא גם יוצר מופע של יחיד, אך שרשור זה אינו מגדיר את המשתנה של האיבר הסטטי (מכיוון שהחוט השני כבר הגדיר אותו), ולכן המשתנה הסטטי והמשתנה המקומי אינם מסונכרנים והמבחן שכן השוויון נכשל. דוגמה 6 מפרטת את פלט מקרה הבדיקה של דוגמה 5: