אבני הבנין של האינטרנט: SOP ותקשורת Cross-Domain

כלל ה Same Origin Policy, או בקיצור SOP, הופיע לראשונה בדפדפן Netscape 2.0. הוא הופיע בד-בבד עם שפת התכנות JavaScript וה (Document Object Model (DOM. כמו JavaScript וה DOM, הוא הפך לחלק בלתי-נפרד מהדפדפן המודרני.

SOP הוא עקרון האבטחה, כנראה המרכזי ביותר בדפדפן. הרעיון הוא דיי פשוט: שני Execution Contexts שונים של ג'אווהסקריפט לא יוכלו לגשת על ה DOM אחד של השני אם הם לא מגיעים מאותו המקור (origin).
מתי יש לנו Execution Contexts שונים? כאשר יש iFrames שונים או טאבים שונים.

איך מוגדר origin? שילוב של שלושה אלמנטים:

  • סכמה / פרוטוקול (למשל http או https)
  • Fully Qualified Domain Name (בקיצור: FQDN, למשל news.google.co.il)
  • מספר port [א].

SOP מונע מצב בו פתחנו 2 חלונות: אחד לאתר לגיטימי והשני לאתר זדוני (בטעות), והאתר הזדוני עושה באתר הלגיטימי כרצונו. בנוסף יש לנו כלי בשם iFrame – כלי ל isolation, כך שאנו יכולים לפתוח בדף שלנו iFrame לאתר חיצוני לא מוכר ולדעת שאנו מוגנים בפניו.

עולם האינטרנט השתנה מאז 1995, והיום יש הרבה יותר אינטראקציה משותפת (mashups) בין אתרים שונים. SOP מגביל את היכולת שלנו לשתף מידע בין מקורות שונים.

אז מה עושים? האם נחרץ גורלו של האתר שלנו להיות מוגן, אך מבודד – לעד?

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

פוסט זה שייך לסדרה אבני הבניין של האינטרנט.

תזכירו לי מה המשמעות של SOP?

נניח שדף ה HTML של האפליקציה / אתר שלנו נטען מהכתובת http://www.example.com/home/index.html.
משמע: ה origin הנוכחי שלנו הוא http://www.example.com:80.

הנה המשמעות של מדיניות ה SOP בנוגע לגישה ל origins אחרים ("Compared URL"):

מקור: http://en.wikipedia.org/wiki/Same_origin_policy

הערה מעניינת: SOP, כמו מנגנוני הגנה אחרים של הדפדפן, מתבססים על ה FQDN ללא אימות מול ה IP Address. המשמעות היא שפריצה לשרת DNS יכולה להשבית את ה SOP ולאפשר קשת רחבה ויצירתית של התקפות על המשתמש.

SOP הוגדר במקור עבור גישה ל DOM בלבד, אך עם השנים הוא הורחב:

XMLHttpRequest (המשמש לקריאות Ajax) מוגבל גם הוא. קריאות Ajax יוגבלו רק ל origin ממנו נטען המסמך (כלומר: קובץ ה HTML). בנוסף, בעקבות היסטוריה של התקפות שהתבססו על "זיוף" קריאות ל origin הלגיטימי עם Headers מטעים – נוספו מספר מגבלות על Headers אותם ניתן לשנות בקריאות Ajax (למשל: Referer או Content-Length וכו').

Local Storage (יכולת חדשה ב HTML5 לשמור נתונים באופן קבוע על הדפדפן) גם היא מוגבלת ע"פ ה SOP. לכל origin יש storage שלו ולא ניתן לגשת ל storage של origin אחר.

Cookies – האמת שמגבלות על Cookies החלו במקביל להתפתחות ה SOP. התוצאה: סט מגבלות דומה, אך מעט שונה. הדפדפן לא ישלח Cookies לדומיין אחר (למשל evil.com) אך הוא ישלח את ה cookies לתת-domain למשל:
evil.www.example.com, תת-domain של www.example.com.
בדפדפנים שאינם Internet Explorer, ניתן לדרוש אכיפה של domain מדויק ע"י השמטה של פרמטר ה domain (תיעוד ב MDN, קראו את התיאור של הפרמטר domain).

כיוון ש Cookie יכולים להכיל מידע רגיש מבחינת אבטחת מידע (למשל: אישור גישה לשרת), הוסיפו עליהם עוד 2 מנגנוני הגנה נוספים:
httponly – פרמטר ב HTTP header של set-cookie שהשרת יכול לסמן, המונע גישה מקוד ג'אווהסקריפט בצד-הלקוח ל cookie שטמן השרת. כלומר: ה cookie רק יעבור הלוך וחזור בין השרת לדפדפן, בלי שלקוד הג'אווהסקריפט תהיה גישה אליו.
secure – פרמטר בצד הג'אווהסקריפט של יצירת cookie (שוב, התיעוד ב MDN) שאם נקבע ל true – יגרום לדפדפן להעביר את ה cookie רק על גבי תקשורת HTTPS (כלומר: מוצפנת).

Java Applet, Flash ו Silverlight כוללים כללים שונים ומשונים הנוגעים ל SOP. חבורה זו היא זן נכחד – ולכן אני מדלג על הדיון בעניינה.

SOP מתיר חופש במקרים הבאים:

Cross domain resource loading – שימו לב, זהו כלל חשוב: הדפדפן כן מאפשר לטעון קבצים מ domains אחרים. לדוגמה: קבצי ג'אווהסקריפט, תמונות, פונטים (בעזרת font-face@) או קבצי וידאו. על קבצי CSS יש כמה מגבלות [ב]. כלומר: האתר שלנו, http://www.example.com יכול בלי בעיה לטעון קובץ ג'אווהסקריפט מאתר אחר suspicious.com. טעינת הג'אווהסקריפט הינה הצהרת אמון במקור – ועל כן קוד הג'אווהסקריפט שנטען מקבל את ה origin שלנו ועל כן הוא יכול לגשת ל DOM שלנו ללא מגבלה. מצד שני: הוא אינו יכול לגשת ל DOM של iFrame אחר שמקורו מ suspicious.com או לבצע קריאות Ajax ל suspicious.com – למרות שהוא נטען משם.

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

Location – סקריפט שמגיע מ origin שונה מורשה לבצע פעולות השמה (אך לא קריאה) על אובייקט ה Location (ה URL הנוכחי של ה frame) כגון ()location.replace. זה נשמע מעט מוזר, אך הסיבה לכך היא לאפשר שימוש בטכניקה בשם iFrame busting הנועדה להילחם בהתקפה בשם clickjacking. כלומר: כדי להבטיח שאתר זדוני לא מארח את האתר שלנו ומראה רק חלקים מסוימים ממנו כחלק מהונאה, מותר לנו לגשת לכל frame אחר בדף (למשל ה Top Frame) ולהפוך אותו לכתובת האתר שלנו. התוצאה: האתר המארח יוחלף באתר שלנו – ללא אירוח.
דרך מודרנית יותר למניעת clickjacking היא שימוש ב HTTP Header בשם X-Frame-Options.

מתקפת clickjacking בפעולה. מקור.

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

טכניקות התמודדות

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

רשימת הטכניקות אותן נסקור בפוסט זה
בגדול ניתן לחלק את הטכניקות ל2 משפחות:
  1. תקשורת בין Frames מ Origins שונים באותו הדף.
  2. תקשורת בין לקוח לשרת ב Origin שונה (כלומר: קריאות Ajax).
אני אפתח במשפחה הראשונה, למרות שהשימוש בה פחות נפוץ בימנו – מכיוון שחלק מהטכניקות שלה מהוות בסיס לטכניקות במשפחה השנייה.

Cross-Origin inter-Frame Communication




להלן נסקור מספר טכניקות מקובלות כדי לתקשר בין iFrames מ origins (או domains) שונים.


Domain Relaxation – הטכניקה הזו אופשרה ע"י הדפדפנים.
בטכניקה זו קוד הג'אווהסקריפט משנה את ה origin הנוכחי ע"י השמת ערך חדש למשתנה: document.domain.
המגבלה: הוא יכול רק "לקצץ" תתי-domains ולא להחליף את ה domain לגמרי. לדוגמה:
  • מ "login.example.com" ל "example.com" – מותר.
  • מ "login.example.com" ל "login.com" – אסור.
  • כמו כן, מ "login.example.com" ל "com" – מותר, אבל מסוכן!!
התוצאה של פעולת ה domain relaxation היא הבעת אמון גדולה בכל אתר מה domain המעודכן, והרשאה לג'אווהסקריפט של אותו האתר לערוך את ה DOM שלנו. אם אפליקציה זדונית שאנו מארחים ב iFrame, מסוגלת לבצע Domain Relaxation לאותו Relaxed Domain כמו שלנו – היא יכולה לגשת ל DOM שלנו ללא מגבלה.
חשוב לציין ש Domain Relaxation לא משפיע על אובייקט ה XmlHttpRequest. קריאות Ajax יתאפשרו רק ל origin המקורי ממנו נטען ה Document שלנו.
בנוסף, בחלק מהדפדפנים Domain Relaxation ישפיע רק על גישה בין Frames באותו הטאב ולא על גישה בין טאבים נפרדים.
סיכום: Domain Relaxation היא טכניקה נוחה בתוך ארגון בו כל המחשבים הם תחת דומיין-על אחיד, אך היא לא נחשבת לבטוחה במיוחד. שימוש ב Domain Relaxation פותח פתח למרחב לא ידוע של תתי-domains שאנו לא מכירים – לגשת ל DOM שלנו.

Encoding Messaged on the URL Fragment Id

טריק מלוכלך זה מתבסס על 2 עובדות:

  • SOP מתיר ל Frame אחד לשנות את ה Location (כלומר URL) של Frame כלשהו אחר בדף.
  • שינוי של FragmentID (החלק ב URL שלאחר סימן ה #) לא גורם ל reload של המסמך (כפי שהסברנו בפוסט על ה URL)
התרגיל עובד כך: פריים (frame) א' משנה את ה FID (קיצור של Fragment ID) של פריים ב'. הוא מקודד הודעה כלשהי שהוא רוצה להעביר.
פריים ב' מאזין לשינויים ב FID שלו, מפענח את ההודעה ומחזיר תשובה ע"י קידוד הודעה ע"י שינוי ה FID של פריים ב'.
וריאציה נוספת של הטכניקה הזו היא שכל פריים משנה את ה window.name של עצמו. SOP מתיר ל frames שונים לקרוא את ה property הזה מ frames אחרים ללא הגבלה.
סיכום: מלוכלך, מוגבל, לא בטוח (אני לא ידוע מי באמת שינה לי את ה FID) – אבל יכול לעבוד.
ישנן עוד כמה טכניקות דומות (בעזרת Flash, למשל) – אך אני אדלג עליהן.

Post Message

בשלב מסוים החליטו הדפדפנים לשים סוף לבאלגן. כשהרבה מפתחים משתמשים בטכניקות כגון ה Encoding על ה FID, עדיף פשוט לאפשר להם דרך פשוטה ובטוחה. כאן נכנסת לתמונה יכולת ה "Post Message", שהוצגה כחלק מ HTML5.

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

מנגנון ה Post Message (בקיצור: PM) מאפשר לשלוח הודעות טקסט בין iFrames או חלונות שונים בדפדפן, בתוספת מנגנון אבטחה שמגביל את שליחת / קבלת ההודעות ל domain ידוע מראש.

הנה דוגמה:

כדאי לציין שניתן לשלוח הודעה עם target domain של "*" ("לכל מען דבעי"). לא ברור לי מדוע איפשרו יכולת זו – אבל היא מדלגת על מנגנון האבטחה החשוב של PM. כמו כן חשוב לבצע בדיקת domain בקבלה ולבדוק רק Domain מדויק.
מימוש אפשרי הוא בדיקה חלקית, למשל:

if (msg.origin.indexOf(".example.com") != -1) { … }

מזהים מה הבעיה פה?
תוקף מ Domain בשם example.com.dr-evil.com (השייך לחלוטין לדומיין dr-evil.com) יוכל גם הוא לשלוח לנו הודעות!

סיכום: טכניקה בטוחה וקלה לשימוש. כדאי תמיד להעדיף אותה – אם היא זמינה.

שווה לציין ספריה בשם EasyXDM לשליחת הודעות Cross Domain.
EasyXDM ישתמש ב Post Message, אם הוא זמין (IE8+) או יבצע fallback למגוון שיטות היכולות לעבוד על IE6 ו IE7 – אם אין לו ברירה. מומלץ להשתמש רק אם תמיכה ב IE6-7 היא חובה.

Cross-Origin Client-To-Server Communication

סקרנו את המשפחה הראשונה של הטכניקות: תקשורת בין Frames בדפדפן. טכניקות אלו שימושית כאשר אנו מארחים אפליקציות או widgets ב iFrame.

הצורך הנפוץ יותר הוא בביצוע קריאות לשרת שנמצא ב origin שונה. תזכורת: אם האפליקציה שלנו נטענה מ origin A והיא תנסה לבצע קריאת Ajax ל origin B – הקריאה לא תצא, הדפדפן יחסום אותה.

בואו נעבור על מספר טכניקות מקובלות המאפשרות לבצע קריאת Ajax ל originB – וננסה לעשות זאת בצורה מאובטחת ככל האפשר.

Server Proxy

כאשר יש לנו דף / document המשויך ל Domain A, אין לו בעיה לייצר קריאות Ajax ל Domain A – הרי זה ה origin שלו. אם אנו מנסים להוציא קריאת Ajax ל Domain B (כלומר: domain אחר), הדפדפן יחסום את הקריאה כחלק ממדיניות ה SOP:

אפשרות אחת להתמודד עם הבעיה היא לבקש מהשרת ב Domain A, לשמש עבורנו כ "Proxy" ולבצע עבורנו את הקריאה:

לשרת אין מדיניות SOP, ולכן אין לו מגבלה לתקשר מול שרת אחר שנמצא ב Domain B. אין משמעות הדבר שאין סיכון בתקשורת לשרת אחר – פשוט אין אכיפה. על שרת A לממש את מנגנוני האבטחה בעצמו ולהחליט באילו שרתים אחרים הוא בוטח.

וריאציה אחרת של שיטה זו היא להציב Reverse Proxy בין הדפדפן ל 2 השרתים. ניתן לקבוע חוקים ב Reverse Proxy שייגרמו ל-2 השרתים להראות כאילו הם באותו ה Domain.

גישת ה Server Proxy היא פשוטה, אולם יש לה כמה חסרונות:

  • השרת צריך לבצע עבודה נוספת של העברת הודעות בין הדפדפן לשרת ב' , מה שיכול בהחלט לפגוע לו ב Scalability (עוד חומרה = עוד כסף). במקרה של Reverse Proxy – העלות הנוספת היא ברורה יותר.
  • שרת ה Proxy שלנו הוא לא דפדפן, הוא לא יעביר באופן טבעי User Agent או Cookies, אלא אם נוסיף קוד שיעשה זאת.
  • "הערמנו" על הדפדפן ועל מנגנון האבטחה שלו, אבל האם יצרנו אלטרנטיבה בטוחה מספיק? (האם יצרנו בכלל אלטרנטיבה בטוחה במידה כלשהי?)
סיכום: פתרון פשוט אבל בעייתי: יש לשים לב למחיר הנוסף ב Scalability, ולוודא שלא יצרנו פרצת-אבטחה.
JSONP (קיצור של JSON with Padding)
טכניקת ה JSONP מבוססת על העובדה הבאה:

  • SOP לא מגביל אותנו לטעון קבצי JavaScript מ Domains אחרים.

מה היה קורה אם קוד הג'אווהסקריפט, שנטען מ Domain אחר, היה כולל קוד שיוצר במיוחד עבור הקריאה שלנו? למשל, קוד המבצע השמה של שורת נתונים לתוך משתנה גלובלי שאנו יכולים לגשת אליו? – משמע שהצלחנו להעביר נתונים, Cross-domain, לדפדפן!

הנה דוגמת קוד כיצד אנו קוראים מצד-הלקוח ל API מסוג JSONP:

והנה התשובה שהשרת מייצר (דינמית) = הקובץ info.js:

הערך "jsonpCallBack" הגיע כפרטמר על ה URL של ה Request, ושאר הנתונים הגיעו מהשרת (מבנה נתונים, DB וכו').
הקוד הנוסף שעוטף את ה data, בין אם זו השמה למשתנה גלובלי או קריאה ל callback ששמו נשלח – נקרא "Padding". זהו ה P בשם JSONP.
לרוב אנו נעדיף Padding מסוג callback, מכיוון ש callback מייצר trigger ברגע המידע חזר מהשרת. כאשר משתמשים ב Padding מסוג "השמה למשתנה גלובלי" אנו נאלץ לדגום את המשתנה שוב ושוב בכדי לדעת מתי הוא השתנה…

ל JSONP יש מספר חסרונות:

  • נדרשת תמיכה מהשרת: על השרת לייצר Padding מתאים לנתונים. ה Padding חייב לקרות בצד השרת ואין דרך "להשלים" אותו בצד הלקוח עבור קובץ ג'אווהסקריפט שלא כולל אותו.
  • מכיוון שלא ניתן לטעון קובץ ג'אווהסקריפט יותר מפעם אחת, אם אנו רוצים לבצע מספר קריאות JSONP יהיה עלינו לייצר שם חדש לקובץ ה script בכל קריאה = מעמסה.
  • JSNOP מוגבל לפעולות HTTP GET בלבד (לא ניתן לטעון scripts במתודת HTTP אחרת) – עובדה שמונעת מאתנו להשתמש ב JSONP כדי לקרוא ל REST API.
  • אין Error Handling. אם קובץ הג'אווהסקריפט לא נטען (404, 500 וכו') – אין לנו שום דרך לדעת זאת. פשוט לא יקרה שום דבר.
  • אבטחת מידע: השרת ממנו אני טוען את הנתונים ב JSONP לא מעביר רק נתונים – הוא מעביר קוד להרצה. אני צריך לסמוך על השרת הזה שלא ישלח לי קוד זדוני.

יתרון משמעותי של JSONP הוא הפשטות (היחסית) כאשר יש לנו קריאה בודדת והעובדה שהוא יעבוד גם עם דפדפנים ישנים. ניתן למצוא הרבה WEB APIs של חברות חשובות כגון גוגל או טוויטר – החשופים ב JSONP.

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

iFrame Proxy / Relay
טכניקת ה iFrame Proxy (או iFrame Relay) מבוססת על תקשורת בין iFrames, וספציפית על Post Messages.
המנגנון עובד כך:

  1. פתח iFrame שה URL שלו מצביע עד דף (שהכנו מראש) הנמצא על הדומיין איתו אנו רוצים לתקשר. ה origin של המסמך ב iFrame יהיה אותו ה domain.
  2. הדף הנ"ל יטען קובץ ג'אווהסקריפט (שהכנו מראש), נקרא לו proxy.js.
  3. נבצע קריאות Post-Message ל iFrame שייצרנו. proxy.js יאזין לקריאות אלו, כאשר מנגנון ה Post-Message מספק לנו אבטחה.
  4. proxy.js יבצע קריאת Ajax לדומיין המרוחק, אין לו מגבלות – כי הדומיין הזה הוא ה origin שלו.
אם השרת מחזיר תשובה, היא תגיע ל Proxy.js.
  1. proxy.js מקבל את התשובה מהשרת ומבצע Post-Message בחרזה ל Frame / לקוד שלנו.
לגישת ה iFrame Proxy יש כמה חסרונות:
  • היא מורכבת למימוש.
  • נדרשת תמיכה מצד השרת.
  • היא דורשת תמיכה ב PM, קרי IE8+.
מצד שני היא טובה מבחינת Security:
  • בעזרת מנגנון ה PM אנו מוודאים ש proxy.js מגיע מה domain הרצוי.
  • proxy.js מבודד בתוך iFrame ואינו יכול להשפיע על הקוד שלנו – כך שלא חייבים לסמוך ב 100% על שרת היעד.
סיכום: אופציה מורכבת למימוש – אך טובה מבחינת Security.

CORS (קיצור של Cross Origin Resource Sharing)
מאחר ו JSONP ו iFrame Proxy הם אלתורים, החליטו הדפדפנים לפתור את בעיית הקריאה לשרת cross-domain בצורה שיטתית. התוצאה היא פרוטוקול ה CORS.

CORS מאפשר לנו בקלות יחסית לבצע קריאת "Ajax" לשרת בדומיין אחר. השרת צריך לממש מצדו את הפרוטוקול (כמה כללי התנהגות ו Headers על גבי HTTP).

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

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

גרסת ה IE הראשונה שממשה את CORS היא IE10 (וגם לו יש באג משמעותי – הוא לא שולח cookies כמו שצריך).
בפועל התמיכה ב CORS מתחילה מ IE10 אם אינכם זקוקים ל cookies, ואם אתם זקוקים ל Cookies – היא מתחילה ב IE11. סוג האפליקציות שיכולות להסתדר עם תנאי סף שכאלו הם בעיקר אפליקציות מובייל.

עדיין אפשר לבצע מימוש יותר מורכב שישתמש ב XDomainRequest ב IE8-10 וב CORS בכל שאר המקרים.

חסרונות סה"כ:

  • ה API של CORS מעט מסורבל, מימוש בצד השרת דורש מעט עבודה.
  • CORS עשוי להזדקק ל 2 HTTP Requests (מה שנקרא "preflight") בכדי להשלים קריאה בודדת. אלו הדקויות של המימוש הפנימי.
  • התמיכה של IE היא בעייתית.
ל CORS יש גם יתרונות:
  • סטנדרטי
  • אבטחה טובה
  • לא מצריך להמציא מנגנון שלם, כגון iFrame Proxy.
סיכום: אופציה טובה, שתהיה טובה יותר בעוד מספר שנים.

סיכום

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

סקרנו והשוונו את הטכניקות הנפוצות לביצוע תקשורת Cross-Domain תחת מנגנון ה SOP, טכניקות מ-2 משפחות:

  • תקשורת בין iFrame ל iFrame.
  • תקשורת מול שרת ב Domain אחר.
סה"כ, הבנת נושא ה SOP היא חשובה למדי עבור מערכות המתקשרות בין Domains שונים, צורך ההולך וגובר עם השנים.
שיהיה בהצלחה!
—-

[א] דפדפן IE לא מחשיב את ה port כחלק מה origin עבור גישה ל DOM. בפועל נדירים מ המקרים בהם דף HTML ייטען מ port לא סטנדרטי.

[ב] המגבלות שונות מדפדפן לדפדפן: IE, פיירפוקס, כרום, ספארי (יש לגלול ל CVE-2010-0051) ואופרה (לפני השימוש ב blink). אינני מתחייב שהרשימה מלאה ו/או מעודכנת.

קוד ספרותי = סופן של ההערות בקוד?

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

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

טרמינולוגיה: קצת סדר

לאידאל של קוד אסתטי, אלגנטי וקל לקריאה נוהגים לקרוא בימנו "Clean Code" או "קוד נקי".
קוד נקי מורכב מ 2 אלמנטים עיקריים – מבנה הקוד (למשל: כל פונקציה עושה רק דבר אחד) וקוד ספרותי ("קל לקרוא את הקוד כמו ספר קריאה").

בפוסט זה אני רוצה להתמקד ב"קוד הספרותי" (לא לבלבל עם Literate Programming – דבר אחר לגמרי) בלבד. לא בגלל שמבנה הוא פחות חשוב (חלילה!) – פשוט אחרת לא אגמור את הפוסט.

גם בתוך הקוד הספרותי יש 2 אלמנטים מרכזיים: שפה טבעית (מונח שהמצאתי הרגע) ותיעוד עצמי (Self Documentation).

כלומר: רק כאשר יהיה לנו קוד החותר לשפה טבעית וגם "תיעוד עצמי" – נגיע לקוד הספרותי עליו אנו מדברים. כמובן שקוד קריא באמת ("Clean Code") יהיה רק כאשר גם המבנה יהיה "נקי".

גישת הקוד הספרותי היא נפוצה – אם כי איננה קונצנזוס. היא התבססה בעיקר בעקבות 3 ספרים "פורצי-דרך":

מכתב בשפת Ruby. מקור.

מהו "קוד ספרותי"?

הרעיון של קוד ספרותי מבוסס על 2 הנחות:

  1. קוד נקרא הרבה יותר פעמים מאשר הוא נכתב – ועל כן השקעה בקוד כדי שיהיה קריא, היא משתלמת.
  2. הערות הן משהו שאנו נוטים (באופן לא מודע, אולי) להתעלם ממנו. אנו יודעים שזה לא העיקר.
    א. הערות יוצאות לאורך הזמן מסנכרון עם הקוד. הקוד הוא האמת – וההערות נשארות צל של העבר.
    ב. אנו מסוגלים לקרוא קוד תוך התעלמות מההערות, להבין את הקוד בצורה שגויה ולהמשיך הלאה.
אם הנחות אלו לא נכונות עבורכם – ייתכן וכל הגישה אינה מתאימה לכם.
קוד ספרותי מבוסס על שני עקרונות:
עיקרון א': שאיפה לשפה טבעית
על הקוד להיות קל לקריאה כמו ספר. השפה אליה אנו שואפים היא אנגלית ולא שפת-מחשב, כך שכל statement צריך לשאוף להיות משפט ברור באנגלית – ולא "קוד סתרים".
האמת ש"קוד ספרותי" הוא שם לא-מדויק, אולי אף מעט מטעה:
סיפור של שייקספיר (מתפלפל) או של ג'ורג .ר.ר מרטין (לא-נגמר) – הם לא המודלים אליהם אנו שואפים. המודל מדויק יותר יהיה עיתון / "קוד עיתונאי":
  1. תמציתי.
  2. ברור וחד-משמעי.
  3. מדויק.
  4. קל לקרוא קטעים ממנו.
    ניתן לקפוץ לעמוד 6' לקרוא פסקה ולהבין – מבלי שקראנו את כל העיתון. זאת בכדי שנוכל להתמקד בקטעי קוד שמעניינים אותנו כרגע, מבלי שנזדקק לקרוא מאות שורות של קוד קודם לכן בכדי להבין את הקטע המעניין.
עיקרון ב': תיעוד עצמי (Self-Documentation)
על הקוד לתאר את עצמו ולהבליט את הכוונה.
כל פעם שאנו מוסיפים הערה – זו נורת אזהרה שכתבנו קוד שלא מסביר את עצמו. עלינו לנסות להסיר את ההערה ולגרום לקוד לבטא את המסר ללא עזרה.

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

אז כיצד כותבים קוד "ספרותי"?

נתחיל בחתירה ל"שפה טבעית". הנה מספר עקרונות שננסה להדגים:
  1. שמות (משתנים, פונקציות, מחלקות) ברורים בשפה האנגלית.
  2. שמות המתארים "מה" ולא "כיצד". למשל: Parser ולא LineScanner.
  3. שמירה על רצף קריאה קולח, ללא צורך לחזור לאחור או לדלג לפנים בקוד בכדי לקבל את ההקשר.

נתחיל במתן שמות:

// bad
var ic; // says nothing
function monitorTransIP() // what is IP?!
var hashUrl = "ae4a0192#erlkde"; // url to a Hash?
// good
int itemCount;
function monitorInProcessTransactions() // proper English
var urlHash = "ae4a0192#erlkde"; // no. a Hash of a URL...

כפי ששמתם לב, על השמות להיות באנגלית ולסייע להרכיב קוד שנראה ככל האפשר כמשפט באנגלית. כמובן שגם Camel Case הוא חשוב. נסו לקרוא שמות כמו mONITORiNpROCESStRANSACTIONS… 🙂

לא קל לקלוע ישר לשמות מוצלחים. ישנן 4 "דרגות" של שם:

  1. שם סתמי – המחשה: NetworkManager
  2. שם נכון – המחשה: AgentCommunicationManager
  3. שם מדויק – המחשה: AgentUdpPacketTracker
  4. שם בעל משמעות ("meaningful") – המחשה: AgentHealthCheckMonitor*
* כמובן שהשם AgentHealthCheckMonitor הוא מוצלח רק במערכת בה שם זה מתאר בדיוק וביתר משמעות את אחריות המחלקה. נתתי דוגמאות להמחשה ממערכת שאני מכיר וחושב עליה – כמובן השמות שציינתי לא נכונים / מדויקים / בעלי משמעות באופן אוניברסלי, אלא רק למערכת הספציפית.
עצלנות ולחץ גורמים לנו להיצמד לתחתית הסקלה (1,2), בעוד הקפדה ומקצועיות דוחפים אותנו לראש הסקלה (3,4).

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

מתי מותר להשתמש בקיצורים?
קשה לטעון שהקוד הבא הוא לא קריא:

for (int i = 0; i < ObjList.length; i++){
    // doSomething
}

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

הכלל אם כן אומר: ככל ש scope החיים של המשתנה הוא מקומי וקטן יותר – ניתן לקצר בשם. ככל ש scope החיים של המשתנה הוא גדול יותר (למשל: קבוע ממחלקה אחרת) – יש להאריך בשם.

דוגמה:

// bad
for (int iterationIndex = 0; iterationIndex < l.length; iterationIndex ++){ 
    // doSomething(l[iterationIndex]) - what is "l" ?!?!
}
// good
for (int i = 0; i < completedTaskList.length; i++){
    // doSomething(completedTaskList[i])
}
// better?
completedTaskList.forEach(function(task){
    // doSomething(task)
});

הדוגמה אחרונה אכן מקרבת אותנו לשפה טבעית ("forEach") וגם מקצרת את הקוד, אולם יש בה גם נקודה חלשה: היא שברה במעט את רצף הקריאה. באנגלית אנו נוהגים לומר: "…for each completed task" בעוד דוגמת הקוד דומה יותר ל "…with completed tasks, for each" (סוג של: "אבא שלי, אחותו …" במקום "אחות של אבי") – שפה קצת מקורטעת.
ספציפית בג'אווהסקריפט יש תחביר של for… in ששומר אפילו טוב יותר על רצף הקריאה, אבל מציג כמה pitfalls משמעותיים – ולכן אני נמנע ממנו.
בסופו של דבר אנו מוגבלים לאופציות הקיימות בשפת התכנות, ועלינו להחליט איזו אופציה אנו מעדיפים. כדאי לשקלל את כל המרכיבים לפני שבוחרים.

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

ג'ורג' אורוול. סופר ועיתונאי, מודל "לכתיבה עיתנואית":
"Never use a long word where a short one will do"

שמירה על רצף קריאה

הנה כמה דוגמאות כיצד ניתן לחזק את רצף הקריאה:

// not very good
if (node.children() && node.connected()) {
  // doSomething
}

// better
if (node.hasChildren() && node.isConnected()) {
  // doSomething
}

נכון, Hungarian Notations היא סגנון שעבר זמנו, אבל ספציפית הקידומות has ו is מסייעות מאוד לקרוא את המשפט כאשר חסר לנו סימן פיסוק חשוב: סימן השאלה. בנוסף, הקוד המעודכן קרוב יותר לשפה האנגלית.

בשפת Java, מקובל לכתוב:

if ("someValue".equals(myString)) { ... }

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

if (myString.equals("someValue")) { ... }
עומס טקסט כמובן גם משפיע לרעה על קלות הקריאה. הייתי שמח לו הייתי יכול לכתוב בג'אווה:
if (myString == 'someVale') { ... }

ג'אווה היא שפה מרבה-במילים (verbose), תכונה המעמיסה טקסט על המסך ומקשה על הקריאה הקולחת.

באופן דומה, עבור הקורא:

if (myString.isEmpty()) { ... }
יותר קולח מקריאה של
if (myString.equals("")) { ... }
למרות שהתבנית מאוד מוכרת.
הנה עוד דוגמה קטנה לכתיבה מעט שונה, אך קולחת יותר:
// switch => reader has to remember 'statusCode' = the context
switch (statusCode) {
  case 169 : // return Something();
  case 201 : // return Something();
  case 307 : // return Something();
  default: // return SomeOtherStuff();
}

// Better: each line is a complete sentence 
switch (true) {
  case statusCode == 169 : // return Something();
  case statusCode == 201 : // return Something();
  case statusCode == 307 : // return Something();
  default: // return SomeOtherStuff();
}

במקום שהעין תקפוץ כל הזמן ל statusCode להיזכר בהקשר (בדומה למשפטי "with"), כל משפט הופך למשפט שלם. כל זאת – בעזרת אותו סט כלים זמין בשפה.

עדכון: קיבלתי כמה הערות מאנשים שחשבו שדווקא הדוגמה השנייה פחות קריאה בגלל "התרגיל" שהיא עושה (true בתוך ה switch), אני מניח שזה עניין של טעם. ראיתי אינסוף ויכוחים על סגנון עם מאפיינים דומים (בדיקת קלט ו return בתחילת הפונקציה – ליציאה מהירה) – ואינני מצפה להסכמה מלאה: יש טיעונים לכאן ולכאן. בסופו של דבר מה שחשוב הוא לבדוק כיצד הקוד יהיה קריא יותר עבור הצוות / הארגון שלכם.

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



"תיעוד עצמי" – סיפורו של מתכנת

בכדי ללמוד כיצד נראה תיעוד-עצמי, בואו נפתח בסיפורו של מתכנת הלומד את רעיונות הקוד הספרותי, והקשר בין כמות ההערות שהוא כותב – להתמחות שהוא צובר:

כשהמתכנת חדש, הוא אינו כותב הערות. הוא עסוק בלגרום לקוד בכלל לעבוד (A).
עם הזמן, המתכנת מבין שיש חיסרון בקוד מבולגן ובלתי-קריא. הוא מתעד ומסביר עוד ועוד מה הקוד עושה. לעתים כל שורת קוד מקבלת הערה – בכדי שהכל יהיה "ברור" (B).
לאחר זמן נוסף, המתכנת מקבל אמון בכתיבת הקוד שלו. תבניות מסוימות בקוד "מדברות בעד עצמן" והוא לא זקוק לתיעוד. הוא נחשף לרעיונות של "קוד ספרותי" ומגלה כיצד ניתן לתעד את הקוד ללא הערות. זה עובד, הוא מתלהב ועכשיו יש לו "משחק חדש" – לנסות ולחתור ל "0 הערות בקוד", תוך כדי שהוא משאיר את הקוד קריא למדי. הוא מפצח מקרה אחר מקרה (C) – ומצליח לכתוב קוד קריא ללא הערות, עד אשר הוא מגיע למצב בו הוא כותב קוד באורך אלפי שורות קוד ללא שורת הערה אחת.
זה אתגר, זה מהנה וזה עובד. הוא משתמש בטכניקות שוב ושוב ומצליח ליצור קוד קריא ללא הערות. הוא מביט בקוד שלו בהנאה – ומתמוגג. "הצלחתי" (D).

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

העברת הערות לקוד

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

בואו ננסה!

ראשית ננסה להיפטר מ"מספר קסם" (Magic Number). מספר קסם הוא מספר שפתאום מופיע בקוד ולא ברור כיצד החליטו עליו. קסם.

/*= Huh?! =*/
totalHeight = $el.height + 14;


/*= Better =*/
totalHeight = $el.height + 6+6+1+1;


/*= Even Better =*/
// two times the border (6) + two times the margin (1)
totalHeight = $el.height + 6+6+1+1;


/*= Introduce constant; Even Better =*/
var BORDER_WIDTH = 6, MARGIN = 1;
totalHeight = $el.height + 2 * BORDER_WIDTH + 2 * MARGIN;

הערה: השתמשתי בהערות מסוג /*= =*/ כמטה-הערות בהן אני משתמש להעיר על הקוד / ההערות.

"14" הוא מספר קסם – לא ברור כיצד הגיעו אליו. פירוק לנוסחה (6+6+1+1) משפר את המצב, ובעזרת הערה – המצב אפילו טוב יותר. אבל, ההערה יכולה לצאת מסינכרון עם הקוד ולהפוך ללא-מעודכנת. לבסוף אנו כותבים את ההסבר בעזרת קוד בלבד – כזה שקשה יותר להתעלם ממנו או להשאיר אותו לא מעודכן. מצוין!בואו נראה דוגמה נוספת:

/*= Why do we need this comment ? =*/
// remove "http://" from the url
str = url.slice(7);


/*= Introduce constant; Slightly better =*/
var HTTP_PREFIX_LENGTH = 7;
str = url.slice(HTTP_PREFIX_LENGTH);


/*= comment -> code; Better =*/
str = url.slice('http://'.length);

הצלחנו לבטל את ההערה, ולהפוך אותה לחלק מהקוד – קוד קריא. נהדר!

פירוק של ביטויים לא ברורים לאיבר נוסף עם שם ברור אינה שמורה רק למשתנים. הנה טיפול ב"ביטוי קסם":

/*= Why do we need this comment ? =*/
// check if document is valid
if ((aDocument.isAtEndOfStream() && !aDocument.hasInputErrors()) &&
    (MIN_LINES <= lineCount && lineCount <= MAX_LINES)) {
    print(aDocument);
}


/*= extract method; comment -> code =*/
if (isDocumentValid(docStream, lineCount)) {
    print(aDocument);
}
הוצאנו (extract) פונקציה, נתנו לה שם ברור – וביטלנו את הצורך בהערה!עד כאן טוב ויפה. יש מקרים שבהם נדמה לרגע שאין לנו דרך סבירה לבטל את ההערות, שהן פשוט נדרשות:

/*= We need these comments to highlight sections, don't we? =*/
function foo(ObjList){
    var result = [], i;

    // first fill objects
    for (i = 0; i < Objlist.length; i++){
        // doSomething
    }

    // then filter disabled items
    for (i = 0; i < result.length; i++){
        // doSomething
    }

    // sort by priority
    result.sort(function(a, b) {
        // apply some rule
    });

    return result;
}


/*= Extract Methods; comments -> code =*/
function foo(ObjList){
    var result = [];

    result = fillObjects(Objlist);
    result = filterDisabledItems(result);
    result = sortByPriority(result);

    return result;
}

כשאנו רואים פונקציה (foo) שמחולקת בעזרת הערות (כגון "first fill objects") למקטעים, זהו רמז טוב שהפונקציה עושה יותר מדבר אחד. הפתרון העדיף הוא לחלק את הפונקציה למספר פונקציות, שכל אחת עושה פעולה אחת ויש לה שם ברור שמתאר מה היא עושה.
נכון, התוצאה היא יותר פונקציות קטנות וממוקדות – שזה בד"כ יתרון נוסף. אם המחלקה שלכם כוללת יותר מדי פונקציות קטנות – זהו סימן בד"כ שאפשר לפצל את המחלקה לכמה מחלקות

האם גם בדוגמה הבאה ניתן לוותר על ההערה?

function calcRevenue(){
    /*= Walla! This comment is Absolutely Irreplaceable! =*/

    // Order Matters!
    calcMonthlyRevenue();
    calcQuartrlyRevenue();
    calcAnnualRevenue();
}


function calcRevenue(){
    /*= Hmmm... better luck next time =*/

    var lastMonthRevenue = calcMonthlyRevenue();
    var lastQuarterRevenue = calcQuartrlyRevenue(lastMonthRevenue);
    calcAnnualRevenue(lastQuarterRevenue);
}
במקרה זה הייתה לנו הערת "meta" על הקוד: "סדר השורות חשוב!" – הערה שנראה במבט ראשון שאין לה תחליף.
ביטלנו את ההערה ע"י יצירת קשר הכרחי (אם כי מעט מלאכותי) בין הפונקציות המתאר בדיוק את הקשר.
דוגמה זו היא מעט קיצונית ויכולה להיכשל בקרב מפתחים שלא מכירים את ה convention של "תלות מפורשת בין פונקציות". מצד שני, יצא לי להיתקל בהערת "Oder Matters" שהתעלמו ממנה ויצרו באג – כך שאני לא בטוח מה עדיף.אני מניח שבשלב זה הרעיון כבר ברור. הייתי רוצה לקנח בדוגמה לא מפתיעה, אך חשובה: מבני נתונים

/*= Custom data structure: working, but not descriptive =*/
var row = new Array[2]; // team's performance
row[0] = "Liverpool";
row[1] = "15";


/*= Comments -> code; but what are 0 & 1? =*/
var teamPerformance = new Array[2];
teamPerformance[0] = "Liverpool";
teamPerformance[1] = "15";


/*= Introduce Class =*/
var tp = new TeamPerformance();
tp.name = "Liverpool";
tp.wins = "15";

ביקורת

ישנן גם התנגדויות לגישת "הקוד הספרותי". הנה ביקורת מפורסמת (וצבעונית) שהתפרסמה. הנה התגובה של דוד בוב.

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

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

סיכום

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

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

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

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

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

איך מתחילים לכתוב "קוד ספרותי"?
הכי פשוט להיצמד לכלל הצופים (The Boy Scout Rule): "השאר את השטח נקי יותר ממה שקיבלת אותו".
כל פעם שאתם נוגעים בקוד ומבצעים שינוי – שפרו מעט את הקריאות וקדמו מעט את הקוד לעבר "קוד ספרותי". עם הזמן – השינוי יהיה ניכר ויגיעו גם התוצאות.

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

בלי הרבה מילים: מצב הדפדפנים לאחר שנה

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

  • אימוץ אטי של גרסאות חדשות (גם בשל הרבה משתמשי חלונות XP)
  • תמיכה ב HTML5 – הרבה מאחורי שאר הדפדפנים.
  • כלים למפתח (בתוך הדפדפן), המפגרים הרבה אחרי המתחרים.
עברה שנה. חלונות 8 שוחררה ובקרוב תשוחרר חלונות 8.1.
IE10 זמין למשתמשי Windows 7 ויש כבר גרסת בטא של IE11.

אז מה השתנה?

הנה כמה נתונים:

(המסגרת הורודה מתארת את חלון הזמן של השנה האחרונה)

מקור

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

מקורות נוספים:

  • W3C Counter
  • Clicky – שימו לב ש IE נופל בסוף השבוע לטובת כרום (בית) ואז בתחילת השבוע חוזר לעצמו (עבודה).
  • NetMarketShare (קיצוני לטובת IE. לא מאוזן).
  • W3Schools (קיצוני לטובת כרום, לא מאוזן).

ומה עם גרסאות IE השונות?

נראה ש IE10 תפס את הבכורה בקרב גרסאות ה IE! (חיזוק נוסף) – בעיקר על חשבון IE9 (כנראה משתמשי חלונות 7).
זו בשורה טובה למפתחים!

ידוע לנו שבשוק הארגוני מגמת השינוי היא מתונה בהרבה – אבל לא ייתכן שהנתונים שאנו רואים כאן לא משקפים משהו גם ממה שקורה בתוך הארגונים. יש לי מעט נתונים על השוק הארגוני (ממדגם לא גדול במיוחד = אין אחריות). הנתונים אומרים:

  • כ 45% ל IE (חלוקה לגרסאות: 50% IE8, כ 3% ל IE10 וכל השאר מתחלק שווה בשווה בין IE7 ל IE9).
  • כ 25% ל כרום (עליה משמעותית!)
  • כ 20% ל FF.

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

אולי שווה להתבונן בחלוקה למערכות הפעלה בכדי להבין כיצד IE8 שורד:

מקור

להזכיר: IE8 הוא דפדפן ברירת המחדל של Window 7 + ה IE המתקדם ביותר שניתן להתקין על Windows XP.

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

מקור

הבשורה הטובה היא שהמכנה המשותף של HTML5 Support עלה בצורה משמעותית.
גם יש יותר דפדפני IE10, וגם התאימות של IE10 היא כבר \"במשחק\". ברגע ש IE11 ישוחרר לחלונות 7 ו 8 (לאחר זמן מה – מייקרוסופט אישרה) – יש סיכוי טוב שהוא יהפוך לגרסת ה IE הנפוצה ביותר וכך תהיה עוד עליית מדרגה.

IE11 מציג שיפור משמעותי מול IE10 בסט הכלים למפתחים, אך הם עדיין בפיגור מורגש מאחורי Chrome או Firefox.

באשר למובייל:

אנדרואיד עברה כמעט מהפיכה בגרסאות שנמצאות בשימוש.
לאחר כשנתיים ש Gingerbread ו Froyo (גרסאות 2.2 עד 2.3) היו הרוב הגדול של השוק גרסת Jelly Bean (מספר 4.1 עד 4.3 שתשוחרר מיד) הפכה להיות הגרסה הנפוצה ביותר.

הנה מצב גרסאות האנדרואיד לפני שנה אחת, ואחורה:

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

והמצב היום:

מקור: http://developer.android.com/about/dashboards/index.html

הנה פיזור הדפדפנים (החשובים) במובייל:

מקור

עדיין הדפדפן הנפוץ בעולם האנדרואיד הוא ה Android Browser הגוסס ולא כרום.
יש הבדל גדול ביכולות בין Android Browser של אנדרואיד 2 ל Android Browser של אנדרואיד 4 – חבל ש StatCounter לא מבחינים בניהם. עוד כמה נקודות לשים לב אליהן:

  • Chrome ל iOS הוא רק מעטפת ל Safari ולא באמת Chromium (בשל מגבלות שמציבה אפל) – אני מניח ש StatCounter סופרים אותו כספארי.
  • משתמשי iOS גולשים כפול בממוצע ממשתמשי Android – בעיקר בגלל שמכשירים רבים של אנדרואיד נקנים למדינות מתפתחות / ילדים, או פשוט החומרה החלשה שלהם הופכת גלישה לאטית בצורה לא סבירה.
  • למרות ניסיונות של גוגל, Chrome הוא דפדפן ברירת המחדל על Android בעיקר בקו Nexus – כלומר \"הטלפונים של גוגל\".

עוד לינק מעניין הוא השוואת User Experience בין iOS 7 ל Jelly Bean. המסקנה מהכתבה: אפל היא כבר לא מובילת-חדשנות יחידה: היא מעתיקה לא פחות ממה שהיא מועתקת.

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

התיאוריה המאוחדת: קוד, תכנון וארכיטקטורה

ניתן לומר שהגדרת ארכיטקטורה מורכבת מ 4 פעולות בסיסיות:

  • חלוקת המערכת למודולים / תתי מערכות
  • ניהול תלויות (בין המודולים)
  • יצירת הפשטות (Abstractions) והגדרת API
  • תיעוד הארכיטקטורה.
כמעט כל העקרונות והטכניקות של הגדרת ארכיטקטורה (למשל Quality Attributes או חלוקה ל Views) הן הנחיות כיצד לבצע פעולות בסיסיות אלו בצורה נכונה יותר."ניתוח Quality Attributes" היא טכניקה שכתבתי עליה בפוסט הזה והזה.

תכנון מונחה אובייקטים – OOD

תחום ה Object Oriented Design הוא תולדה של רעיונות שהתפרסמו במספר ספרים / מאמרים משפיעים – אך בניגוד למה שניתן לחשוב, אין הגדרה "חד-משמעית וברורה" מהם "עקרונות ה Object Oriented Design".
2 המודלים המקובלים ביותר להגדרת OOD כיום הם:

חוצמזה ניתן בהחלט להזכיר את תנועת ה Patterns (קרי POSA, PLOP, GOF) שלא ניסחה חוקים אלא "תיעדה" תבניות עיצוב מוצלחות – אבל יש לה השפעה ניכרת על הדרך בה אנו עושים היום Design (ולא תמיד לטובה).

העשור האחרון

זרם ה Agile השפיע גם הוא רבות על OOD וקידם כמה רעיונות:
  • "כשאתה מקודד – אתה בעצם עושה Design" (מקור: TDD) –> ומכאן רעיונות כמו "Design by Tests/Coding"
  • ההכרה שביצוע Design או הגדרת ארכיטקטורה הם Waste – שיש לנסות ולייעל אותם ("Just Enough Software Architecture")
  • ההבנה שחיזוי העתיד הוא דבר בלתי-מציאותי, גם על ידי אנשים נבונים למדי, במיוחד במוצרים חדשים אך גם במוצרים קיימים. ירידת קרנם של "העיקרון הפתוח-סגור" (מתוך SOLID) ו "(Predictable Variations (PVs" (מתוך GRASP) והצבת סימני שאלה בפני כמה מהעקרונות האחרים…

התאוריה המאוחדת

בכל מקרה ישנה השאלה: בהינתן עקרונות ל"ארכיטקטורה", "תכנון" ו"כתיבת קוד" – היכן בדיוק עוברים הגבולות הללו? מתי יש להשתמש בעקרון ארכיטקטוני ומתי בטכניקת Design?

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

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

ניסיתי למפות את "ארבע הפעולות הבסיסיות של הגדרת ארכיטקטורה" לפעולות תכנון וכתיבת קוד:

אפשר לראות שמות שונים ("גבוהים" ו"נמוכים") לרעיונות דומים – אבל ההקבלה הרעיונית יפה למדי.

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

הנה שתי דוגמאות:

עקרון קוד: המעט בכתיבת הערות – ע"י כתיבת קוד שמסביר את עצמו טוב יותר ("Literary Code").
אני לא מכיר כלל ברור ש"תיעוד ארכיטקטורה הוא סממן לארכיטקטורה לא ברורה" אבל דיי ברור לי שזה נכון. אם צריך לתעד ארוכות את הארכיטקטורה – כנראה שהיא לא ברורה ואפשר לנסות לשפר את המטפורות / הגדרת המודולים בכדי שתהיה ברורה יותר. כך יהיה צורך בפחות תיעוד.

עקרון ארכיטקטוני: Interface Segregation Principle 
עקרון זה אומר שמודול לא אמור להיות תלוי ב interfaces רבים שאינם בשימוש. אם נוצר מצב כזה – יש לפצל את ה Interfaces כך שהמודול יהיה תלוי, עד כמה שאפשר, רק ב Interfaces שהוא משתמש בהם בפועל. העיקרון נכון מאוד גם למתודות בתוך interface יחיד (רמת ה Design) או לפרמטרים בחתימה של פונקציה בתוך הקוד (רמת הקוד).

עוד היבט קוד שאני מאמץ מעקרון ה ISP הוא לנסות ולהימנע משימוש בספריות (כגון "Open Source") שיש לי מעט שימוש בהן. אני אשתדל לא להכליל במערכת ספרייה של אלף שורות קוד – אם אני משתמש רק ב 50 מהן. אני מעדיף למצוא ספרייה אחרת או אפילו לכתוב אותן שורות קוד לבד. מדוע? א. סיכוי לבעיות מ 950 שורות קוד לא רלוונטיות, ב. מסר לא ברור האם נכון "להשתדל" להשתמש במתודות אחרות בספריה או לא. ג. אם צריך לשנות / לדבג – ייתכן וצריך להבין הרבה קוד בדרך שלא רלוונטי למקרה שלנו.

אפשר להראות כמה דוגמאות של עקרונות ש"עוברים פחות נקי":

  • מקבילה ל"ניהול תלויות" ברמת הקוד – לא מצאתי בדיוק. הסתפקתי בעקרון של הימנעות ממשתנים גלובליים.
  • לרעיון של חלוקת מודולים ע"פ "Unit Of Work" (כך שקבוצות פיתוח שונות יוכלו לעבוד במקביל עם מינימום תלות) – אני לא חושב שיש הקבלה אמיתית ברמת קוד.
  • העיקרון האלמותי של (DRY (Do Not Repeat Yourself הוא "No Brainer" בקוד, אבל הופך לנושא מורכב ולא חד-משמעי ברמת הארכיטקטורה.

בסופו של דבר – יש הרבה מאוד חפיפה בין עקרונות "ארכיטקטורה" לעקרונות "קוד", כך שאין כ"כ חדש ללמוד. הרעיונות חוזרים על עצמם בשינוי אדרת. חלק מהעקרונות (למשל KISS = Keep It Simple Stupid) הם פשוט אוניברסליים.

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

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

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

השינוי, אם כן, שנדרש בקוד הוא להעביר את השורה dbTable.save לפונקציה שביצעה את הקריאה ל drawItem – בהנחה שזו רמת ההפשטה המתאימה.

קושי מסוים בשימוש ב SLAP הוא שאין "מד רמת-הפשטה", כזה שנכוון אותו לשורה בקוד והוא יגיד לנו "רמה 6!" או "רמה 7!". זה הכל בראש שלנו כמפתחים ובני-אדם אינטליגנטים. לפעמים יהיו "סתירות" כך שיוחלט שפעולה X תהיה פעם אחת ברמה n ופעם אחרת ברמה n+1. אני אומר: זה אנושי. זהו עקרון חשוב – פשוט קחו אותו בפרופורציה ("We figured they were more actual guidelines").

סיכום

כפי שאולי אתם שמים לב, התחלתי לאחרונה לתקוף את נושא הארכיטקטורה וה OOD. זה נושא גדול ומורכב, עם זוויות רבות לכל עניין.
במקום להסתגר כשנה וחצי ולהוציא בסוף פוסט באורך של ספר (גישת ה Waterfall), אני מנסה לתקוף את הנושא בצורה אג'ילית: בעזרת nibbles ("ביסים קטנים"). כמו ערמה של דוקים לפרק אחד אחד – עד אשר אוכל להגיע לגרעין הקשה של העניין.

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

פ.ס. : הערות, מחשבות, ביקורות – יתקבלו בהחלט בשמחה!

47 ספריות ג'אווהסקריפט שכל מפתח *חייב* להכיר (חלק ב')

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

בדיקות-יחידה ואוטומציה

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

Jasmine כוללת תחביר אלגנטי שמושפע מ RSpec (כלומר BDD), כך הקוד שדומה יותר לטקסט בשפה האנגלית, נוסח:

expect(items.length).toBeGreaterThan(6);

Jasmine היא דיי מקיפה וכוללת יכולות לבדיקת קוד אסינכרוני (פקודות waitFor ו runs) ו Mock Objects (ליתר דיוק: Spies או fakes).

QUnitהיא ספריית הבית של jQuery. היא פשוטה יותר ומבוססת על תחביר פחות אלגנטי של xUnit נוסח:

ok(items.length > 6); // ok = "assertTrue"
או
strictEqual(0, items.length); // strict to reflect ===

הייחוד של QUnit הוא ביכולת שנקראת Fixtures – היכולת "לשתול" ב DOM קטע של HTML עליו יתבצע הבדיקה – ולהעיף אותו מיד אחריה. ניתן לעשות זאת גם ב Jasmine – במחיר של מספר שורות קוד.
שינויי DOM בעת הבדיקה לא הטכניקה המומלצת לבדיקות-יחידה – אבל לעתים היא נדרשת. במיוחד אם אתם ספרייה שבעיקר עובדת על גבי ה DOM – כמו jQuery.

מכיוון שאין ל QUnit יכולות של Mock Objects, מצמדים אותה לרוב עם ספרייה בשם Sinon– ספרייה עשירה, אלגנטית וטובה ליצירת Mock Objects בג'אווהסקריפט מכל הסוגים (Spy, stub, mock או fake).

בגלל האופי הדינמי של שפת ג'אווהסריפט – קל מאוד לייצר בה Mock Objects. מספר פעמים יצא לי לצרף את Sinon ל Jasmine – בכדי "לכתוב קוד אלגנטי יותר", אך בכל הפעמים גיליתי ש sinon לא מצדיקה את עצמה כש Jasmine בסביבה: Jasmine עם קצת אלתור מספקת יכולת מקבילה ל Sinon – כך שלא הייתה הצדקה להשתמש בספרייה נוספת. הסבר: אני מקפיד להיפטר מספריות שאני משתמש בפונקציה אחת או שתיים שלהן – שאני יכול לכתוב בעצמי ב 5 דקות. (ע"פ עקרון ה segregation of interfaces + כדי לא ליצור דילמה חוזרת באיזה כלי להשתמש לכל בדיקה).

Mocha(מוקה) היא ספרייה שמשתמשים בה יותר ב Node.js, אבל היא מתאימה גם לדפדפן. אפשר לבחור להשתמש בה בתחביר BDD (כמעט כמו התחביר של Jasmine, אך טיפה פחות אלגנטי) או xUnit (נקרא משום מה "TDD") – שניהם נתמכים. למוקה מצד שני, יש תחביר יפה לבדיקות אסינכרוניות (ולכן כנראה אוהבים אותה ב Node). עבור Mock objects מצמדים לה את sinon.

Mocha מבצעת גם הרצה של הבדיקות כך שהיא לא זקוקה ל jsTestDriver. פיצ'רים נחמדים הם סימון בדיקות אטיות כמו ב Karma (כדי שתשפרו אותן או תעבירו אותן ל suite של האינטגרציה) וגילוי קוד שכותב משתנים למרחב הגלובלי.

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

ספריות חדשות בברנג'ה הן Karma, Intern ו Casper.

Internמנסה לספק פתרון בדיקות-יחידה מקיף, מקצה-לקצה, שאין כדוגמתו עד היום (כלומר: יש להרכיב כיום מספר ספריות שונות על מנת לקבל אותו). דברים כגון: הקמה, כתיבת בדיקות, הרצת בדיקות, CI, Coverage וכו'.
עצתי אליה: שתשקיע באינטגרציה ל IDEs – כרגע זה לא נראה על המפה שלה. היא נשמעת מבטיחה מאוד – אבל היא עדיין הספרייה עם הכי פחות משתמשים (בינתיים).

Karma(לשעבר Testacular (נשמע כמו "אשכים"?), מבית Angular.js מבית גוגל) היא פורט של jsTestDriver (הכתוב ב java) ל Node.js (כלומר היא כתובה בג'אווהסקריפט ומריצה ג'אווהסקריפט).

עבדתי עם Karma מעט ומאוד אהבתי. היא כוללת את כל היכולות של jsTestDriver וקצת יותר, היא נראית יציבה יותר מ jsTestDriver (שהשרת שלה לא חי זמן רב ללא restart), היא עובדת טוב יותר עם AMD/require.js, ונראה שחלק גדול מהקהילה של jsTestDriver עובר אליה. היא יודעת להאזין למערכת הקבצים לשינויים ולהריץ את הבדיקות לבד ברקע – מוד עבודה נהדר ל TDD! יש לה את כל הנתונים להצליח בגדול.

היתרונות של jsTestDriver כרגע: אינטגרציה ל WebStorm ו Eclipse וקהילת משתמשים קיימת.

Casperהוא עוד כוכב עולה: הוא מיועד לכתיבת בדיקות אינטגרציה שהן יותר מבדיקות-יחידה אבל עדיין לא בדיקות UI (כלומר Selenium).
Casper מבוסס על Phantom.js (כלומר headless webkit – דפדפן ללא Shell) ויכול לבדוק את קוד הג'אווהסקריפט עם DOM ו CSS. היתרון על Selenium: הוא מהיר הרבה יותר. החיסרון: זה לא דפדפן אמיתי. התנהגויות ייחודיות לדפדפנים ספציפיים לא ישתקפו בו.

אם אתם רציניים לגבי אוטומציה – זהו כלי שבהחלט כדאי לבדוק!

הערה: עם ההצגה של WebDriver, יכול Selenium להריץ גם את Phantom.js ולהנות מהמהירות של דפדפן ללא Shell / שלא מצייר באמת על המסך. יש פה באמת פתח לתחרות אם כי נראה ש Casper דווקא הולך וצובר מומנטום למרות עובדה זו.

Template Engines

Template Engines הן הספריות שחוסכות מאתנו ללכלך את קוד הג'אווהסקריפט שלנו עם concatenations של מקטעי HTML.

מוד העבודה הוא לרוב כזה:

  • כתוב template ל HTML sinppet רצוי והשאר "placeholders" לחלקים הדינמיים.
  • אכסון את ה template כ string, כמקטע בלתי-נראה ב HTML או טען אותו ב ajax מהשרת.
  • "קמפל" (בעזרת ה Template Engine) "אוטומט" של ה template. תוצאת הקומפילציה היא פונקציה שמקבלת נתונים מתאימים ומייצרת על בסיס ה template מחרוזת HTML מתאימה. הערה: פעולת הקומפילציה היא אטית
  • הפעל את הפונקציה שנוצרה כדי להזריק נתונים ולקבל HTML snippet מעודכן – פעולה זו היא מהירה.

יש ה-מ-ו-ן ספריות של Template Engines: אני מכיר כ 15.
במבט ראשון, קשה מאוד להבין מה ההבדל. אפילו קריאה מהירה של ה tutorials – לא מגלה הבדלים משמעותיים בין הספריות.

אז מה בעצם ההבדלים? מדוע צריך כ"כ הרבה ספריות?

  • יש ספריות מהירות יותר ומהירות פחות בהזרקת הנתונים (חלקן יודעות "להזריק ל DOM" רק את מה שהשתנה)
  • יש ספריות מהירות יותר בשלב הקומפילציה.
  • (יש ספריות שלא מאפשרות קומפילציה – אבל מעט).
  • יש ספריות בתחביר ERB / JSP קרי ויש כאלו בתחביר {{ val }} – ענין של סגנון.
  • יש כאלו עם מעט לוגיקה של templating (לולאות, תנאים), יש כאלה עם הרבה לוגיקה (פונקציות, משתנים, …) ויש כאלו שבתור עקרון לא מאפשרות לוגיקה (כי לוגיקה אמורה להיות ב Model ולא ב View).
  • יש ספריות גדולות (כ 2k) או גדולות (כ 30K ויותר).
  • חלקן מתאימות יותר לצד הלקוח (דפדפן) וחלקן מתאימות יותר לצד השרת (node).
  • נראה אבל שהגרום המשמעותי לכך שיש כ"כ הרבה ספריות הוא שלרובן יש פטרון חשוב, מישהו שמקדם אותן ושומר על קיומן. מיד אחשוף מעט מהפוליטיקה המורכבת הזו.
בהחלט רשימה חלקית

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

שתי הספריות הבולטות ביותר הן כנראה mustache.js  ו handlebars.

Mustache.js(שבולטת בתחביר ה {{ x }}). הסוגריים המסולסלים נראים כ"שפם" שסובבו ב90 מעלות – ומכאן שמה. היא פשוטה ובעלת קהילה גדולה. הפטרון שלה הוא פרויקט mustache שיצר מימושים של התחביר לשפות תכנות שונות, javaScript היא רק אחת מהן. Mustache מקפידה לא לאפשר לוגיקה עסקית ב template. המימוש של mustache.js אינו "מקפל" templates ולכן איננה כ"כ יעילה. Handlebars ו Hogan הן ספריות המממשות את תחביר mustache בצורה יעילה.

Handlebars (מקור השם) היא הרחבה של Mustache המספקת יכולות משופרות, בעיקר רינדור הדרגתי ב DOM רק של מה שהשתנה – מה שגורם לה להיות אחרת הספריות המהירות בהזרקת נתונים. היא נכתבה ע"י יהודה כץ, וכך פטרון מרכזי של Handlebars היא ספריית Ember.js (שראינו בחלק א' של הפוסט).

Jade היא לא ספרייה קטנה (כ 40kb) וכל כן כנראה היא פופולרית יותר בצד השרת (קרי node). הפטרון שלה הוא express.js – ספרייה מאוד דומיננטית ב node (סוג של low level Servlet ב node). jade הוא ה default שלה לייצור HTML markup.

Hogan.js היא ספרייה קטנה ומהירה מאוד שטובה ל templates פשוטים. היא תומכת בתחביר של mustache, ללא כל תוספות. הפטרון שלה הוא חברת טוויטר.

dust.js נחשבת ספרייה מבטיחה. הפטרון שלה הוא חברת LinkedIn.

doT.js היא עוד ספרייה פצפונית (פחות מ 3k) שטוענת להיות מאוד מהירה ותומכת בכתיבת לוגיקה. אין לי מושג מי הפטרון שלה… אבל בטח יש מישהו!

jsRender – היא "הדור הבא של jQuery Templates". אני מניח ש jQuery Templates הייתה פופולרית בזכות המותג "jQuery", אבל עובדה שזה לא הספיק לה. היא נסגרה. אינני יודע להשוות את jsRender לאחרות, אבל נדמה לי שהפופולריות שלה בנסיגה. הפטרון: רוחה של jQuery Templates.

איך משווים בין כל הספריות? איך בוחרים?

  1. אם אתם משתמשים בספרייה שעובדת עם אחד מהמנועים – נסו להיצמד אליה ולחסוך לעצמיכם בעיות אינטגרציה.
  2. נסו את http://garann.github.io/template-chooser/ – אתר שכל ייעודו לנסות לעזור לכם לבחור Template Engine.
  3. לא עזר? הייתי מהמר על הגדולות: Hogan – למשימות מיני, Mustache למשימות קטנות או Handlebars למשימות גדולות ומורכבות.

טעינת סקריפטים דינמית, AMD ו CommonJS

פירטתי על ספריות אלו והקשרים בניהן בפוסט require.js – צלילה לעומק.

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

גרפיקה וויזואליזציה

פירטתי על קטגוריה של ספריות אלו בפוסט ויזואליזציה בצד הדפדפן.

מאז הפוסט נראה ש D3 (קיצור של Data Driven Documents) רק הולכת וצוברת תאוצה!
מזכירים אותה המון, ומשתמשים בה בהמון פרויקטים – משמעותית יותר משאר הספריות בקטגוריה.

לינק: סקירה נוספת לספריות ויזואליזציה.

ספריות אחרות

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

Modernizr – "הספריה" ל Feature Detection. במקום לעקוב אחרי גרסאות הדפדפנים (כבר אי אפשר [א]) והיכולות שלהם, קרי "IE8 לא תומך ב SVG, אז נבצע fallback ל gif" – אפשר לשאול בעזרת Modernizr: "דפדפן, האם אתה תומך ב SVG?" ולפעול בהתאם. דרך כתיבה זו הרבה יותר אמינה (robust) לאורך זמן.

על feature detection ו שימוש ב Modernizr ניתן לקרוא בעברית בבלוג אינטרנט-ישראל.

Underscore.js – היא ספרייה נוספת מהיוצר של Backbone שמספקת utilities רבים: עבודה עם מערכים ואובייקטים, תכנות פונקציונלי או סתם utilities שעוזרים לכתוב קוד קצר יותר. כמו ש jQuery תופסת את השם "$", Underscore תופסת את השם "_" ומכאן שמה. לדוגמה:

_.sortBy(array, function(n) {...});

פעמים רבות רציתי להשתמש ב underscore. צירפתי אותה למספר פרויקטים – אבל היא לא שרדה שם ללא Backbone (שם היא מתאימה כמו כפפה ליד): יכולות של ECMAScript 5 (כגון foreach על מערך) והסט המצומצם של utilties של jQuery (כמו each, filter, extend) – סיפקו את הסחורה מספיק טוב כדי לייתר את Underscore.

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

על Bootstrap ו Foundation ניתן להסתכל בשני אופנים:

  1. סט מוכן של של CSS files (ומעט javaScript משלים) לשימוש מהיר באתרי אינטרנט.
  2. ספריית UI מבוססת CSS ולא ג'אווהסקריפט – ועל כן רזה ומהירה.
שתי דרכי ההתבוננות נכונות במידה, אולם אני רוצה לפרגן ולהתמקד בנקודת המבט השנייה.
אי אפשר להתעלם מכך שרוב ה"קוד" בספריות אלו כתוב ב CSS – זה אולי מטריד כמה אנשים, אולם בסופו של יום הן מספקות פונקציונליות מקבילה ל jQuery UI או כל ספריית פקדים אחרת. מלבד הפשטות והביצועים הטובים שנובעים מהשימוש ב CSS, הן מאפשרות Responsive Design מובנה – כך שה"פקדים" או "Layouts" שלהם יתאימו את עצמם לכל גודל של מסך: מסך מחשב, טאבלט או סמארטפון – ויעשו את זה יפה. זה חלק מהיתרון של שימוש ב CSS.
שתי הספריות יכולות לעבוד יפה מאוד עם ספריות שאינן כוללות פקדים כגון MVC או Meteor.
נראה שההתפתחות של CSS3 (שהוא מתקדם בהרבה מ CSS2.1) – הוא שאפשרה זן חדש זה של ספריות.
אופי העבודה עם הספריות גם הוא דיי שונה:
  • לכל ספרייה של כ 100-200 CSS classes שעליכם להצמיד ל DOM במקומות הנכונים. יש כללים שיש ללמוד אילו classes יכולים להשתלב עם אחרים. "מאות CSS Classes" נשמע כדבר מאיים, אך ישנה חוקיות פשוטה ו references טובים שיאפשרו לכם להסתדר איתם בקלות.
  • הפקדים שנוצרים הם דיי "שמרנים". אלו לא ספריות ל UI "סקסי ויוצא דופן" – אלו ספריות ל UI פשוט, אסתטי, אך לא מתחכם. לא תקבלו את פקד ה Date המשוכלל של jQuery UI, למשל. Bootstrap ו Foundation מתאימות במיוחד ל Multiple Page Applications (בניגוד ל SPA) – אפליקציות בעלות מספר רב של מסכים, שאנו רוצים שיהיו אחידים. אלו לרוב יהיו "אתרי אינטרנט המשרתים back-end" או LOB Applications (כלומר: אפליקציות עסקיות).
  • הנתונים עבור הפקדים לא מגיעים מקריאת JavaScript – הם מגיעים מה DOM. יש classes של תיאור נתונים ו classes של "קשירת נתונים לפקד". איך הם מגיעים ל DOM? אולי מצד השרת (RoR, Java, node או ASP.NET), אולי ע"י קוד ג'אווהסקריפט (הנעזר ב template engine?) הדוחף אותם ל DOM. כלומר: מודל זה לא מיטיב מבחינת ביצועים עם אפליקציות שמחליפות נתונים כל הזמן (כמו Dashboards|).
התוצאה היא שאפשר דיי להסתבך ב SPA כשמנסים לעבוד איתן, אבל אפשר להתחיל מהר ולהתקדם מהר כשעובדים על MPA (כקונספט, יכולה להיות MPA עם דף אחד).

האם Bootstrap ו Foundations הן "הדבר הבא"? הדעות חלוקות.
כרגע הן קונספט חדש, מעניין וחם – שכדאי להכיר.

אז מה ההבדלים בין שתי הספריות?

  • Bootstrap (מבית טווטיר) משתמשת ב LESS, בעוד Foundationמשתמשת ב SASS (כיסיתי את LESS ו SASS בפוסט זה). אם אתם מושקעים כבר באחד הכלים – הרי לכם מוטיבציה להעדיף ספרייה אחת על השנייה.
  • ל Bootstrap יש בפירוש קהילה גדולה יותר. הנה אתר עם יותר מ 300 plugins והרחבות ל bootstrap. נ.ב.: הנתונים של JSter נראים מוגזמים לחלוטין, לפיהם נראה כאילו Bootstrap היא הספרייה הפופולרית בכל הפוסט – זה לא המצב.
  • Bootstrap תומכת ב IE7-8 (אם אתם פונים למגזר העסקי – זה משמעותי).
  • Foundation נחשבת גמישה יותר לשינויי עיצוב.
  • יש עוד הרבה הבדלים קטנים בפרטים ובסגנון של כל ספרייה.

סיכום

עברנו על מספר לא מבוטל של ספריות (47!). אני מאמין ש"התמצאות במרחב הספריות" היא חשובה ויכולה בהחלט לסייע, אפילו אם לא תשתמשו ביותר מרבע מהן עד יום מותכם.

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

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

[א] כשגרסאות דפדפן יוצאות בקצב של 6 שבועות (כרום, פיירפוקס) או פעם בשנה (ספארי, IE) – כבר לא סביר באמת לעקוב בקוד אחר הגרסאות.

קישור רלוונטי:
https://github.com/codylindley/frontend-tools
הרבה ספריות Frontend וכמה השוואות

רשימה של ספריות Polyfill – ספריות שמאפשרות תכונות "חדשות" בדפדפנים "ישנים"