2014-05-20

תבנית עיצוב: Null Object

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

יש לנו מסורת ב SAP, להעביר פעם בשנה קורס "תבניות עיצוב" (Design Patterns) לעובדים החדשים. חלקם מכירים את הנושא - אבל תמיד יש מה לשפר. אני כותב פוסט זה תוך כדי הכנה של החלק שלי בקורס.
האמת? יש לי הסתייגות פנימית קלה מנושא תבניות העיצוב: תבניות עיצוב הוא תחום שעלול לגרום ליותר נזק מתועלת - כאשר מנסים "לדחוף כמה שיותר תבניות עיצוב במטר מרובע", וזה יכול לקרות הרבה.
מצד שני, כשמתבגרים ונרגעים ניתן להשתמש בהן בצורה מאוזנת ומועילה. חוץ מזה: זה נושא שאנשים רוצים לדעת היטב - ומי אני שאמנע מהם? האם אני הייתי מוכן לוותר על להכיר את הנושא? בטח שלא!

בקורס, אנו מנסים להדגיש את הנקודות החשובות הבאות:
  • תבניות עיצוב כשפה רק עבור התקשורת הפשוטה, כדאי ללמוד תבניות עיצוב: "ה class הזה הוא בעצם כמו Strategy" יכול להגיד המון למי שמכיר את ה Pattern או לגרור דיון של כמה דקות - למי שלא.
  • תבניות עיצוב כתיקון למצב בעייתי, או מה שנקרא Refactoring To Patterns: כדאי ומומלץ להימנע משימוש בתבניות עצוב במחשבה על "מצב עתידי", קרי "יהיה יותר קל לטפל גם במספר רב של אובייקטים אם נשתמש כאן ב Composite". חכו שיהיה מצב בו יש מספר רב של אובייקטים - ורק אז עשו Refactoring ל Composite.
    ההמלצה היא לזהות צורך מיידי ש Pattern יכול לפתור, או לסירוגין smell בקוד שנובע מאי-שימוש ב Pattern ורק אז לעשות refactoring ולהכניס את ה Pattern.

אם "smell" נשמע עבורכם סינית - מדובר על code smells, שהם חלק מהשפה של טכניקת ה refactoring.



דיי מדהים איך חברת קוקה קולה לוקחת מושג כ"כ שלילי בעולם הגברי ("אפס") ומנסה למכור אותו כמותג נחשק - לגברים.
האם יכולים לשחק לנו בשכל עד כדי כך? מה עם קבעון גברי טיפוסי?!



קצת על null בג'אווהסקריפט


ג'אווהסקריפט הופכת לשפה משמעותית עבורנו - ולכן הדוגמאות שאספק יהיו בעיקר מעולם זה. לא מעניין אתכם javaScript או ה nulls שלה - דלגו ל section הבא.

למה "nulls" ולא "null"? כי יש כמה כאלו בג'אווהסקריפט. תלוי את מי שואלים, יש שניים או שלושה, אני אציג את נקודת המבט המחמירה:
  • יש ערך "null", שמשמעותו היא "נקבע למשתנה זה ערך ריק / חסר משמעות". בניגוד לג'אווה זהו איננו ערך ברירת המחדל למשתנים.
  • יש ערך "undefined" ששמו דיי מבלבל: נשמע שהוא מתאר משתנים שלא הוגדרו - אך בעצם הוא נקבע כערך ברירת מחדל למשתנים שהוגדרו אך לא נקבע להם ערך. שם מוצלח יותר היה יכול להיות unassigned.
  • יש מצב בו לא הוגדר משתנה. אפשר לקרוא לו "בום!": ניסיתם לגשת למשתנה, אפילו בזהירות? - ייזרק error. שם שהייתי מעדיף לכזה משתנה הוא undefined, אבל השם כבר תפוס. זה לא בדיוק null value, אבל הוא דורש התגוננות דומה (ונוספת).

בואו נראה דוגמת קוד:



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


מקור

