2012-12-04

Backbone.js - ספגטי או רביולי?

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

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

ספריות JavaScript MVC באו לנסות ולעזור למתכנתי ג'אווהסקריפט לעשות סדר - אך האם הן מספיקות?

בפוסט זה אני רוצה לעזוב את מסלול הTutorial שהתחלנו בו לגבי Backbone.js (בקיצור: BB): מסלול שמציג את יכולות הספרייה בצורה אופטימיסטית בה הכל מסתדר יפה. אתם בוודאי מכירים את המצב הבא: אתם לומדים טכנולוגיה, עושים כמה tutorials ומרגישים שהכל ברור ואז אתם מנסים את הטכנולוגיה בעבודה היומיומית שלכם ו..."הופ" - נתקעים אחרי שעות בודדות רק כדי לגלות איזו בעיה עקרונית, מוכרת, שהרבה פורומים דנים בה - אך יש לה מעט פתרונות יפים. מכירים?!

ספריות JavaScript MVC הן לא שונות, ועל כן אני רוצה להציג גם את הצדדים הללו. 


שייך לסדרה: MVC בצד הלקוח, ובכלל.

קוד "ספגטי"?

בחרתי להתמקד בדוגמה "קטנונית" לכאורה. הדוגמה מבוססת על מודל ה Person וה View בשם PersonView מהפוסטים הקודמים. בחלק זה הוספתי לה את היכולת לעשות expand/collapse לפרטים של האדם (במקרה שלנו: האימייל) + אייקון יפה שמציג את המצב, "פתוח" או "סגור".

הנה הקוד:
הקוד אמור להיות ברובו מוכר.

1- העשרתי מעט את ה template. הוספתי תמונה המתארת את מצב הפריט ברשימה (סגור/פתוח) והוספתי class בשם hidden שבעזרתו אני מסתיר חלקים ב markup. תיאור ה class יהיה משהו כמו:
.hidden {
display : none
}
טכניקה זו מאפשרת לי להסתיר / להציג את ה markup ע"י הוספה / הסרה של CSS class - פעולה קלה בjQuery.


2 - שימוש במנגנון האירועים של BB. הפורמט הוא key: value, כאשר:
Key = מחרוזת: <שם אירוע jQuery><רווח><שאילתת Selection ב jQuery>".
Value = מחרוזת עם שם הפונקציה באובייקט ה View שתופעל כתגובה לאירוע.

BB בעצם מפעילה כאן את jQuery: היא רושמת את הפונקציה שהוגדרה ב value, לשם האירוע (למשל "click" או "keypress"), על האלמנט(ים) שחוזרים מהשאילתה.

את השאילתה היא מבצעת על אלמנט ה el, ואם לא הוגדרה שאילתה - היא מפעילה jQuery.delegate על el, כך שהאירוע יופעל עבור כל אלמנט-בן של el.

בנוסף, BB גם עושה עבורנו Function Context Binding (לטיפול ב this) - כך שאין צורך לבצע bind/bindAll לפונקציית הטיפול באירוע.

את האובייקט הנכון של ה View היא מזהה בעזרת ה cid שעל ה el - מה שחוסך לנו הרבה עבודה.

3 - זו הפונקציה שתקרא לאחר שהמשתמש לחץ על ה person-frame שלנו. היא מזהה את המצב הנוכחי ומבצעת את השינויים הדרושים ב DOM. קריאת toggleClass של jQuery מסירה / מוסיפה CSS class בדיוק עבור שימושים כאלו.


הנה ההרצה של הקוד:



1 - אנו מוודאים שה default הוא הנכון.

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

3 - אני בודק גם את המצב ההופכי, עבור שלמות הבדיקה.


הקוד נראה בסה"כ קריא, ולא כ"כ מסובך. מדוע אם כן אני קורא לו "קוד ספגטי"?
  1. אנו שומרים state של האפליקציה (isExpanded) על ה DOM.
    נכון, זו טכניקה מקובלת בג'אווהסקריפט - אבל זה "ספגטי": כל פעם שאנו רוצים לעשות שינויים ויזואליים אנו צריכים "לבחון" את ה DOM ולהסיק מה המצב שלו. כשהקוד גדל ונהיה מסובך - זה יכול להיות פשוט סיוט של קוד.
  2. אנו מציבים לעצמנו מגבלה שה View לא יתרנדר מחדש. אם מרנדרים אותו - אנו מאבדים את ה state שלנו.
  3. ה markup שלנו נובע בעצם מ 2 מקורות שונים: מה template וממנהלי האירועים. כדי להבין מה / כיצד נוצר - יש לבחון את שניהם, מה שיגרום לנו לשאול את עצמנו: "מאיפה 'זה' הגיע...?"
