2013-07-27

האם אנו זקוקים ל Software Design?

ביצוע תכנון (Software Design) מתחבר לאנשים רבים ל"עשיית הדבר הנכון".
אם את/אתה אחד מאותם אנשים, עצרו לרגע והסבירו: מדוע זה הדבר הנכון? מה יקרה אם לא נעשה Design?

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

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





מעט ספקות

אנחנו לא באמת יודעים מה היה קורה "אילו היינו עושים..." - אנחנו יכולים רק לדמיין: "אם היינו משקיעים במניית אפל בתחילת 2007 ומוכרים אותה ברבעון הראשון של 2012 היינו עושים 600% רווח!". במקבילה שלנו בעולם התכונה: "אם היינו שומרים את כל ה properties על אובייקט ה xyz זה היה פתרון אחד לשלושת הלקוחות ולא היינו מסתבכים ככה!".

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

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

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


עברו 30-40 שנה. מה השתנה?

מאז שהחלו להתעסק בצורה מקיפה ברעיונות של Software Design עברו קצת מים בנהר, ונוספו כמה כלים חדשים ומשמעותיים המקלים על פיתוח תוכנה גדולה ומורכבת:
  • Unit Tests & Automation
  • Agile (ספרינטים קצרים, MVCs)
  • Static Analysis Tools, Modern IDEs
  • מערכת ניהול קוד מבוזרת, כלי build מתקדמים (כמו Gradle [א]) ו Continuous Integration
  • ווב, מחשוב ענן, Continuous Delivery or Deployment 

האם כלים אלו:
  1. מעלים את רף מורכבות התוכנה בה תכנון (Design) הוא משתלם?
  2. מייתרים את הצורך ב Design?
  3. הופכים את ה Design למשהו אחר לגמרי?
  4. או בכלל לא משנים?

מה דעתכם?



הכלכלה של ה Design


את הטיעון "הכלכלי" המצדיק השקעה ב Software Design היה מקובל להציג לאורך השנים בדרכים שונות:

שנות ה 60 עד שנות ה 90: "נדל''ן יכול רק לעלות"



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


שנות ה 2000: "הבורסה היא השקעה בטוחה לאורך זמן, עם תשואה צפויה של 6% בשנה"

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

מקור: הבלוג של מרטין פאולר

כמה הבחנות חשובות שנוספו הן:
  • הטענה שהשקעה ב Design משתלמת היא תחושה (של רבים ומקצועיים) - אך לא עובדה מוכחת.
  • יש אי-הסכמה בקרב המקצוענים מתי Design משתלם: אחרי שבועות של פיתוח או חודשים רבים.
  • יש אבחנה ברורה בין "good design" ל "mediocre design" - פעולת ה Design לא מבטיחה תוצאות, התוצאות תלויות מאוד באיכות העבודה.

לינק רלוונטי: Design Stamina Hypothesis 



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


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

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

לינק רלוונטי: ?Is Design Dead



סיכום

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

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

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

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

קוראים קבועים ישימו לב שאני תוקף לאחרונה את כל הנושא, אולם לאט ובזהירות. היעד הבא הוא להתמודד עם 2 השאלות הנ"ל.

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

שאלות, תגובות, מחשבות וכו' - יתקבלו בשמחה!



----

[א] ב Technology Radar האחרון (מאי 2013) סימנו את Mavan ב "Hold" (תרגם לעברית: "צמצמו השקעות קיימות והימנעו משימוש בו בעתיד"), בעוד Gradle זכה ב "Adopt".



2013-07-20

אבני הבנין של האינטרנט: 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 יש כמה מגבלות [ב]. כלומר: האתר שלנו, 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). אינני מתחייב שהרשימה מלאה ו/או מעודכנת.




2013-07-08

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

כ 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 of 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): "השאר את השטח נקי יותר ממה שקיבלת אותו".
כל פעם שאתם נוגעים בקוד ומבצעים שינוי - שפרו מעט את הקריאות וקדמו מעט את הקוד לעבר "קוד ספרותי". עם הזמן - השינוי יהיה ניכר ויגיעו גם התוצאות.



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

2013-07-04

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


בפוסט שפרסמתי לפני כשנה, מה הבעיה של אינטרנט אקספלורר?, הצגתי כמה בעיות של הדפדפן:
  • אימוץ אטי של גרסאות חדשות (גם בשל הרבה משתמשי חלונות 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. המסקנה מהכתבה: אפל היא כבר לא מובילת-חדשנות יחידה: היא מעתיקה לא פחות ממה שהיא מועתקת.



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