2019-07-20

NoScrum


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

אקשר את הפרק ברגע שייצא.

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


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

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

זה כמובן לא היה הדבר היחידי שהתחברתי אליו. היו גם רעיונות כמו אחריות End-to-end למפתחים (באמת), אי הסתמכות על QA, השפעה גדולה של רעיונות Lean Startup - ואחרים שחיברו אותי.

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





מה זה סקראם?


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



אפתח כמה נקודות חשובות שאני רוצה להדגיש ולהזכיר לצורך הדיון הנוכחי:


סקראם היא מתודולוגיה שעיקר הדגש שלה הוא ניהול פרויקטים.

רוב רעיונות האג׳ייל הן בעצם פרשנויות וניסיונות התאמה של Lean Manufacturing (או המקור שלה: TPS של טויוטה) לעולם התוכנה. על התהליך הזה כתבתי בעבר.

לרעיונות האג׳ייל יש פרשנויות שונות, בדמות מתודולוגיות שונות: SCRUM, Crystal, KANBAN, XP, וכו׳
  • XP (כלומר: Extreme Programming) מתמקדת בפרקטיקות של קוד: איך משפרים כתיבת קוד - בעזרת רעיונות של Agile/Lean.
  • Lean Startup מתמקדת בהגדרת מוצר (או פיתוח לקוח).
  • סקראם וקנאבאן מתמקדות בניהול פרויקטים. אין בסקראם (או קאנבאן) שום פרקטיקות העוסקות ישירות בכתיבת קוד. 






לסקראם יש ״תעשיה״ מאחוריה - תעשיית ייעוץ והסמכות 

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

כאשר אני קיבלתי את הסמכת הסקראם מאסטר שלי (כן, כן) - הארגון שעבדתי בו היה צריך ״להשתחרר״ מ 5000 ש״ח בערך (+ דמי חידוש שנתיים?), בכדי לקבל הסמכה של The Scrum Alliance. היה מדובר ביום וחצי של סדנה שלא חידשה לי שום דבר (אחרי שקראתי ספר בנושא) - וזה הרגיש לי קצת יקר.

הסמכה של מאמן SCRUM (זה שמכשיר SCRUM Masters), למשל, עולה $5000 לשנה ו/או הפרשה של $50  ל ScrumAlliance עבור כל תלמיד שהוסמך.

לכאורה The Scrum Alliance מוגדר כארגון שלא למטרות רווח, אך בהחלט יש לו צד כלכלי.

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

בקיצור: סקראם זו לא רק סט-רעיונות. זה גם ביזנס.


סקראם הוא הגורילה בשוק

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

בהינתן הניסיון בשטח - אני חושב ש SCRUM הוא במידה רבה סוג של "ברירת-מחדל". דווקא "לא לעשות SCRUM" - היא סוג של בחירה.



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



מה לא טוב בסקראם?


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

מה המחלה הנפוצה ביותר בקרב אנשי-תוכנה? נכון - בזזת.
הנטיה לקבל החלטות על בסיס ״באזז״.

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

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

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

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

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


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

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

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

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

באמת?!

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

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

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

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

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

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

הקביעה ש"הארגון עושה סקראם" - מקבעת אותנו מנטלית.





סקראם כ Whole


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

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

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

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

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

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

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

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

בעולם הסקראם אפילו צמח מונח כנגד יישומים חלקים של סקראם (שלא מצליחים): ScrumBut: ״מימשנו סקראם... אבל״.

יישמנו סקראם, אבל אולי לא יישמנו פגישות retrospective בכל ספרינט, או לא שינינו את תחליף ה Team Lead ל Scrum Master = "אתם עושים ScrumBut, לא מימשתם את ס-ק-ר-א-ם במלואו - אך איך אתם מצפים שהוא יעבוד?"

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

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

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


אם אני צריך לבחור בארגון כלשהו מה השיפור הבא שארצה לקדם - "חבילת הסקראם", כנראה לא תהיה באחד המקומות הראשונים. כמובן שהבחירה צריכה להתאים לארגון וצרכיו, אבל אחרי שחוויתי הצלחות ברורות בכמה ארגונים עם פרקטיקות אחרות, כמו Minimal Viable Products או Continuous Integration (אמיתי) - ברור שאעדיף לנסות שוב את מה שכבר הצליח, ולא מה שהתמהמה ולא הניב תוצאות חד-משמעיות.



ההתמקדות בסקראם ממסכת אפשרויות אחרות


צריך מאוד להיזהר מהקישור הלא מודע, שאנשים לפעמים עושים: סקראם זה Agile ו Agile זה סקראם. זה פשוט לא נכון.

רעיונות ה Agile מופיעים בצורות ובגוונים שונים. סקראם הוא חלק מהן - אבל יש עוד המון.

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

אני זוכר את Uncle Bob מספר שתנועת ה Software craftsmanship בעצם צמחה, כסוג של תגובה לשימוש הנרחב בסקראם בתור המתודולוגיה המובילה בארגוני-פיתוח.

כבר אמרנו שסקראם מגיעה כ״חבילה״. יישום של סקראם, בד״כ - כולל הרבה מאמץ, וזה אומר שאנחנו הולכים להשקיע הרבה מאוד מאמץ ארגוני ביישום, הטמעה, ושכלול הסקראם שלנו.
כל אותן פגישות Retrospectives, ו Scrum Masters - יזינו למערכת הרבה פידבק ושיפורים רצויים - לגבי הסקראם.

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

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

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

זו בעיה עיקרית שתנועת ה Software Craftsmanship movement ניסתה לפתור - להחזיר כובד משקל מספיק לפרקטיקה הסופר חשובה - של הנדסת תוכנה.


?Why we switched from Scrum to Kanban
זו לא שיטה זו או אחרת, זה החופש לעשות את מה שבאמת צריך....


אז מה עושים?


כמו שאמרתי בראיון: הדבר הכי טוב (עבור תעשיית-התוכנה) שיכול לקרות לסקראם כרגע הוא להתפרק מחבילה - ולהטמיע בתעשייה כפרקטיקות שימושיות on-demand…. ממש כמו Extreme Programming (בקיצור: XP) או Lean Startup

מכירים היום ארגונים שמצהירים על עצמם שהם "עושים XP"? - כמעט ואין כאלו. אבל הפרקטיקות של XP - הן בכל מקום:
  • Continuous Integration
  • Unit Tests
  • Pair Programming
  • Refactoring
  • Coding Standards
  • Small releases
  • 40 שעות עבודה בשבוע
  • ועוד ועוד...
אלו פרקטיקות נפוצות מאוד בתעשייה. הניסיון לקחת את XP כחוק מחייב, לכל פינה בארגון, עבור כל האנשים, כל הזמן - פשוט לא צלח. Pair Programming ב 100% מהזמן - הוא מאוד לא יעיל. Pair Programming מדי פעם (וב context הנכון) - זה דבר נהדר.

מרגע שהתחילו להשתמש בפרקטיקות של XP במידתיות וע״פ הצורך הספציפי - הגיע הערך המשמעותי.

כנ״ל בסקראם: Story Points, Retrospectives, time-boxing, ועוד הן פרקטיקות טובות ושימושיות - במצבים המתאימים.

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

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

ScrumBut צריך להפסיק להיות עלבון - כלי לנזוף במישהו שהוא לא עושה מספיק סקראם. במקום זאת, ScrumBut צריך להיות Best Practice: ״אני משתמש רק במה שטוב לי, ואם לא מוכח זה עוזר לי - אני לא אעשה את זה״