אפילו בקוד הבדיקות, "נגררתי" (עלק) לבדוק את מצב ב DOM: האם יש css class מסוים או לא. מבנה ה DOM הפך לציבורי.


מודל רזה או שמן?

ב JavaScript MVC יש בגדול 2 אפשרויות כיצד לחלק את האחריויות בין ה View וה Model:
  • "מודל רזה" (נקרא גם anemic model) - המודל הוא בעיקרו DataStructure / DTO שמחזיק בצד הלקוח עותק קונסיסטנטי של מצב האובייקט בצד-השרת / בבסיס-הנתונים. כל הלוגיקה ה"עסקית" מתרחשת ב View.
  • "מודל שמן" (נקרא גם rich model) - המודל הוא המקום בו מתרחשת הלוגיקה העסקית, בעוד ה View הוא רק שיקוף של UI למצב המודל.
BB, באופן רשמי, לא נוקטת עמדה לגבי הגישה המועדפת. "אפשר גם וגם".

אם אתם משתמשים ב"מודל רזה", BB מספקת מנגנון בשם Backbone.sync שעוזר לשמור / לטעון את המודל משרת בעל RESTful APIs: אם תגדירו על מודל / אוסף (collection) את ה url של השרת / REST servuce ותקראו לפעולת ()fetch, ספריית BB תקרא לאותו ה URL ב get (קונבנציות REST) ותטען מחדש את המודלים מהשרת. פעולת create על האוסף תיצור בשרת (ולאחר תשובת HTTP 201 - גם באוסף שלכם) את המודל. כנ"ל יש גם update ו delete וכו'.

אם אתם משתמשים ב"מודל שמן" - אזי BB מספקת פחות תשתיות מוכנות. אין בעיה להוסיף פונקציות על המודל שמחשבות ערכים, כגון ()isVIP שמכילה את הלוגיקה האם Person מסוים הוא אדם "חשוב מאוד".

הקושי נובע מהמצב בו אתם רוצים לשמור על המודל client-side state או ui state - כלומר properties שישמשו אתכם בצד הלקוח אך לא תרצו לשמור בשרת.
כיצד תוכלו לומר ל BB אילו תכונות של המודל נשמרות בשרת ואילו לא? BB לא מספקת יכולת מובנה לדילמה זו.
ההורשה ב BB, כלומר extend._ איננה הורשה אמיתית, היא בעצם ממזגת עותק של אובייקט האבא (כלומר Prototype Design Pattern) עם אובייקט חדש שאתם מגדירים.
BB גם לא תומכת בארגון מודלים בהיררכיה - רק ברשימות (כמו טבלאות ב DB רלציוני). כלומר: איננו יכולים להשתמש בהיררכיה על מנת להפריד בין ה״מודל לשמירה בשרת" ל״ערכי המודל של צד-הלקוח״.
  1. לפלטר את ה ui state בצד השרת? אאוץ.
  2. אפשר להשתמש בplug-in ל BB שמאפשר ניהול היררכי של מודל (במקום הורשה), משהו כמו BB deep model. קצת מסורבל.
  3. אפשר לוותר על שימוש ב Backbone.sync ולפלטר לבד את ה ui state.
בואו נבחר בדרך מס' 3.

בלי קשר לבעיה זו - אינני אוהב את הדרך בה עובד מנגנון הסנכרון לשרת של BB :
  • הוא בעיקר עושה דלגציה ל ajax.$ ומוסיף עליה מעט מאוד. אני מרגיש שבנקודה זו BB לא השקיעה בי מספיק.
  • נקודת הגישה לשרת נעשית דרך המודל ולא ישירות - דבר שלא מרגיש לי נכון. טיפול בשגיאות והתנהגויות חריגות הוא מסורבל, הקשר ההדוק הזה מקשה על היכולת לבצע בדיקות-יחידה, ואי אפשר להשתמש ב Backbone.sync עבור צורות תקשורת אחרות (למשל Push/WebSockets).


"מודל שמן" ב Backbone.js

הנה המודל שלנו ב"גרסה השמנה" (Rich Model):

  • הוספתי למודל תכונה בשם isCollapsed שהיא חלק מה UI State שלו - שלא אמור להישמר בבסיס הנתונים.
  • יצרתי מתודה בשם getJSONToPersist שתחזיר לי את ה JSON של מה שצריך להישמר בשרת. ניתן ליצור superclass חדש של BB.Model על מנת למחזר קוד זה.
  • את השינוי במצב (גלוי/חבוי) שייכתי לפונקציה במודל בשם toggle. כרגע היא מזערית, אך זה הרגל טוב לכתוב גם פונקציות לוגיות מזעריות - במקום הנכון. הן נוטות להתרחב.
  • הוספתי עוד דוגמה לפונקציה "לוגית" קצת יותר עשירה בשם isVIP. היא איננה בשימוש בדוגמה זו (ולכן מסומנת באפור).