כלומר: כדי לא לחטוף "NullPointerException" וגם / או "UndefinedPointerException" (בקריצה לג'אווה) האם עלינו לבדוק כל משתנה שאנו מקבלים - פעמיים בפני אפשרות ל null?

לשמחתנו, אנו יכולים לנצל את "הגמישות" בבדיקת הטיפוסים בג'אווהסקריפט מדי פעם כדי לבדוק את שני ערכי ה null בפעם אחת. התחביר המועדף עלי הוא שימוש ב ! (או !!, כאשר השמת הערך כפי שהוא לא אפשרית / ברורה):

נראה עקבי.
אבל... מה קורה עם משתנה z שלא הגדרתי (כלומר הוא: truly undefined)? 
בואו ננסה:

אולי סוג אחר של הגנה?



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


אוי ואבוי? האם צריך לעשות בדיקה כזו בכל מקום בקוד?
בכלל לא... ארגומנט של פונקציה תמיד יהיה מוגדר, לפחות undefined.
יש לעשות בדיקה מקיפה זו כל פעם שניגשים (פעם ראשונה?!) למשתנה במרחב הגלובלי.
הערה: גישה למשתנה גלובאלי בצורה  <משהו>.window היא דווקא בטוחה בפני ReferenceError. תודה לקורא שתיקן אותי.

אז האם יש 2 או 3 ערכי null בג'אווהסקריפט? 
  • אולי אלו 2: null ו undefined
  • אולי אלו 2: null/undefined ו truly undefined
  • אולי אלו 3.
 תלוי את מי שואלים....







מה הבעיה עם null?


ראשית, ש"אבא" שלו - פשוט לא אוהב אותו.
טוני אוהר'ה, מי שהמציא את הקונספט בשפת ALGOL איפשהו בשנות השישים, אומר שזו פשוט הייתה עצלנות. "לא היה לי זמן למצוא פתרון יותר אלגנטי באותה התקופה". הוא קורא להחלטה ליצור את null טעות, ולא סתם טעות: "טעות מיליארד הדולר שלי" (טריליון דולר - בתרגום למונחים ראליים של היום). איזה נזק לתעשייה כולה! ומאז ששפת C החליטה לאמץ את הרעיון - אבדה הדרך חזרה...

מה הבעיה עם null?
איזה Exception מעיף לכם את התוכנה, בשפה ה strongly types עם הקומפיילר ההדוק שלה (קרי: Java), הכי הרבה פעמים?
רגע... תנו לי לנחש (לתקשר) ... האם זה אולי NullPointerException?

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

ב List אנחנו זוכרים לטפל: זהו ה common scenario - מה שצפוי, רצוי וקל לחשוב עליו.
את ה null אנחנו מפספסים הרבה יותר. והיכן אנו מגלים את הבעיות הללו? בתוך ה IDE? - לא כ"כ. אולי בזמן בדיקות, אולי בשרת האינטגרציה... אולי ב production ... ואולי אצל הלקוח.

אם כ"כ הרבה מפתחים שוכחים אותו דבר - כל כך הרבה פעמים, אולי הבעיה היא בשפה / כלים ולא במתכנתים?!


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

הנה דוגמה מקוד של jQuery, ספרייה שעפו בה המון referenceErrors לאורך פיתוחה. באופן כמעט אקראי בחרתי בפונקציה add שרושמת אירוע על אלמנט בדום בעקבות קריאה ל (...)on.$. היא לא מקבלת את הפרמטרים כמו שהם - פונקציית on עושה המון בדיקות קלט, כולל nulls למיניהם. שימו לב עדיין מה יש בה:


סימנתי בצהוב כל מיני בדיקות null למיניהן שעדיין יש לעשות, למרות ההגנה של פונקציית on.
מה זה?? האם jQuery הוסיפה טיפוס null משלה בשם "strundefined"? 
לא לדאוג. זהו פשוט קיצור של "typeof undefined" - כנראה בגלל שיש הרבה בדיקות כאלו.

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



"מראה מראה שעל הקיר, מי השפה עם ה Null המזיק-פחות, שאותו יש להוקיר?"
מקור: הבלוג הטכני של לוסידצ'ארטס https://www.lucidchart.com/techblog/



מה עושים?


וואהו! הצלחתי למלא את הפוסט דיי מהר, ובלי לשים לב. ככה זה ארכיטקטים: דברנים!
ישנן שפות מעטות (ואמיצות!) שהחליטו להיפטר מ null. אני שמעתי על Cyclone (דיי נדירה) ועל Haskell [א].
הייתי פעם בהרצאה של מישהו שהציע תחביר בו מצב ברירת המחדל הוא שאובייקט לא יכול להיות null. אם פונקציה יכולה להחזיר null, חובה עליה להכריז על ערך ההחזרה עם prefix של סימן-שאלה (כמו nullable object ב #C). למשל: 

public ?ListItem getChild(....);

או

private ?List calcItems(...);


אני מוצא את הפתרון הזה מאוד אלגנטי - אך אינני מכיר שפה שאמצה אותו.

פתרון אחר הוא Design Pattern בשם Null Object שאומר: במקום להחזיר null, עם אופציה ברורה לשבור את הקוד במקומות אחרים שלא מוכנים לכך - החזר אובייקט ("Null Object") שיתאר מצב null-י, מבלי לחרב את המערכת. על האובייקט הזה לדמות בצורה הטובה ביותר שניתן (אך סטנדרטית) מצב של "לא ידוע" / "אין".

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

class Employee {

  public final static NULL_EMPLOYEE = new Person('<none>', [], 0, '<empty record>');

  Employee (String name, String[] expertise, int age, String desc) {
        …
  }
}

הרעיון הוא שבמקום להחזיר null, אני אחזיר את Employee.NULL_EMPLOYEE, שתאפשר לקוד להמשיך לרוץ - מבלי לשבור אותו.

אם יש לולאה שרצה על מערך מומחיויות העובד - ריצה על מערך ריק (שמגיע מה null object) לא תשבור אותו, אין בעיה. הקוד ימשיך בד"כ gracefully. פה ושם יהיו מסכים מוזרים שמדברים על עובד ששמו "<none>" - אך זה הרבה יותר טוב מהודעת שגיאה לא ברורה, והמתנה ל patch הקרוב.

את תבנית העיצוב של Null Object מתארים באופן פורמאלי בצורה הבאה:


מקור: AndyPatterns

אפשר בהחלט לא להשתמש ב Null Object אחד בעל משמעות כללית, אלא לייצר כמה, עם משמעות סמנטית עשירה יותר, ועם התנהגויות שמתארות בצורה טובה יותר את ה "null behavior" (או "do nothing") של אותו האובייקט. למשל במערכת העובדים:



פתרון אחר, אולי "פתרון חצי-דרך", הוא בשלב מוקדם ככל האפשר - להחליף ערכי null בערכים סבירים אחרים - עבור שאר ה flow. זכרו שאם בדקתם בפני null נקודתית אז פתרתם את הבעיה של היום, אבל לא את התיקון / תוספת של "עוד-שעה". פתרון זה (שאני קורא לו "inline null object") הוא לא אידאלי, אבל מהיר וקצת יותר ארוך טווח מטיפול נקודתי ב null. כמו פלסטר עם דבק חזק במיוחד.

הוא נראה כך (בכתיבה המקוצרת שלו בג'אווהסקריפט):


init: function(options) {
    this.user = options.user | { name: “unkown”, items: []};
},


עדכון יוני 2014:

אפל הודיעה על שפת Swift, שתחליף עם הזמן את Objective-C לפיתוח OS X/iOS ומה שאומר שצפוי לה שימוש נרחב. מה הקשר? ב Swift משתנים לא יכולים להיות nil אלא אם הוגדרו כ Option Type (מה שנקרא גם "Maybe") - שבתחביר השפה זהו סימן שאלה בסוף - למשל ?Int. פתרון מאוד אלגנטי לטעמי לעניין ה null!
התחביר דוגמה מאוד ל nullable types ב #C - תחביר המאפשר nulls ב primitives. כלומר - עשו שם רק את החצי הקל של העבודה...


סיכום 

  • התרגלנו (בעצם: נולדנו) לעולם בו פונקציות עלולות להחזיר null בכל רגע, וללא התראה.
  • null יכול להפתיע בכל מקום בקוד - מה שהופך אותו לבעיית תחזוקה קשה.
  • NullObject יכול לעזור.
    • לצמצם הפתעות.
    • להוסיף סמנטיקה עשירה יותר למצב ה null.
  • NullObject הוא לא מושלם: הוא דורש מעט יותר עבודה. מומלץ לשימוש באופן מקיף במערכת שמתקרבת לשלב התחזוקה (לא MVP).
הפוסט נכתב בזמן שיא (לפוסט מסוגו) של שעתיים וחצי בערך. סליחה מראש במידה ויש תקלות הגהה הגעה הגאההה.


שיהיה בהצלחה!


----

[א] יש לה משהו שנקרא maybe שאמור לטפל בצורה פשוטה במקרים לא צפויים. אני לא יודע איך הוא בדיוק עובד.



6 תגובות:

  1. אנונימי21/5/14 18:27

    תודה רבה !! מענין מאד ומעורר מחשבה.
    רק הערה קטנה בנוגע להערתך על javascript:
    "יש לעשות בדיקה מקיפה זו כל פעם שניגשים (פעם ראשונה?!) למשתנה במרחב הגלובלי, קרי .window."
    לדעתי אם ניגשים ל- .window אין צורך בבדיקה הארוכה אך אם רוצים לגשת ישירות ל- ולא באמצעות ה-window יש להוסיף את הבדיקה הארוכה.
    לדוגמא - window.xxxxxxx לא יזרוק שגיאה אך - xxxxxxx יפול.
    תכל'ס זה לא כ"כ משנה לרעיון העיקרי של הפוסט - סתם הערה שולית... :)
    שוב תודה, היה מענין!

    השבמחק
  2. אתה בהחלט צודק. אתקן.

    תודה על ההערה!

    ליאור

    השבמחק
  3. אנונימי26/6/14 07:08

    הי ליאור.
    שפת Ceylon של רד הט מיישמת nullable ref types . כמו כן, אף דיון ב nullable לא שלם בלי maybe monad, ע"ג למ(ב)דות. ממליץ בחום להרחיב את הפוסט

    יואב

    השבמחק
    תשובות
    1. היי אנונימי,

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

      תודה,
      ליאור

      מחק
  4. בג'אווה יש מימוש חלקי לגישה הזאת.
    אני מקפיד להחזיר Collections.emptyList במקום null

    השבמחק