יותר מדי פעמים ראיתי יישומים של סקראם שמודדים את ההצלחה במידת האימוץ של הסקראם (״האם כל הצוותים מקפידים לעשות Daily Stand-up לפי הכללים, כל יום״) יותר ממה שהם מודדים את השיפורים שנעשים בארגון (״האם ה lead time שלנו באמת משתפר בקביעות?״). האם אנחנו תמיד זוכרים מה המטרה, שלשמה אימצנו סקראם?!


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

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

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

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


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




----


[א] ביקורת שהועלתה כמה פעמים לגבי סקראם, היא שסקראם רואה בצורה הפוכה ממש, שני עקרונות ליבה שעליהם חתמו ב Agile Manifesto:
  • Responding to change over following a plan - בסקראם אסור להפריע במהלך הספרינט. הנה סרטון שמלמד זאת בצורה משעשעת (ובמבט לאחור: מעט מבהילה).
  • Individuals and interactions over processes and tools - בסקראם התהליך הוא במרכז, ואפילו מלמדים ״לא להסתמך על סופר-מנים, אלא על תהליך טוב יותר״.
אני יכול להתחבר בנקודות האלו ל-2 נקודות המבט: גם של סקראם, וגם של ה Agile Manifesto. בשתיהן יש היגיון בעיני, אם כי אני נוטה מעט לכיוון הפרשנות של ה Agile Manifesto.



2019-07-07

על פרדיגמות תכנות, ומה ניתן ללמוד מהן בשנת 2019? - חלק ב׳


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





התכנות הפרוצדורלי - מה הוא נתן לנו?


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

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



Functional Decomposition

הרעיון החזק ביותר שהציג התכנות הפרוצדורלי [א] נקרא Functional Decomposition. הוא הציל את עולם התוכנה מכבלי הקדמוניות - אבל מאז הפך גם ל Anti-Pattern מסוכן ונפוץ.

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


הרעיון, הולך בערך כך:

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

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

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

יתרה מכך - הפרוצדורות / פונקציות הללו - הן בסיס טוב ל code reuse. טרנספורמציה על מחרוזת כגון ()trim או ()replaceAllCaptialsWithNonCapitalLetters - יכולה לשמש אותי במקרים רבים נוספים!


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

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


מה הבעיות של Functional Decomposition שהפכו אותו ל Anti-Pattern בעולם ה OO?
  • אי התייחסות / מחסור ב Information Hiding ו Encapsulation:
    • האם ה State הראשוני חשוף לכלל המערכת? האם הוא גלובאלי, למשל? - כך היה בהתחלה.
    • האם כל פונקציה שאפעיל רשאית לעשות כל שינוי ב state?
      אם ה state הוא רשימה של מחרוזות - אז אין בעיה. ככל שה state גדול ומורכב יותר (קיצונות אחרת - לב הנתונים של המערכת) - קל יהיה יותר לאבד שליטה ובקרה: איזה פונקציה עושה מה - על הנתונים הללו.
  • סיכויים גוברים לארגון נחות של המערכת:
    • כאשר הפונקציות מפוזרות במערכת ללא סדר וללא היגיון - קל להגיע לבלאגן.
    • כאשר אין סדר במערכת, סביר יותר שלא אמצא פונקציה קיימת ומספקת - ואכתוב אותה מחדש = קוד כפול.
  • סיכויים גדולים למידול נחות של המערכת:
    • חשיבה על המערכת בצורה של ״נתונים״ ו״טרנספורמציות״ אינו טובות למערכת גדולה ומורכבת. במערכת מורכבת, חשוב מאוד למצוא הפשטות (abstractions) טובות לפרטי המערכת, כך שיתאפשר לנו לחשוב ברמה גבוהה יותר של הפשטה. ה Functional Decomposition לא מוביל אותנו לשם, ואולי אפילו מפריע.
    • ביטוי של הסיכון הזה עשוי להגיע בדמות מודל אנמי - דפוס עיצוב שלילי שכתבתי עליו בהרחבה.

למרות ש Functional Decomposition נשמע כמו נאיביות של העבר, השימוש בו צץ שוב ושוב גם היום -  גם בשנת 2019. בעשור האחרון נתקלתי בעשרות מקרים של ״נסיגה״ מ OO ל Functional Decomposition שלא הועילו למערכת. שווה ללמוד את ה Anti-Pattern הזה ולהבין אם אתם מיישימים אותו במערכת. אולי ההבנה הזו - תעזור לכם לפשט את המערכת, ולהפוך אותה לקלה יותר לשינויים ותחזוקה.

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

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



Modules


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

יש דרכים שונות לארגן קוד לאזורים שונים במערכת. בשפות תכנות שונות, יש וריאציות מעט שונות או לפחות שמות שונים לאותם "מודולים": Packages, Namespaces או Modules - הם שלוש צורות / שמות - לאותו הרעיון:
  • כל פיסת קוד שנכתבת תהיה ״מקוטלגת״ או ״מיוחסת״ לאזור מסוים בקוד - להלן ״מודול״.
  • הקרבה הזו היא קרבה רעיונית, ולא בהכרח פיסות קוד שקוראות אחת לשנייה.
  • אין ברירה, קוד ממודול אחד - יקראו לקוד במודול שני. אבל, אנחנו רוצים לחלק את הקוד למודולים כך, שנצמצם את הקריאות בין המודולים, ונמקסם את הקריאות / ההיכרות בתוך אותו המודול.
    • הרעיון הזה נקרא גם: ״High Cohesion / Low Coupling״. בעברית: אחידות גבוה (בתוך המודל) / תלות נמוכה (בין המודולים).
למרות שרעיון החלוקה למודלים הוא לא מדויק (לא מדויק בהגדרה, ולא מדויק במימוש) - הוא רעיון רב עוצמה שיש עליו קונצנזוס גדול: רבים ממובילי דעת הקהל בעולם התוכנה מאמינים שאם נחלק את המערכת לחלקים ע״פ סדר משמעותי - יהיה יותר קל להשתלט עליה ולנהל אותה. גם אם אין לכך הוכחה מתמטית ברורה.

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



הפרדיגמה מונחית-העצמים (OO)


מי שחי בתקופה בה צמחה הפרדיגמה מונחית העצמים, בעיקר בשנות ה-80 וה-90 - חווה דיון נוקב בהבדלים בין תכנות מונחה-עצמים (OOP), ותכנון מונחה-עצמים, Object-Oriented Design, או בקיצור: OOD.
אפילו לא פעם נשמעה הטענה ש"OOD הצליח - אך OOP נכשל". טענה שנויה במחלוקת, הייתי מוסיף.

אני לא רוצה להשקיע זמן על שחזור הוויכוח, ועל הדקויות הנלוות - ולכן אתייחס בעיקר ל OO כ OOD+OOP.

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

על פייטון ניתן לומר שהיא יותר שפת OOD מאשר שפת OOP... 


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


Everything is an Object

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

לרעיון יש שני פנים:
  • לארוז קוד ביחידות קטנות (יותר) של מודולריזציה - להלן מחלקות. 
  • למדל את המערכת ליחידות המתארות את העולם האמיתי. זה עיקרון ליבה של ה OOD, שלעתים לא מודגש מספיק: זה לא מספיק לחלק את הקוד ל״מחלקות״. חשוב מאוד שהחלוקה הזו תתאר את עולם הבעיה (העסקית) - בצורה נאמנה והגיונית. המיפוי / מידול הזה - מקל על הבנת המערכת, ועל התקשורת לגביה - מה שמקל על לקבוצה גדולה של אנשים להיות שותפים אליה ולעבוד בצורה קוהרנטית.
    • אם בעולם שאותו המערכת משרתת יש מונחים שלא מתבטאים במערכת כאובייקטים - אז כנראה עשינו מידול לא מוצלח.
    • אם הקשר בין האובייקטים לא תואם להיגיון מקובל - אזי כנראה שהמידול שלנו לא מוצלח.
    • אחד מכלי המחשבה למידול לאובייקטים הוא המחשבה על ״תחום אחריות״ (להלן: SRP). אם האובייקט היה בן-אדם, מה הוא היה דורש שרק הוא יעשה? שיהיה רק באחריותו?
      • טכניקה מעניינת של מידול שצמחה, למשל, היא CRC cards.