הנה ה View:



אפשר לראות שיש 2 templates עבור כל מצב: Collapsed או Expanded - גישה זו טובה כאשר יש 2-3 מצבים. אם יש יותר - אז כדאי להחזיק template בסיסי ולבצע ב ()render שינויים ב markup.

המחזור של תגובה-לאירוע שונה משמעותית מזה של הדוגמה הקודמת:
אם קודם המחזור היה: רינדור View ל DOM, לחיצה של משתמש, אירוע -> שינוי ה DOM,
עכשיו המחזור הוא: רינדור View ל DOM, לחיצה של משתמש, אירוע -> שינוי המודל -> אירוע שינוי המודל -> רינדור ה View ל DOM.

השינוי מתרחש במודל - וה View משקף אותו. ב View אין חוכמה לוגית / עסקית - הוא מטפל ב UI נטו.
שימו לב שעל מנת לשנות את המחזור, הוספתי binding לאירוע ה change של המודל בבנאי - כלי נוח ש BB מספק לי.
קוד ה render הוא לא קצר הרבה יותר מהדוגמה הקודמת - אך הוא פשוט יותר ו"שביר" פחות. הוא בוחן את המודל (קוד js פרטי ולא DOM - שיכול להיות מושפע מהרבה מקורות) ורק על פיו הוא מחליט מה לצייר.
אין לי צורך "לבצע delta" (במקרה זה: הסרה של ה CSS Class בשם hidden) כי בכל מחזור אני מתחיל על בסיס ה template מחדש - דבר שמפשט את הקוד משמעותית.


שיקולי ביצועים

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

זה עשוי להראות דיי מרתיע - אך לרוב אין פה בעיית ביצועים אמיתית: רוב הפעמים ההחלפה תהיה של "מודל בודד" שלא יכלול הרבה markup - ולכן החלפתו לא תהיה יקרה.

אם שמתם לב, הוצאתי את "קימפול" של ה templates (פקודת template._) מחוץ ל instance הבודד של ה view - זה דבר משמעותי הרבה יותר מבחינת ביצועים!

אם בכל זאת אתם רוצים לבצע החלפה דינמית, כי ה markup שלכם גדול או הביצועים הם ממש קריטיים - BB מספק מספר כלים שיכולים לעזור.
  • אתם יכולים לעשות binding לשינוי של שדה ספציפי במודל, בפורמט '<change:<field name' על מנת להיות מסוגלים להגיב לשינויים נקודתיים מאוד.
  • אתם יכולים לבקש מ BB להשתמש ב el קיים ב markup ולא לייצר אותו, כך שתוכלו להשתמש בספריית templating סלקטיבית (לדוגמה pure.js או handlebars) - אשר עושה שינויים ב markup מבלי לרנדר אותו כל פעם מחדש.
  • אתם יכולים לא להשתמש בספריית templating ולבצע רינדור סלקטיבי בעצמכם. עשו סדרה של שינויים ב DOM - אך שאבו את המידע מהמודל ולא מה DOM.

אני בטוח שיש עוד כמה דרכים אפשריות...

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


סיכום

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

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

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

אני מניח שאם הגעתם להתעסק ב Backbone / JavaScript MVC - כנראה שעלה צורך ל"קוד רביולי".
אני, הייתי הולך עם זה עוד צעד קדימה - ועובד עם "מודלים שמנים, Views רזים".


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






2 תגובות:

  1. אחלה סדרת פוסטים בנושא Backbone.
    נתת לי הרבה חומר למחשבה.
    האם BB מתאים בעיקר לפרוייקטים קטנים או שאני יכול גם להשתמש איתו בפרוייקטים גדולים בלי בעיה? ואם אפשר אז האם לא תיווצר לי בעית ביצועים כשאני יעבוד עם מודלים שמנים, Views רזים? כיוון שיהיו לי הרבה מודלים שלפעמים גם ירוצו במקביל..

    השבמחק
    תשובות
    1. תודה!

      ישנו מספר לא מבוטל של מערכות גדולות למדי המבוססות BB: הסתכל לדוגמה על http://www.planbox.com/ או על https://supportbee.com/ - שניהם נכתבו בעזרת BB. האם השתמשו בעזרים נוספים / הרחבות עצמיות? אני מניח שכן, אך BB הוא הבסיס.

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

      ליאור

      מחק