2012-11-02

מחלקות ב javaScript

תלמיד: מאסטר, כיצד כותבים מחלקה Class בג׳אווהסקריפט?
מאסטר: ג׳אווהסקריפט היא דינמית, כחומר ביד היוצר - עליך להגדיר בעצמך כיצד נראה Class.
תלמיד: בעצמי? אין איזו המלצה מקובלת? Best Practice? Pattern?
מאסטר: יש הרבה. מאסטר רזיג אוהב את אופצייה #1, מאסטר כץ אוהב את אופציה #3. אם יש לך מערכת שדורשת אלף Classes, בג׳אווהסקריפט אתה יכול להגדיר כל Class במערכת בצורה ייחודית.
תלמיד: כל Class בצורה ייחודית?? כיצד ניתן לתחזק קוד שכזה? כיצד אפשר בכלל לקרוא אותו?
מאסטר: אל תהיה טיפש. זה שהשתמשתי באמירה כלשהי לא אומר שאתה באמת צריך לרוץ לעשות את זה.
תלמיד: הא...


שייך לסדרה מבוא מואץ ל JavaScript ו jQuery


מוטיבציה
בג׳אווהסקריפט אין מחלקות (Classes בג'אווה / NET.), אולם רוב המפתחים המקצועיים בג׳אווהסקריפט מדמים מחלקות. לא ניתן להאשי אותם: מחלקות הן דבר שימושי.

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

בעצם, אולי זו לא רק שאיפה של "מפתחים חדשים". ניתן לראות שהקהילות של שפות Ruby וג׳אווהסקריפט מתבגרות בשנים האחרונות ונותנות דגש רב יותר לרעיונות כמו הכמסה (Encapsulation), ניהול תלויות, מודולריות וממשקים - כל אותם דברים שמפתחי ג׳אווה או דוטנט קלאסיים עסקו בהם מאז ומעולם (ניתן לומר ״לכאורה״, ניתן לומר ״קצת בהגזמה״).

ישנן עשרות צורות אפשריות לתאר מחלקות בג׳אווהסקריפט. ניתן לחלק אותן בבירור ל-2 משפחות:
  • מחלקות בעלות אכיפה נוקשה של הכמסה (מבוססות closure).
  • מחלקות יעילות מבחינת צריכת זיכרון ואפשרויות הורשה (מבוססות prototype).
שילוב אלגנטי שנותן ״גם וגם״ - עדיין לא ראיתי.

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


הנה המחלקה בשפת Java אותה ארצה לדמות בג'אווהסקריפט:

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


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


תיאור זה של מחלקה הוא ידוע מאוד ונקרא ״revealing module״, המבנה הבסיסי אמור להיות מוכר - עסקנו בו בפוסט הקודם בסדרה. זו גם הצורה בה פרויקט jQuery ממליץ לכתוב מחלקות.
יצירת instance חדש נעשה ע"י קריאה ל Factory Function (המקבילה הג'אווהסקריפטית ל Factory Method). בכדי "לסמן" את ה Factory Function - אני ממליץ לקרוא לה createXXX.

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



הנה צורה אחרת, פחות נפוצה. כל member שמוגדר עם "this" הוא ציבורי וכל member שמוגדר עם var הוא פרטי.
אני אישית מחבב אותה יותר: הקוד קצר יותר. הניראות (visibility) מצוינת באותו המקום בו מוגדר ה member/method כך שבמבט חטוף אני יכול לדעת מה private ומה לא (כמו בג׳אווה - המלים השמורות private ו public). אני לא צריך לגלול לתחתית המחלקה על מנת לדעת מהי הניראות, כמו בדוגמה הקודמת
.
בעת יצירת instance, יש לזכור להשתמש במילה השמורה new. הקונבנציה המקובלת היא לקרוא לבנאים באות ראשונה גדולה (Capital Letter). זהו חיסרון מסוים יחסית לצורה הקודמת, מכיוון שיש להיות מודעים לצורה בה המילה השמורה this עובדת בג'אווהסקריפט. פוסט המשך בסדרה מסביר את הנושא לעומק.

סכנה מוחשית במבנה זה הוא החלפה של הנראות private <=> public. מכיוון שגם הגישה למתודה/משתנה היא שונה ע"פ הנראות שלו (this.xx או xx) אזי סביר שבעת שינוי הנראות נשכח לתקן חלק מהקריאות לצורה המעודכנת - ונשבור את הקוד. אני ממליץ להשתמש בצורה זו תחת הכלל: כל המשתנים - פרטיים, כל הפונקציות - ציבוריות. מבנה כזה שימושי עבור Data Objects, Mock Objects וכו'.

משפחה ב׳ - יעילות בצריכת הזיכרון ואפשרויות הורשה
משפחה זו מתבססת על prototype שהוא האמצעי בג׳אווהסקריפט לשתף קוד בין אובייקטים שונים. Prototype הוא גם הכלי לבטא הורשה בין אובייקטים (או ״מחלקות״). אם אתם רוצים להשתמש בהורשה (לא, לא!) אזי זו הדרך ללכת בה.


תיאור זה של מחלקה הוא נפוץ מאוד, הוא קרוי לעתים Prototype Pattern. אתם אמורים לזהות את המבנה הבסיסי מהפוסט הקודם בסדרה. בעצם זו וריאציה קלה על ה "Pattern הרשמי" מכיוון שהיא כוללת "סימון" של private members - בעזרת התחילית "_" (קו תחתון).  סימון זה הוא קונבנציה בלבד - ועל המפתחים לשמור על משמעת אישית על מנת לא-להשתמש ב private members מחוץ למחלקה.



תיאור זה הוא הדרך שבה CoffeeScript בחרה לתאר מחלקות. הוא מאגד את כל המחלקה בבלוק אחד (נדיר למשפחה ב') ונפטר מה object literal notation להגדרת הפונקציות.
החיסרון? המבנה מורכב מעט יותר: יש שימוש גם ב closure וגם ב Constructor.


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

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


הערות? מחשבות? - אשמח לשמוע!


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


16 תגובות:

  1. הפוסטים על JavaScript מעולים!!

    תודה!

    השבמחק
  2. אנונימי3/11/12 19:29

    מה רע עם פרוטוטיפ יותר טריוויאלי?
    Object=function(){..}p
    Object.prototype={..}p

    השבמחק
    תשובות
    1. לא רע.

      כפי שאמרתי: יש המון דרכים אפשריות, בסוף צריך לבחור.

      מחק
  3. תגובה זו הוסרה על ידי המחבר.

    השבמחק
  4. ליאור תודה רבה, פוסט ממצה וברור!

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

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

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

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

      ליאור

      מחק
    2. אנונימי5/2/13 22:07

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

      מחק
    3. היי אנונימי,

      יש משהו במה שאתה אומר - תפיסה טובה!
      אמנם הפונקציות הן private אך המשתנים (this._value) הם בעצם ציבוריים. אין לי בעיה לשנות את _value שלא דרך המתודות.

      פתח console של דפדפן ותנסה.

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

      ליאור

      מחק
  6. אנונימי13/8/13 11:01

    היי ליאור,

    האם יש חסרון להשתמש במחלקות json?

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

      אני מניח שאתה מדבר על Object Literal, קרי { ...ns.x = { methodName: function

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

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

      ליאור

      מחק
    2. אנונימי18/8/13 14:28

      היי,

      יש אפשרות לקחת אוביקט json ולשמור אותו כך:
      ObjectBase.prototype = json object
      ואז לרשת מ - ObjectBase.

      מחק
    3. אנונימי18/8/13 14:33

      להלן דוגמא :

      http://jsfiddle.net/4fNKX/1/

      מחק
  7. אנונימי18/12/13 19:10

    פוסט מצוין כמו תמיד,
    אבל למה לא להשתמש בהורשה?

    השבמחק
    תשובות
    1. אנונימי22/12/13 12:19

      בגלל הפוסט הזה?
      http://www.softwarearchiblog.com/2012/02/object-oriented.html

      מחק