האובייקטים תוארו בעזרת המטאפורה של כ״תא ביולוגי עצמאי״. לתא יש מעטפת קשיחה המגנה עליו מהעולם (על כך - בהמשך) והוא מכיל בפנים את מה שהוא זקוק לו לפעולה עצמאית. כלומר: אנו מצמדים את הנתונים (state) לפעולות (functions). זה עוזר לנו גם בכדי להגן על ה state - אבל גם בכדי ליצור סדר הגיוני וצפוי במערכת.

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

אם המערכת שלנו בנויה מ"מחלקות של נתונים", ומנגד, מ"מחלקות של פעולות" - אז פספסנו את הרעיון המרכזי הזה של Everything is an Object. זה לא משנה בכלל אם אנחנו עובדים ב Enterprise Java או Shell Script. יותר מהכל, OO הוא לא כלי - אלא רעיון.

אל תבטלו את העניין הזה סתם: אנחנו עדיין כותבים הרבה קוד שלא מציית לרעיון של Everything is an object. הרעיון הזה קל אולי להבנה - אבל לא תמיד קל ליישום.




Information Hiding

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

OO חידשה בכך ששמה את ה Information Hiding כעקרון ליבה מחייב - ולא עוד כהמלצה ״לפעמים כדאי לצמצם את הידיעה של חלק אחד בקוד על חלקים אחרים״. ב OO מדגישים: ״בכל מקום במערכת אנחנו נסתיר כל מה שלא נחוץ - לחלקים האחרים״.
  • Encapsulation הוא השילוב של הרעיון הזה, בשילוב הרעיון של Everything in an Object. ההכמסה היא ״קרום התא הביולוגי״ המגן כל פנים התא הרגיש (ה state הפנימי) - מהשפעות חיצוניות.
    בשפות OO לרוב יש לנו directives הנותנים לנו לשלוט על הנראות: private, package, protected - וכו'.
    • אובייקטים הרוצה משהו מאובייקט אחר, שולח לו ״הודעות״. לא עוד אלך ואפשפש לי (ואולי גם אשנה) State של חלק אחר במערכת. יש מעתה Gate Keeper, שלו אני שולח רק את המידע הנדרש ("ההודעה") - ואקבל בחזרה - תשובה.
      • אם אתם יוצרים הודעות בין אובייקטים ושולחים את כל ה State של האובייקט, אז אתם מממשים משהו שקרוב יותר לתכנות פרוצדורלי - מאשר ל OO.
  • יש צורות נוספות של Information Hiding מלבד הכמסה של אובייקטים:
    • הפרדה של בסיס הנתונים לסכמות, תוך הגבלת הגישה לסכמות השונות - הוא information hiding
    • תכנון המערכת כך שיהיה צורך של פחות אובייקטים להכיר אחד את השני - הוא information hiding מעבר להכמסה של האובייקט הבודד.
    • העברה ל Front-End רק את פיסות המידע הנדרשות (לפעמים נדרש עיבוד נוסף לצורך כך) - הוא information hiding. 
  • המוטיבציה העיקרית ל Information Hiding היא היכולת לבצע שינויים בחלקים מסוימים במערכת, בלי שהדבר יגרור צורך לשינויים באזורים נוספים. להגביל את גודל השינוי.
    • אם אני לא יודע על חלקים אחרים - שינוי בהם לא אמור לדרוש ממני להתעדכן.
    • זה לא תמיד נכון. לפעמים יש השפעות עקיפות - שכן ידרשו ממני להתעדכן. למשל: סדר שונה של פעולות שמרחש במערכת. לא נחשף לי מידע חדש / שונה, אבל עדיין אני צריך להתאים את עצמי.

לרעיון של Information Hiding (או הכמסה: אנו נוטים היום להשתמש במונחים לחליפין) - יש מחיר: הסתרה של נתונים דורשת עבודה נוספת - עוד כתיבת קוד, וחשיבה ותשומת לב - שבעקבותיה עוד כתיבת / שינוי קוד.

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

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






הורשה

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

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

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

שלא תגידו שלא ידעתם.


non-Objects

מהה?? אם "Everything is an Object" - אז איך יש דברים שאינם אובייקטים?

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

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

ג׳אווה, שפה מאוד פופולארית ומשפיעה - דילגה על ההבחנה בין מחלקות למבני-נתונים. אם אנחנו רוצים להגדיר בה מבנה-נתונים - אנחנו משתמשים ב classes. בעצם כך, שפת ג'אווה "מנעה" במידת מה, מההבחנה בין מחלקות למבני-נתונים לחלחל בתעשייה.
  • האם Map או List הם מחלקות? לא - אלו הם מבני-נתונים. כל מטרתם היא להחזיק ולחשוף נתונים. הכמסה? זה לא הקטע שלהן [ב].
  • האם DTOs או Enum הם מחלקות? לא - אלו מבני-נתונים. בשפת ג׳אווה נממש DTOs כ Class, אך מבחינה רעיונית הן לא מחלקות: אין להן הכמסה [ב], ואין להם אחריות לתאר פעולות בעולם.
    • בג'אווה הרבה פעמים כאשר ממשמשים DTO (לרוב מצוין ע"פ Suffix בשם) - מאפשרים לכל השדות של ה DTO להיות public - ומדלגים על השימוש ב getter/setters. הגיוני.
חוסר ההבחנה בין מחלקות ומבני-נתונים לא מאפיין את כל השפות: ב #C קיימים structs, בקוטלין יש Data Classes, ובפייטון היו named tuples כסוג של ייצוג, ולאחרונה הוסיפו גם Data classes.
אפשר להסתכל על המבנים הללו רק ככלי לאופטימיזציה של קוד/ביצועים בשפה - אבל שימושי הרבה יותר להשתמש בהם ככלי להבחנה סמנטית בין אלמנטים שונים בתוכנה: מחלקות, ומבני-נתונים.

יש כמה סימנים שיעזרו לנו לזהות מבני-נתונים:
  • על מבני-נתונים אין פעולות, לפחות לא כאלו שמבצעות Business Logic.
  • מחלקות יכולות להכיר מבני-נתונים, אך מבני נתונים לעולם לא יכירו מחלקות.
  • אם שדות של מחלקה השתנו, זה יכול להיגמר בלי השפעה סביבתית. אם שדות של מבנה-נתונים משתנים - הם כמעט תמיד יגררו שינויים במחלקות אחרות. למה? כי זו מטרתם בחיים: להעביר נתונים בין מחלקות, וגם מ/אל מערכות חיצוניות (קריאה ל API, שליפה מבסיס נתונים, וכו׳).

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



סיכום


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

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


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



-----

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

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

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


2019-06-30

על פרדיגמות תכנות, ומה ניתן ללמוד מהן בשנת 2019?


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

בתכנות ישנן כמה פרדיגמות תכנות מקובלות. הפרדיגמות המשפיעות ביותר כיום הן פרדיגמת ה Objection-Orientation (בקיצור: OO) ו Functional Programming (תכנות פונקציונלי) - אך גם יש אחרות מוכרות יותר ופחות.


באיזה פרדיגמה אתם מאמינים? עם איזו פרדיגמה אתם עובדים?

עבור הרוב המוחץ של אנשי-התוכנה, התשובה היא זו:

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

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



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


מה עדיף?



