השתמש בסוגים קבועים לקוד בטוח ונקי יותר

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

המושג קבועים

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

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

public void setColor (int x) {...} בטל ציבורי someMethod () {setColor (5); }

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

פיתרון ברור יותר הוא להקצות ערך 5 למשתנה עם שם משמעותי. לדוגמה:

סופי סטטי ציבורי int RED = 5; בטל בציבור someMethod () {setColor (RED); }

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

סופי סטטי ציבורי int RED = 3; גמר סטטי ציבורי int GREEN = 5;

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

אנו יכולים לפתור בעיה זו על ידי יצירת מחלקת צבע סופית:

מעמד ציבורי צבע {גמר סטטי ציבורי int RED = 5; גמר סטטי ציבורי int GREEN = 7; }

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

בטל בציבור someMethod () {setColor (Color.RED); }

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

 setColor (3498910); 

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

ראשית אנו מגדירים מחדש את חתימת השיטה:

 set public color setColor (צבע x) {...} 

כעת מתכנתים אינם יכולים להעביר ערך שלם שרירותי. הם נאלצים לספק Colorחפץ תקף . דוגמה ליישום זה עשויה להיראות כך:

בטל בציבור someMethod () {setColor (צבע חדש ("אדום")); }

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

בטל בציבור someMethod () {setColor (צבע חדש ("היי, שמי טד.")); }

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

מחלקה ציבורית צבע {צבע פרטי () {} סופי סטטי ציבורי צבע אדום = צבע חדש (); סופי סטטי ציבורי צבע ירוק = צבע חדש (); גמר סטטי ציבורי צבע כחול = צבע חדש (); }

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

בטל בציבור someMethod () {setColor (Color.RED); }

הַתמָדָה

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

בשנות ה JavaWorld במאמר הנ"ל, אריק ארמסטרונג בשימוש ערכי מחרוזת. שימוש במחרוזות מספק את הבונוס הנוסף בכך שאתה נותן לך משהו משמעותי להחזיר toString()בשיטה, מה שמבהיר את פלט איתור באגים.

מיתרים, אם כי, יכולים להיות יקרים לאחסון. מספר שלם דורש 32 ביטים כדי לאחסן את ערכו ואילו מחרוזת דורשת 16 ביטים לכל תו (עקב תמיכה ב- Unicode). לדוגמא, המספר 49858712 יכול להיות מאוחסן ב -32 סיביות, אך המחרוזת TURQUOISEתדרוש 144 סיביות. אם אתה מאחסן אלפי אובייקטים עם תכונות צבע, ההבדל הקטן יחסית בסיביות (בין 32 ל 144 במקרה זה) יכול להסתדר במהירות. אז בואו נשתמש בערכים שלמים במקום. מה הפיתרון לבעיה זו? אנו נשמור על ערכי המחרוזות מכיוון שהם חשובים להצגה, אך איננו הולכים לאחסן אותם.

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

מחלקה ציבורית מיישם צבע java.io.Serializable {value int value; פרטי חולף שם מחרוזת; סופי סטטי ציבורי צבע אדום = צבע חדש (0, "אדום"); גמר סטטי ציבורי צבע כחול = צבע חדש (1, "כחול"); סופי סטטי ציבורי צבע ירוק = צבע חדש (2, "ירוק"); צבע פרטי (ערך int, שם מחרוזת) {this.value = value; this.name = שם; } public int getValue () {ערך החזרה; } מחרוזת ציבורית toString () {השם שם; }}

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

מסגרת הסוג הקבוע

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

מחלקה ציבורית צבע מרחיב סוג {מוגן צבע (ערך int, מחרוזת desc) {super (value, desc); } גמר סטטי ציבורי צבע אדום = צבע חדש (0, "אדום"); גמר סטטי ציבורי צבע כחול = צבע חדש (1, "כחול"); סופי סטטי ציבורי צבע ירוק = צבע חדש (2, "ירוק"); }

Colorהכיתה מורכבת אלא בנאי, ובמקרים מסוימים גם נגישים לציבור. כל ההיגיון הנדון עד לנקודה זו יוגדר ויושם במעמד העל Type; נוסיף עוד ככל שנלך. הנה מה Typeשנראה עד כה:

מחלקה ציבורית סוג מיישמת java.io.Serializable {ערך int פרטי; פרטי חולף שם מחרוזת; סוג מוגן (ערך int, שם מחרוזת) {this.value = value; this.name = שם; } public int getValue () {ערך החזרה; } מחרוזת ציבורית toString () {השם שם; }}

חזרה להתמדה

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

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

 hashtable.put (מספר שלם חדש (GREEN.getValue ()), GREEN); 

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

סוגי Hashtable סופיים סטטיים פרטיים = Hashtable חדש (); סוג מוגן (int ערך, מחרוזת desc) {this.value = value; this.desc = desc; types.put (מספר שלם חדש (ערך), זה); }

אבל זה יוצר בעיה. אם יש לנו תת-מחלקה שנקראת Color, שיש לה סוג (כלומר Green) עם ערך 5, ואז ניצור תת-מחלקה אחרת בשם Shade, שיש לה גם סוג (כלומר Dark) עם ערך 5, רק אחד מהם יהיה יישמרו בטבלת ההאשט - האחרונה שתתקנה.

על מנת להימנע מכך, עלינו לאחסן ידית לסוג על סמך הערך שלה, אלא גם המעמד שלה . בואו ניצור שיטה חדשה לאחסון הפניות לסוג. נשתמש ב- hashtable של hashtables. Hashtable הפנימי יהיה מיפוי של ערכים לסוגים לכול תת ספציפי ( Color, Shadeוכן הלאה). הטאבלט החיצוני יהיה מיפוי של מחלקות משנה לטבלאות פנימיות.

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

ריק ריק storeType (סוג סוג) {מחרוזת className = type.getClass (). getName (); ערכי Hashtable; מסונכרן (סוגים) // הימנע מתנאי גזע ליצירת טבלה פנימית {ערכים = (Hashtable) types.get (className); אם (ערכים == null) {ערכים = Hashtable חדש (); types.put (className, ערכים); }} values.put (מספר שלם חדש (type.getValue ()), סוג); }

והנה הגרסה החדשה של הבנאי:

סוג מוגן (int ערך, מחרוזת desc) {this.value = value; this.desc = desc; storeType (זה); }

Now that we are storing a road map of types and values, we can perform lookups and thus restore an instance based on a value. The lookup requires two things: the target subclass identity and the integer value. Using this information, we can extract the inner table and find the handle to the matching type instance. Here's the code:

 public static Type getByValue( Class classRef, int value ) { Type type = null; String className = classRef.getName(); Hashtable values = (Hashtable) types.get( className ); if( values != null ) { type = (Type) values.get( new Integer( value ) ); } return( type ); } 

Thus, restoring a value is as simple as this (note that the return value must be casted):

 int value = // read from file, database, etc. Color background = (ColorType) Type.findByValue( ColorType.class, value ); 

Enumerating the types

הודות לארגון hashtable-of-hashtables שלנו, זה פשוט להפליא לחשוף את פונקציונליות הספירה שמציעה יישום אריק. האזהרה היחידה היא שהמיון, אשר העיצוב של אריק מציע, אינו מובטח. אם אתה משתמש בג'אווה 2, אתה יכול להחליף את המפה הממוינת בטבלאות ה- hasht הפנימיות. אבל, כפי שציינתי בתחילת טור זה, אני עוסק רק בגרסת 1.1 של ה- JDK כרגע.

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