איזו פרדיגמה היא הטובה ביותר?

אולי OO? - שהיא (עדיין) הדומיננטית, והמשפיעה ביותר ב 20-30 שנה האחרונות?
ואלי תכנות פונקציונלי שמרגיש חדש יותר (הוא בעצם קדם ל OO) - וזוכה להרבה מומנטום מחודש בעשור האחרון?


אם אנחנו הולכים לכתוב קוד קצר בן כמה עשרות שורות קוד - שימוש ב OO, או תכנות פונקציונלי, או Reactive Programming - יהיה סיבוך ובזבוז זמן!

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

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

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


בעצם, משם הכל התחיל [א]: מפרדיגמות מאוד פשוטות, עבור תוכנות פשוטות, שרצו על חומרה - שלא יכלה לתמוך בתוכנות מורכבות יותר:



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

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


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

למשל: בתיאורים המקודמים של Alan Key ל Object-Oriented - יש דגש גדול על העיקרון ש"אובייקטים מתקשרים ע"י שליחת וקבלת הודעות". אפשר להסביר למה ההדגשות הללו היו חשובות בזמנו, ומה משמעותן, אבל היום ההגדרות הללו נכנסות תחת הגדרת ההכמסה (Encapsulation). כלומר: אובייקט שולח הודעות - כי הוא לא יכול לגשת לנתונים הפנימיים של האובייקט האחר בעצמו. בכלל, המונח "שליחת הודעות" מתקשר היום חזק יותר לפרדיגמה של Event-Driven - וזה כבר נושא אחר.



הדברים מסתבכים


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

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

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

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

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

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

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


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

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

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

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

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

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


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

הדיונים על נכונות השימוש בפרדיגמה הם כמובן חשובים: לא כל פרדיגמה מורכבת יותר - היא אכן טובה יותר. לאימוץ פרדיגמות יש מחיר ניכר, ולא כל פרדיגמה ש"תפסה" בארגון X - תצליח בהכרח גם בארגון Y. למשל: ב C עדיין משתמשים במשפטי goto - בעיקר בשם היעילות הגבוהה (קרבה למעבד).
בקרנל של לינוקס, למשל, יש כ 13K משפטי goto (בדקתי הרגע).

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

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

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

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

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

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

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

הורשה מרובה, למשל - היא לא בהכרח דבר רע, וב 90% מהפעמים - אפשר להשתמש בה בצורה נשלטת. אני זוכר שבעבודה עם ++C היו שטענו שכל הפחד מהורשה-מרובה - הוא מוגזם.
10% מהפעמים שבהם היא יצאה משליטה - היו מספיק קשות -- שהתעשייה (!) באופן גורף החליטה לזנוח את האפשרות הזו.

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

גם היום, עדיין יש בשפות, וב Frameworks יכולות בעייתיות: שמדי פעם מקצרות את הקוד - אבל בפעמים אחרות הן זרז לבעיה. מדוע חשיש יצא מחוץ לחוק ואלכוהול לא? - קשה לומר.

חשוב לזהות אלמנטים במערכת שגורמים לבעיות רבות מדי - ולחסום אותם. אני בטוח שיהיו מתנגדים, אבל זכרו שמערכות לא הופכות עם הזמן לפשוטות יותר, או קלות יותר לתחזוקה. הסתמכות על משמעת ודיוק של כל וכל מהנדס במערכת - היא מתכון לכישלון.
על זה נאמר: "It must be simple - or it simply won't be"




?Monty Python: What have the Romans ever done for us
(זכורה לי טענה שזה במקור דיון שהופיע בתלמוד -- אבל לא מצאתי מקורות המאשרים זאת)


אז מה הפרדיגמות המוקדמות תרמו לנו?



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



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

למשל: המצאת ה"בלוק". בתחילה - לא היה דבר כזה.

למשל: משפטי if היו יכולים באותה התקופה להפעיל רק ביטוי (expression) יחיד. לו היו 4 פעולות שלא היינו רוצים שיפעלו אם x הוא שלילי - היה עלינו לכתוב 4 משפטי if שכל אחד חוזר על הבדיקה "האם x שלילי" ואז מבצע עוד פעולה.

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

דוגמה אחרת היא משפטי ה GOTO המפורסמים. תוכנה היא לא תמיד רצף סדרתי של פעולות: לפעמים רוצים לקחת דרך אחרת. למעבר יש יכולת לעשות מעבר ולהריץ קוד ממקום אחר בזיכרון, התרגום הישיר של היכולת הזו היא פקודת GOTO (או JMP - אם אתם חושבים באסמבלי). יישום ראשוני של GOTO היה מעין תחליף פרימיטיבי לפונקציות: לך לשורה 300, ואז שורה 306 מחזירה אותנו לשורה 28 ממנה באנו. שימושים נוספים היו טיפול בשגיאות וניקוי זיכרון - מה שבשפות מודרניות כבר זוכה לטיפול ראוי יותר.

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

סיזיפי? סיזיפי אך אפשרי? - כך נראתה החלוציות בתחום התוכנה...

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

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


ארצה להדגיש שני עקרונות שצצו מהגישה הזו ושווים דיון, גם ממרום שנת 2019:


Scoping [ב]

הרעיון אומר כך: כאשר אנו נותנים שם (name binding) למשתנה, פונקציה, קבוע, וכו' - נאפשר להגביל את מרחב הקוד שיהיה חשוף לשם הזה.

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

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

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

היום, בעולם ה IDE המתקדם - המשמעות המידית של scoping היא מה יציע לנו ה IDE ב Auto-complete. אם נגדיר הכל ב Scope הגלובלי - כל הקלדה תציע לנו את כל מה שיש.
אם נקפיד על הגדרת ישויות ב scopes מצומצמים ככל הניתן - הצעות ה Auto-complete יהפכו לממוקדות, והרבה יותר רלוונטיות.

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

מתלבטים מה עדיף?
- אני לא.

שווה לציין ש Scoping הוא צעד ראשון בכיוון של Information hiding ו Encapsulation. שוב ושוב עלו, לאורך ההיסטוריה - רעיונות בכיוון הזה.






Single Exit Principle

בכדי להגדיר מהו מבנה צפוי ומנוהל של רצף הרצת התוכנה, ולהפסיק "לקפוץ לשורה אחרת בקוד", הגדירו בפרדיגמת התכנות המובנה את "עיקרון היציאה האחידה" (מפונקציה). שם נוסף:"Single Entry, Single Exit". לכל בלוק קוד (=פונקציה) צריך להיות רק מקום אחד להיכנס דרכו - ורק מקום אחד שניתן לצאת ממנו.


העיקרון הזה הוא מובנה בשפות שאנו עובדים איתן - עם כמה חריגות. קשה מאוד להסתדר ב 100% מהמקרים עם Single Exit יחיד.

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

הנה דוגמאות עיקריות:
  • Exceptions שנזרקים - הם לא Single Exit.
  • continue או break בלולאה ל Label (בג'אווה, למשל) - הם לא Single Exit.
  • אפשר גם לטעון ש break ו continue באופן כללי - הם לא Single Exit, לא ברמת הבלוק.
    • בשפות פונקציונליות - לעתים אין להם מקבילה.
  • coroutines (על עוד לא ממתינים באותו הבלוק לסיומם) - הם גם לא Single Exit. הפעלנו קוד - שרק בונה-עולם יודע מתי בדיוק הוא יסתיים.
  • אחרון וחביב: יש גם מי שרואים בקריאת return מתוך גוף הפונקציה - הפרה של עקרון ה Single Exit. אמנם נקודת היציאה נתונה וקבועה - אך דילגנו על קוד - להגיע אליה. הטענה העיקרית - היא שזה לא צפוי. אני מצפה שהפונקציה תגיע תמיד לשורתה האחרונה.

אין לי פה איזה המלצה או מסר גורף לגבי רעיון ה Single Exist - אבל אני חושב ששווה להכיר אותו.
אני, אישית, שמח שיש break ו continue בלולאות. אני מרגיש נוח להשתמש ב return מתוך גוף של פונקציה (יש לזה מחיר מסוים - אבל האלטרנטיבה פחות טובה לדעתי).




סיכום



שוב לא הצלחתי לסיים את הפוסט ב 400 מילים.

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

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

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



----

[א] דילגתי עם קוד מכונה, אסמבלי, ועל הדיון של המכונה של טיורינג לזו של פון-ניומן. זו היסטוריה low level מדי, ולא מעניינת לדיון.

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


2019-06-01

שיעור מס' 8 בהורשה


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

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

לצורך הדיון אני אציג רק ממשקים, ולא אתעכב על חלוקה ל Interfaces ו Concrete classes (שהוא באמת משני לדיון).


בואו נתחיל.


הבעיה


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


מיקום הצורה (x, y), לצורך הדיון - הוא לא חלק מההגדרה.

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

למשל: אנו יכולים לייצר Line עם fillColor - מצב לא תקין. הכוונה להשתמש ב fillColor היא רק עבור פוליגון.

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

כיצד ניתן למדל את האובייקט בצורה יותר נכונה?

יש לכם איזה רעיון?!



פתרון ראשון




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

בואו נבחן את המודל שלנו עכשיו:
  • DRY (חוסר חזרה על הקוד) - מצב מצוין. אין באמת הרבה מה לקצר את הקוד.
  • הכמסה (encapsulation) - מצב בינוני. על הפשטת העל יש כמעט את כל התכונות, מה שאומר שכל מי ש"אוחז" ברפרנס ל BaseShape יקבל גישה לכל התכונות הללו. האם יש לזה צורך? או האם "שיתפנו" יותר מדי?
    הנחת היסוד היא שהשימוש האחיד באובייקטים הוא בעזרת הפונקציה ()draw. נניח: רוצים לרוץ על מערך ולצייר את כולם, אך בכל מקרה, כל גישה מעמיקה יותר דורשת את בדיקת הטיפוס ו downcasting. 
    • אין נזק מיידי באי-הכמסה ("אז שלא ישתמשו בזה! מה הבעיה?!") - אבל זה זרז לשימוש לא מבוקר בתכונות הללו. מפתח שמקבל הצעה לשימוש בתכונה ב autocomplete של ה IDE - לעתים רחוקות ישאל את עצמו אם נכון היה שהאובייקט יחשוף תכונה זו, או האם בכלל נכון להשתמש בה.
    • כל מפתח יכול להיכנס לאובייקט ולשנות את כל שדות האובייקט ל Public - אבל זה סייג המעורר הערכה מחודשת על נכונות הפעולה (ברוב, התקין, של המקרים).
  •  אמינות המודל למציאות / פשטות הבנה (presentation) - הרבה יותר טוב, אך עדיין ישנם עיוותים. 
    • הסרנו את ה Path כתכונה, ופתרנו את הבעיה בה fillColor זמין להיכן שאינו רלוונטי - שזה מצוין. 
    • שמות המשתנים אינם טובים. הם "פשרה" עבור "שימוש חוזר בקוד" / או סתם מכח האינרציה - פשרה לא טובה.

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

  • אנו מספקים את הצורך הבסיסי של המערכת ביישות לאחסן בה נתונים / להגדיר פונקציות. לחלופין זה היה יכול להיות מערך גלובאלי של נתונים. זו חובה כדי שהקוד יעבוד - אך זה קו האפס מבחינת הנדסת תוכנה.
  • אנו מתארים כוונה / רעיון - שאנו רוצים להנחיל לשותפנו לבסיס-הקוד (להלן Presentation), ובתקווה שקבענו את הכוונה / רעיון בצורה טובה ואמינה לעולם העסקי הרלוונטי.
  • אנו מגבילים את חשיפת הידע במערכת, בעזרת הכמסה - אם אנחנו עובדים לפי מודל Object Oriented.
  • כן, פרקטיקה טובה של קוד, היא לא לשכפל קוד (DRY = Don't Repeat Yourself) - אבל זו לא ממש מטרה בהגדרת אובייקט.

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

כדי לחדד מדוע האובייקטים בדוגמה שלנו אינם ממודלים בצורה טובה, נשטח שנייה את האובייקטים מהיררכיית שבה הם בנויים, ונציג אותם כפי שיופיעו ב run-time, או למשל - ב IDE כאשר אנו מקבלים המלצות שימוש ב autocomplete:


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

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

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

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

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


פתרון שני



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

עבור Point, הרבה יותר מדויק להסביר שתכונה מסוימת היא קוטר (או לחלופין זה יכול היה להיות רדיוס) - ולא "line width", שמשאיר מקום לפרשנות. בוודאי שיותר טבעי לי לחשוב על color ולא "line color" - כי אין קו בנקודה.
ב Polygon יותר מדויק לדבר על Border ולא Line, עניין של דייקנות - שיכולה להשתלם מאוד לאורך חיי-מערכת.

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

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

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

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

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

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



סיכום


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

DRY היא פרקטיקה חשובה מאוד - אבל יותר באזור של תוכן הפונקציות: יש כמה שורות קוד שחוזרות על עצמן? הקפידו להוציא אותן לפונקציה משותפת.
יש משתנים כפולים (או state כפול כלשהו)? אבוי - היפטרו מהעותקים מיד! ("data duplication is the mother of all Evil").

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


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


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



2019-05-29

JavaScript ES6/7/8 - להשלים פערים, ומהר - חלק ג' ואחרון


פוסט שלישי ואחרון בסדרה:
  • בחלק הראשון - כיסינו את let ו const, חידושים בגזרת הפונקציות, ו spread operator השימושי.
  • בחלק השני - כיסינו Classes ו Modules - וגם הזכרנו בקצרה יכולת ליבה חשובה בשם Promises.
בחלק הזה, נכסה עוד כמה אלמנטים בתחביר שעשויים להיות לא-מובנים, נכסה יכולות מתקדמות כמו Symbols ו Generators - ונסיים ב Async-Await - המנגנון שמפשט את השימוש ב Promises, וכנראה שנשתמש בו ברוב המקרים.


בואו נתחיל!



שימושים שונים ל [ ]


ב ES5 אנו רגילים לסוגריים המרובעים - כמגדירים של רשימה. ב ES6 נראה אותם בעוד כמה וריאציות. למשל:


אמנם Array בשפה הוא iterable, אבל כאשר אנו רוצים להפעיל לולאה עם האינדקס, אנו משתמשים בפונקציה בשם ()entries המחזירה iterator של האיברים עם האינדקס שלהם. כלומר: בכל אינטרקציה אנו נקבל מערך [index, item]. מכאן הכי טבעי להשתמש ב deconstructing assignment על מנת לקבל את הערכים.

שימו לב ש for x... of XList הוא פורמט חדש ללולאה, המבוססת על iterables. ברור.



Computed property names

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

  1. יצרנו לאובייקט תכונה ששמה מגיע ממשתנה חיצוני - ולכן עשוי להשתנות עם הזמן.
  2. הנה אנחנו ממש מפעילים חישוב כדי לבנות את שם ה property. אפשר ומותר.
  3. האובייקט שנוצר - מייצג את תוצאות החישוב.
    1. הנה אפשר לגשת לתכונות ששמן נוצר ע"י חישוב - באופן רגיל.
    2. ניתן גם להשתמש בתחביר הסוגריים המרובעים לשלוף תכונה מתוך האובייקט. האמת, שגם ב ES5 ניתן לגשת לתכונה עם שם דינאמי באובייקט, אם כי בצורה קצת אחרת: פשוט צריך להרכיב מחרוזת של השם שלה.
    3. התחביר של computer property name עובד רק ב context של אובייקט. ב contexts אחרים, לסוגריים מרובעים יהיו משמעויות אחרות.

אז מה בעצם השימוש לתחביר החדש הזה של computer properties? האם TC39 ניסו בכח לבלבל אותנו עם משמעויות שונות לסימנים מוכרים?!

אני מקווה שלא.

הנה שימוש טיפוסי אחד: יצירה דינאמית של אובייקטים (או Classes):


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



Symbols

ל ES6 נוסף טיפוס פרימיטיבי חדש בשפה, בשם Symbol.

אולי אתם מכירים Symbol מ Ruby, Elixier או Closure (או Scala. הכל יש בסקאלה). אמנם יש דמיון, אבל זה לא אותו הדבר. ב ES6 השימוש הוא שונה, ובעיקר סובב סביב הוספת metadata לאובייקטים, והוספת יכולות חדשות לשפה בצורה תואמת-לאחור.

נתחיל בדוגמה:


באובייקט customer1 הגדרנו 2 שדות: שם ומזהה לקוח. משום מה, שדה ה customerId לא זמין לרוב הפעולות בשפה - אנחנו רואים רק את השדה name. מוזר!

הפונקציה ()Symbol מייצרת עבורנו מופע חדש (וייחודי, Singleton) של symbol. המחרוזת שמועברת לה - משמשת כמזהה ל Symbol.

Symbol נשמע כ property "בלתי נראה" על האובייקט. לא ניתן property שהוא Symbol מעזרת מפתח שהוא מחרוזת, והוא לא יתנגש עם properties עם "שם" זהה (כפי שאמרנו, אין לו שם). אפשר לחשוב על Symbols כ properties ביקום מקביל.

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

  1. אפשר לשלוף ערך מאובייקט - ע״פ ה Symbol. אם לא קיים - יחזור undefined.
  2. אפשר לקבל את כל שמות ה Symbols על אובייקט - בעזרת הפונקציה ()Object.getOwnPropertySymbols
  3. למרות שהגדרנו 2 symbols שונים עם אותו "מזהה" CUSTOMER_ID - הם שונים. המזהה שלהם הוא המופע של פרמיטיב ה Symbol - ויש לעשות בו שימוש-חוזר! עובדה: יש לנו 2 symbols עם אותו "מזהה" על האובייקט - אבל הם בעצם Symbols שונים לחלוטין.

בלי להרחיב יותר מדי, אציין רק של Symbols יש global registry שדרכו אפשר לקבל את ה instance (הייחודי, singleton) של Symbol מסוים - מבלי ליצור תלויות של כל הקוד בקובץ יחיד. הפונקציה בעזרתה מקבלים מופע של Symbol נקראת (...)Symbol.for. לרוב מי שישתמש ב Symbols הם Frameworks. אם אתם רוצים להבין Symbols יותר לעומק - אני ממליץ על המאמר המקיף הזה.

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

הפתרון הוא בשימוש ב symbols - כך שלא תהיה התנגשויות. בכדי להגדיר אובייקט כ Iterable עלינו לממש פונקציה בשם ()[Symbol.iterator], כאשר Symbol.iterator הוא Symbol של השפה. פונקציות הרי רשומות כ property על האובייקט (או prototype) - וה property הזה יכול להיות גם Symbol.



Generators


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

Generators מייצרים פונקציות שלא מבוצעות במלואן בקריאה אליהן - אלה רק בסדרה של קריאות, בעצם סוג של iterator. הסימון שלהן בשפה: *function.

כלומר: Generator הוא פונקציה שמייצרת iterator.

מה השוני שלהן מ [Symbol.iterator] או מפונקציה שמחזירה פונקציה? יש לפונקציה שנוצרה ע״י Generator כמה תכונות מיוחדות ומתקדמות - המנוהלות ברמת המפרשן. השימוש העיקרי של Generators הוא לנהל בצורה קלה לקריאה סדרה של פעולות, בד"כ אסינכרוניות - בעלות קשר אחת לשנייה.

הנה תיאור ההתנהגות הבסיסית:


ה Generator מייצר את ה iterator מאחורי הקלעים. גוף הפונקציה שלו - היא בעצם ה template לקוד של כל iterator - כאשר המפרשן מנהל את ה state של ה iterator הזה: הוא זוכר היכן בדיוק עצרנו, ואת ערכי המשתנים ב scope הפונקציה באותה הנקודה (קרי: ה activation frame).

yield היא מילה שמורה חדשה בשפה - המשמשת את ה Generators. המשמעות שלה היא כמו return שגורם לשמירת ה state של ה iterator: כשנקרא ל ()next פעם נוספת - נמשיך בדיוק מהשורה הבאה אחרי זו שממנה יצאנו, ועם אותם המשתנים.

הנה דוגמה קצת יותר מציאותית:

  1. כשמגדירים generator מתוך מחלקה, אין צורך במילה function - נשארת רק הכוכבית (*).
  2. ה Generator בעצם מתאר רצף פעולות שנדרש כדי להשיג תוצאה כלשהי. במקרה שלנו, בכדי להגיע לפרטי התשלום של הלקוח:
    1. קודם צריך למצוא account token
    2. איתו לעשות login
    3. עם ה token הזה לשלוף את המידע על התשלומים.
כלומר ה generator מתאר את הקשר בין הפעולות והסדר שלהן - אבל הוא לא מבצע אותן. מי שיבצע אותן הוא מי שיקרא ל iterator, ויש פה הפרדה בין הגדרת סדר הפעולות (ה Generator) לביצוע שלהן (הפעלת ה iterator).

האם זה לא מעצבן לקרוא לפונקציה next, next, next בכדי שתסיים את הפעולה? למה לא לכתוב פונקציה רגילה שמבצעת ברצף את כל סדר הפעולות?

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

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

תכונה חשובה של generators היא יכולת מובנה להתערב בשלבים. אם אנחנו שולחים ערך לפונקציה ()next - הערך הזה יחליף את הערך ב state של האיטרטור:


סה"כ generators נחשבים פעולה מעט low-level עבור רוב המפתחים, ורבים שכן עבדו עם generators מצאו את עצמם עוטפים את התשובות ב Promises. מנגנון בשפה בשם Async / Await מסתמך על מנגנון ה generators בכדי לתת לנו רמת הפשטה גבוהה ונוחה מאוד לשימוש - לביצוע רצף פעולות אסינכרוני.



Async - Await


בפשט אפשר לומר ש Async-Await הוא Syntactic Sugar שהופך את השימוש ב Promises לקל ואלגנטי יותר.
עוד נוסחה שמוזכרת הרבה היא ״Async-Await ≈ Generators + Promises״ - אל דאגה! השימוש ב Generators הוא פרט מימוש מאחורי הקלעים.

Promises הם בהחלט שיפור יחסית לעבודה עם callbacks - אבל כשהשימוש בהם נעשה רחב, התגלה שגם להם יש כמה נקודות לא-נוחות.
Async Await הוא שיפור נוסף מעל Promises. מתכנתי ג׳אווהסקריפט ותיקים בוודאי קצת מקנאים במתכנתים חדשים שנוחתים לכל הנוחות הזו...


תחילה ארצה להציג את 2 הבעיות העיקריות שב Promises. אציין ש Async-Await גם יותר יעיל ברמת הביצוע וצריכת הזיכרון - במידה ואתם מריצים המון קוד מקבילי.


טיפול בכישלונות - בשני אופנים שונים:


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


Control Flow נוטה להסתבך - החלוקה לפונקציות הנשלחות ל then

בעבודה עם Promises קל להגיע לקוד מהסוג הבא (דוגמה מפושטת):


אולי אני צריך לבצע קריאה אסינכרונית ואולי לא - קשה לי לשלב את המשך ה flow בצורה אלגנטית בלי להוציא חלק מהקוד לפונקציה שניה / אולי לעטוף קטע קוד סינכרוני ב Promise בשביל הסימטריה...

הנה אותו הקוד עם async-await:


בהחלט קוד יותר אלגנטי, ושיותר קל לעבוד איתו!


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


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



את המילה השמורה await ניתן להציב רק בתוך פונקציה שהוגדרה כ Async.
הצבה שלה לפני Promise תגרום למפרשן להמתין עד של promise יש תשובה - ואז היא מחזירה אותה.

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

בעצם מאחורי הקלעים, המפרשן מייצר מפונקציית ה async מופע של Generator ומשתמש ב yield בכל נקודת await. ברגע שה Promise מוכן עם תשובה - הוא יקרא שוב ל Generator - שימשיך מהנקודה האחרונה.

זהו Syntactic Sugar לא קטן - ואין טעם לנסות לממש זאת לבד!


למרות ש await יוצר סדרתיות ברצת הפונקציה, עדיין אפשר להשתמש בכלים של promises על מנת להריץ קוד במקביל:


שימו לב שבדוגמה זו, לא עשינו כלום עם הערך שחזר מהביטוי "(...)await Promise.all". אם קרתה שם תקלה - לא נדע ממנה. אם היינו משתמשים ב return לערך - תקלה הייתה מתרגמת ל Exception.


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



התשובה היא:

a start
b start
a end
b end

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

לו היינו מוסיפים ביטוי await גם על הפעלת הפונקציה b, התוצאה תהיה:

a start
b start
b end
a end

במקרה זה פונקציה a נערצת בקריאה "await b" והפונקציה b נעצרת קריאת ה await שלה.
רק לאחר שפונקציה b מסתיימת - ה await שבפונקציה a משתחרר - וממשיך לסיומה.

זה עשוי להיות מבלבל - אבל זו המציאות בעבודה עם Async-Await - שיש לשים לב אליה.


סיכום


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

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


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



2019-05-20

JavaScript ES6/7/8 - להשלים פערים, ומהר - חלק ב'


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

נוספו הרבה Utilities לשפה ב ES6: מבני נתונים הם היום iterable, וניתן להפעיל עליהם פונקציות
()find(), sort(), filter(), foreach(), map וכו'

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

יש עוד תוספות נחמדות לשפה: האובייקט String קיבל סדרה של מתודות שימושיות כמו ()startsWith או ()includes. מה שהייתם מצפים.

הספריות הסטנדרטיות  עברו מודרניזציה. נוספו יכולות internationalization כגון Intl.DateTimeFormat או Intl.NumberFormat. מי שזקוק בוודאי יבין מיד את היתרונות.

נוספו מבני נתונים כמו Map או Set.

כאן אולי עולה השאלה, למה צריך Map עם הקלות של ייצוג Dictionary על אובייקט {}?

בגדול Map הוא iterable וניתן לעשות על ה entries שלו פעולות כמו map/filter, ויש לו כמה פונקציות/תכונות שימושיות שלא זמינות לאובייקט. אבל הייתרון הגדול בשימוש ב Map לדעתי - היא בקריאות בקוד, כאשר שימוש ב Map מדגיש את הכוונה: פשוט לאחסן זוגות איברים, ותו לא.

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

מטרת הפוסט היא להתמקד בתחביר חדש / ייחודי ב ES6 שקשה להבין לבד.
לספק לכם כלים להבין את התחביר המורכב, ומה שמתרחש מאחוריו.

נצא לדרך!






השלמות


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

האם קטע קוד הבא מובן לחלוטין? נגענו ברוב האלמנטים - אבל לא חיברנו אותם לגמרי:


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

ביצירת אובייקטים ב ES6, אם ה key וה value בעלי שמות זהים, במקום לכתוב height: height - אפשר פשוט לכתוב height.

הנה ההפעלה:


אני מקווה שזה פשוט הגיוני.


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

  1. ההגדרה:
    1. אנו מציבים את הערך h של ה destructed object בתוך המשתנה height.
      הייתי רוצה להמליץ ליוצרי השפה על תחביר חץ כגון: height <- h = 6  שהיה אולי יותר ברור - אבל כבר מאוחר מדי.
    2. סיפקנו ערכי ברירת-מחדל, ב-2 רמות: גם אובייקט, וגם ערכים לשדות.
  2. ההפעלה:
    1. כשלא סופק אובייקט, ברירת המחדל היא האובייקט שסופק.
    2. סופק אובייקט, אך properties החסרים בערך - יקבלו ערך ברירת-מחדל.
    3. השם הארגומנט הוא h ולא height, ולכן שליחת אובייקט עם property בשם height - הוא חסר משמעות (אם כי ניתן להתבלבל).

שתי דוגמאות אחרונות לסיום ההשלמות:

  1. יש לנו Arrow Function שמחזירה אובייקט. המפרשן של ג'אווהסקריפט עשוי לא להבין בצורה חד-ערכית למה התכוונו (?! אולי זה destruction של אובייקט). הפתרון התחבירי - לעטוף את גוף הפונקציה בסוגריים.
    אם היה מדובר ב object deconstruction - היינו עוטפים בסוגריים את כל הביטוי (אגף ימין + שמאל של ההשמה).
  2. מה זו שרשרת החצים הזו? מאוד הגיוני: פונקציה שמחזירה פונקציה. אתם כנראה תתקלו בכאלו. 
    1. הנה ההפעלה: ההפעלה הראשונה (basePort = 80) מקבלת פונקציה, וההפעלה השנייה (distance = 100) מפעילה את הפונקציה שהתקבלה. אוי, יצא מספר מוכר!

זהו. סיימנו את ההשלמות ואפשר להמשיך הלאה.


Classes


ES6 הציגה Syntactic sugar להגדרת Classes. כלומר: נוספה מילה שמורה class שעוזרת להגדיר class, אבל זה אינו מבנה שמציג יכולות חדשות בשפה - אלא רק מקצר כתיבה, וחוסך התעסקות עם prototype. מאחורי הקלעים נוצר קוד שיכולנו לכתוב גם ב ES5:


באופן דומה, יש גם הורשה:


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

כלומר: לא קיבלנו Full-fledged classes מובנים בשפה - אך קיבלנו כלי שבהחלט כדאי להשתמש בו.

עוד 2 דברים שניתן להגדיר על מחלקות הם getters/setters, ו static members - הזמינים רק מתוך המחלקה, ולא מתוך המופע (כלומר: ב ES5 אלו properties שיושבים על המצביע ל constructor ולא על ה prototype):


זהו. עכשיו אתם מכירים classes ב ES6. מזל טוב!


Modules


ההפרדה ב JavaScript בין קטעי קוד מקבצים שונים ("מודולים") צמחה מ 2 תקנים: CommonJs הסינכרוני (NodeJs) ו AMD האסינכרונית (בדפדפן, המימוש הנפוץ נקרא Require.js - כתבתי עליו פוסט בזמנו).

הגדרות המודולים הבשילו - והיום הם חלק מהתקן של השפה. הם נמצאים במלואם בתקן - אבל לא כל פרטי המימוש זמינים לרוחב כל המנועים השונים. הדפדפנים המודרניים תומכים היום בטעינה של מודולים בנוסח <script type=”module”> וחלקם גם ב dynamic import. עדיין מדובר ב 75-85% מהמשתמשים בלבד (בעת כתיבת הפוסט, ע"פ caniuse) - משהו שקשה מאוד להסתמך עליו.

הפתרון הפשוט היום הוא להשתמש בכלי להרכבת קבצי ה source ל bundle - כמו WebPack או Parcel, ע"מ לקבל תמיכה במודולים בדפדפן - משהו שרבים מאיתנו כבר עושים היום, בכל מקרה.

בצד השרת (NodeJs) התמיכה הרשמית במודולים החלה בגרסה 12.
בגרסאות ישנות של Node, זהו פיצ'ר ניסיוני שאפשר להדליק עם feature flag - או שאפשר לקבל אותו מספריות צד-שלישי.

בכל מקרה, הכיוון הוא לאמץ מודולים, וגם אם אתם לא משתמשים בהם כרגע - סביר שתשתמשו בקרוב. את דוגמאות הקוד בפוסט אני מריץ על node 12.

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

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

הנה האופנים בהם ניתן להחצין אלמנטים במודול, שימו לב שענייני ה import/export הם עניין שבו נוטים להתבלבל:

  1. אנו מוסיפים את המילה השמורה export לפני ההגדרה (משתנה, פונקציה, וכו') כדי להחצין את ההגדרה. פעולה זו נקראת named export.
  2. אנו מחצינים שורה של אלמנטים בפקודה בודדת. הסוגריים המסולסלים עוטפים את האלמנטים. זהו גם named export.
  3. אנו משתמשים בפקודה מיוחדת בשם default export המחצינה אובייקט - ויכולה להיות מוגדרת לכל היותר פעם אחת בקובץ. 
    1. מי שיבצע import למודול יוכל לתת איזה שם שירצה לאובייקט הזה.
    2. הסוגריים המסולסלים מגדירים אובייקט בו במקום. יכולנו גם לקרוא ל export default עם רפרנס לאובייקט שנוצר קודם לכן.
  4. אפשר לערבב את ה default export בתוך פעולת export של מספר איברים אחרים. גישה זו איננה מומלצת! 
פרקטיקה מקובלת היא להשתמש ב default export בלבד, על מנת שיהיה מקום אחד ברור שמציין מה בדיוק מוחצן מהמודול - ולשים את פעולת ה default export בסוף הקובץ. זוהי פרקטיקה הלקוחה מ CommonJS.

דרך אחרת היא להחליט להשתמש ב export על כל אלמנט שאנו רוצים להחצין, כך שרמת הגישה תהיה מוגדרת "על הרכיב". הערבוב בין הגישות - עשוי להיות מבלבל, ולכן אינו מומלץ.



באופן דומה, ניתן לבצע import:

  1. צורה זו מייבאת את כל הקובץ, והיא תריץ מחדש את כל הקוד בקובץ (כמו import בשפת C). צורה זו איננה מומלצת לשימוש, ואיננה חלק ממערכת המודולים!
  2. הצורה המומלצת היא לייבא את ה default export - ולתת לו שם מקומי. פשוט.
  3. צורה זו היא named import בו אנו מציינים את שמות האלמנטים בהם אנו רוצים לעשות שימוש.
    התחביר מזכיר תחביר של deconstructing assignment.
  4. אפשר להשתמש ב named import אך לתת שמות אחרים מקומיים לאלמנטים שלהם עשינו import.
  5. אפשר לייבא את אל האלמנטים שהוחצנו, מה שנקרא namespace import.
    1. אפשר לבצע deconstructing assignment בכדי להגיע לתוצאה דומה ל named import.
  6. אפשר לבצע import מעורב (אם היה גם export מעורב). foo  (מחוץ לסוגריים המסולסלים) הוא השם לאובייקט ברירת המחדל שהוחצן. הגישה הזו יוצרת מקום לבלבול בכמה רמות שונות - ולכן אני ממליץ להימנע ממנה.



Promises


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

מחמת הספק, ומחמת החשיבות של היכולת - אסכם אותה בקצרה.

Promise הוא הבטחה לקבלת תוצאה עתידית של הפעלת לוגיקה כלשהי, לרוב הפעלה אסינכרונית. שם נרדף, ולא פחות מוצלח הוא "Deferred" - תשובה המגיעה מאוחר יותר. ה Promise הוא ה handler להפעלה, המאפשר לאסוף את התשובה:
  • במקום אחר (להעביר את ה promise/handler לאורך כמה פונקציות - ורק אז לקרוא את התשובה)
  • ובזמן אחר (להספיק לעשות פעולה נוספת בזמן שהפעולה האסינכרונית רצה).

יתרון נוסף וחשוב של Promises הוא כתיבת קוד למסודר יותר - יחסית ל callbacks.

"אז מה ההבדל? במקום callbacks מקוננים, אני כותבת את השורות בזו אחר זו?"

  1. כן - זה שיפור מורגש. 
  2. callbacks עם error handling הוא קוד שנוטה להסתבך במיוחד. נראה בהמשך כמה יותר אלגנטי הוא הפתרון של Promises.


הנה דוגמה פשוטה:

  1. יצרתי Promise והעברתי להרצה פונקציה המבצעת קריאה אסינכרונית ברשת (request הוא מודול של NodeJs). כשתחזור התשובה - הפונקציה תמשיך לפעול ותקבע תשובה ב Promise, תשובה שיהיה אפשר לאסוף מרגע שנקבעה.
    1. אם הבקשה מצליחה - אני מאפשר החזרת ערך בעזרת ה Promise - בסימן הצלחה (להלן "resolve").
    2. אם החלטתי שהבקשה נכשלה - אני מאפשר החזרת הסבר בסימן כישלון (להלן "reject").
  2. במקום / שלב מאוחר יותר - אני שולף את הנתונים מה Promise
    1. then - אם הייתה הצלחה.
    2. ה Promise שלי, יכול היה להחזיר Promise בעצמו - וכאן הייתי יכול להמשיך ולשרשר את הטיפול בתשובה שלו. במקרה שלנו, לא הגדרנו תשובה בפעולת הסעיף הקודם, ולכן הערך הוא undefined.
    3. catch - יתבצע אם היה כישלון (במקרה שלנו - הייתה הצלחה). "ערוץ" ה catch משתשרשר כל ה promises שבדרך ("then"), כך שאם נכשל ה promise המקורי (למשל: נקלקל את ה url) - יופעל ה catch.
    4. finally - קוד שירוץ בכל מקרה. 

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

  1. Promise.all יוצר Promise חדש, שיהיה resolved (או rejected) כאשר כל ה promises ברשימה יחזירו תשובה. זהו כלי חשוב מאוד להרצה של מספר פעולות אסינכרוניות במקביל - ואז טיפול בתשובות.
    1. אפשר לשים לב שהתקן הוגדר לפני שהיה Rest Parameter לפונקציות...
  2. הפעולה ההופכית, race, שיכולה הייתה גם להיקרא any - מחזירה promise שיחזיר לנו תשובה כלשהי (שחזרה) מה promises שנשלחו כפרמטרים. פעולה פחות נפוצה - אך עדיין חשובה.
  3. ניתן להשתמש ב Promises גם לפעולות סינכרוניות. פשוט יוצרים promise כבר עם "התשובה בפנים" בעזרת הפונקציות reject או resolve.



סיכום


טוב, התקדמנו צעד חשוב, ואני מקווה שלאחר פוסט זה, יש לנו מושג לגבי עיקרי השפה ES6.

השפה איננה עוד שפה פשוטה, ולצערי עדיין לא כיסינו עדיין כמה נושאים מתקדמים אך חשובים, כמו: async-await (השלב המתקדם יותר של Promises), מנגנון ה Generators - ואולי עוד כלי או שניים.

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

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


שיהיה בצלחה!