ארכיטקטורה: האם לנסות שוב?!

לעתים אנו נתקלים בעת בניית המערכת בבעיות שקשה לפתור.

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

מה עושים ב 0.1% (עשירית אחוז) מהמקרים האחרים, כאשר דווקא מודול B מסיים ראשון?

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

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

סוג אחר של מתכנתים, יחפש פתרונות אג׳יליים ופשוטים למימוש. ״בוא נוסיף sleep של 500ms למודול ב׳ – וכך הוא תמיד יסיים אחרי״. הפתרון אמנם פשוט מאוד למימוש אך הוא נדחה על הסף על ידי הצוות: להאריך את התהליך כולו סתם בחצי שניה, עבור 99.9% מהמקרים  – זה לא נשמע סביר…
ננסה שוב: ״בואו נזהה מצב שמודול ב׳ מסיים ראשון, ואם זה קרה – נפעיל את כל התהליך בשנית״.

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

מה אתם אומרים?

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

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

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

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

התבונה המקובלת היא: \"בואו נעשה את זה פעם אחת ולתמיד\".

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

התבונה המקובלת היא: \"בואו לא נסבך את זה\".

אלתור. יעיל, או חפלפ?  מקור: redstateeclectic

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

במשך שנים בקריירה, הצעות מסוג זה נתפסו על ידי כחוסר מקצועיות, הבנה, או סבלנות נדרשת. כסמל של התנהגות שיש להוקיע. עבדתי אז ב SAP וקיבלתי גיבוי מלא לגישה הזו מעמיתי והמנהלים. פעמים רבות עבדנו עוד שבוע או שבועיים – בכדי לא להגיע לפתרונות של \"ניסוי שני\". לעתים, כאשר היה מדובר בשבועות רבים – היינו \"מתגמשים\" (ורושמים עוד שורה באקסל ה Technical Debt [א] שאז ניהלתי).

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

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

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

לא ספר אמיתי 🙂

אז מה אני חושב על פתרונות \"מהירים ואופטימיים\"?

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

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

\"לפתור את הבעיה אחת ולתמיד\" זו בעצם הגישה שאומרת להשקיע יותר בפתרון, ו\"בוא לא נתעכב\" זו בעצם הגישה שאומרת (you ain\'t gonna need it (YAGNI – להשקיע פחות.

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

צעד שני הוא לשאול את השאלות הבאות:

  • עד כמה חלק הקוד המדובר הוא קריטי להצלחת הביזנס? עד כמה הוא מורכב ואנו סובלים מתחזוקתו? – ככל שהוא יותר מורכב ורגיש, נכון להשקיע יותר משאבים ולצמצם סיכונים.
    • מצד שני: ככל שמדובר באיזור קריטי למערכת – סביר יותר להניח שנחזור ונתקן / נשפר את ה \"Retry\" אם הוא לא מספיק טוב.
  • עד כמה המערכת צעירה / בוגרת? ככל שהמערכת בוגרת – יש טעם להשקיע יותר בכדי לשמר את האיכות והיציבות שהיא כבר השיגה. במערכת ממש חדשה – תקלה אקראית עשויה להיבלע בזרם בעיות גדולות וחמורות יותר. בעיות גדולות יותר יכולות להיות באגים, אך לא פחות מכך הן יכולות להיות פיצ\'רים חשובים שחסרים ללקוחות, או אי-התאמה בסיסית לצרכים העסקיים.
  • עד כמה ההתנהגות שאנו יוצרים היא צפויה? (Principle of least astonishment) – ככל שהפתרון ה\"אופטימי\" שלנו הוא מפתיע ולא-צפוי (יחסית להתנהגות הסדירה והמקובלת של המערכת) – כך גדלה הסבירות שהוא יישבר עם הזמן, או יגרום לבעיות שיהיה קשה לצפות. 
  • עד כמה קל לדעת אם ה Retry עובד? אם יש לנו שטף של אירועים מבוקרים ומנוטרים – קל יותר להצדיק התנסות ב\"פתרון מהיר ואופטימי\". סיכוני אבטחה (אירועים נדירים יחסית, אך אולי עם השפעה חמורה), למשל – הם מקום פחות מומלץ לעשות בו ניסויים.
    • הוספת Alerts למצבים בלתי צפויים סביב הפתרון – עשוי להיות מעשה נבון.
  • אל תנהגו בטיפשות. אל תעשו Retry על פעולות שאינן idempotent – כלומר פעולות שהפעלה שלהן מספר פעמים תסתיים בתוצאה שונה מהפעלה בודדת (למשל: חיוב של כרטיס אשראי).
בקיצור: It depends.
שיהיה בהצלחה!

[א] מונח המתאר את ה\"חובות הטכנולוגיים במוצר\". כמו חובות פיננסיים, נבון לעתים לקחת אותם – אבל אם הם תופחים הריבית היא בלתי-נשלטת ועלולה להוביל לאסון.
במילה Debt, אגב, לא מבטאים את ה b. זה נשמע כמו \"Technical Det\".

איך (לא) לבצע בחירות טכנולוגיות עבור המערכת שלכם?

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

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

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

  • דוכן ראשון – עם 6 פריטים בלבד.
  • דוכן שני – עם מבחר של כ-30 פריטים.

כיצד יהיה שונה דפוס הקנייה בשני הדוכנים?

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

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

כי אנשים מתקשים לבחור. בחירה בין 30 פריטים היא קשה יותר מבחירה בין 6 פריטים – כך שהדוכן בן ששת הפריטים מצליח למכור יותר (חיזוק חיובי לחנויות מעצבים / high-end).

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

  • אם פעם היה Framework דומיננטי לכל שפת תכנות (STL ל++C, בג׳אווה JEE, ובדוטנט – מה שמייקרוסופט סיפקה) – עם הזמן מבחר ה frameworks הלך וגדל.
    ג׳אווהסקריפט היא הדוגמה הקיצונית לשימוש ב micro-frameworks. הבדיחה הנפוצה היא על מפגש מתכנתים שמתנהל כך: ״שלום, אני ליאור – וגם אני כתבתי Framework לג׳אווהסקריפט…״.
  • כיום, ה Technology Stack – "מתפוצץ מאפשרויות": יש לנו את Nano Server, Deis, Fastly, את Spark (על כל חלקיו) ואת Kubernetis. כל שבוע יוצאת טכנולוגיה / ספריה חדשה ומדליקה!
    האם אתם כבר מכירים את Axton, Sleepy Puppy, שפת Nim ושפת Julia? אל תחשבו אפילו לומר שלא בדקתם את OneOps של וולמארט…
  • בסיסי נתונים: אם פעם הבחירה הייתה ברורה (אורקל – באנטרפרייז, SQL-Server – ל Microsoft-shop, ו MySQL לסאראט-אפים) היום יש יותר מכמה בסיסי נתונים רלוונטיים לכל סביבת עבודה. למשל: MongoDB, Redis, ו Cassandra יכולים לשמש את כולם – בנוסף לבחירות המקוריות. בעולם ה open source כבר אין בסיס נתונים דומיננטי אחד: MySQL, אולי PostgreSQL ואולי MariaDB? מה עם Aurora?
  • המושג ״Polyglot Persistence״ או ״Polyglot Programming״ (המתאר סביבה רבת-טכנולוגיות) עלול לעזור ולהצדיק מצב של שימוש בריבוי טכנולוגיות במקביל [ד] – אבל הוא לא מפשט את הבחירה.
    Stack טכנולוגי רחב מידי – הוא עדיין בעיה תפעולית ממעלה ראשונה. איזו רמת עומק ממוצעת אתם רוצים בטכנולוגיות שאתם עובדים עימן? ככל שיהיו לכם יותר טכנולוגיות – רמת העומק הממוצעת תפחת.

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

כיצד אנו מתמודדים עם הקושי לבחור – כאשר המבחר גדול?

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

בעולם הצרכנות יש לנו יוריסטיקות לקנייה:

  • המלצה של מגזין / מקור ״בעל סמכות״ (לכאורה!) – תשפיע עלינו לטובה.
  • המלצה של אדם ממשי אחד שאנו מרגישים שהוא דומה לנו – לרוב תהיה חזקה יותר מהמלצה של "מקור סמכותי".
  • מה שיקר יותר – בטח טוב יותר [א].
    • טרנד של הדורות האחרונים: מה שחדש יותר – בטח טוב יותר [ב].
  • הצמדות למותגים ידועים.
    • למרות שמי שבחן פעם מוצרים לעומק – יודע שזו יוריסטיקה חלשה: מותגים ״חזקים״ לעתים קרובות מייצרים גם מוצרים חלשים / לא-תחרותיים / יקרים ללא הצדקה.
מה אנו עושים כ״מקצועני-תוכנה״? – בערך את אותו הדבר!
אנו נצמדים לשמות גדולים, למותגים, להמלצות של אתר / בלוג , והמלצות של חבר, לדברים חדשים ויקרים (במידת האפשר). הבחירות שלנו הופכות יותר "אופנתיות", ופחות "לוגיות".
השיטות הללו הן יוריסטיקות לא-מעולות, אבל גם לא בהכרח גרועות: להיות מ Mainstream אומר שיש יותר רפרנסים וחומר על הטכנולוגיה, יותר אנשים שמכירים (ואפשר גם לגייס), ואם יש בעיה ממש מציקה – אז יש ecosystem שיש לו מניע כלכלי לפתור את הבעיה הזו.

אז זו לא בחירה "מקצועית נטו"?

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

הנה כמה קריטריונים שעוזרים להבין אם מדובר בתהליך בחירה או תהליך הצדקה:
  • אם הדיון הוא קיצוני, והאלטרנטיבות מתוארות לחלופין כנפלאות ("Rocks") או איומות ("Sucks") – זהו כנראה דיון רגשי.
  • אם יש אלנטרטיבה יחידה: "Angular.js זו הבחירה הנכונה", אבל לא מוכנים לדון ברצינות ב Ember.js או React+Flux (שהן דיי מקבילות) – כנראה מדובר בדיון רגשי.
  • אם מוכרים לנו את הטכנולוגיה כמו שמוכרים ביטוח – אז זה כנראה תהליך הצדקה.
    • איך מוכרים ביטוח? מתמקדים בתסריט קיצוני ומבהיל ("ואם הבית שלכם יהרס ברעידת אדמה…") בכדי להצדיק מחיר מסוים ("לא היה עדיף לשלם 3000 ש"ח בשנה?"). כאילו שאם הבית יהרס ברעידת אדמה חברת הביטוח לא תציב בפנינו כל קושי אפשרי בכדי שנקבל כמה שפחות כסף… תוך התעלמות מסבירות (מעולם לא קרה…) תוך התעלמות מנזקים אחרים (שחלילה – עלולים להיות קשים יותר)….
    • איך מוכרים טכנולוגיה כמו ביטוח?
      • "אבל בריילס יש הגנה בפני cross request forgery – וב Java האתר יהיה פרוץ להתקפות (!!)"
      • או: "אם אתה צריך לטעון סט של 5GB נתונים זה ייקח פי 10 זמן (!!)" (בעוד אנו לא חיים עם dataset בגודל דומה לזה בכלל). "חבל לאבד את כל הלקוחות שמייד יעזבו את החברה, וישמיצו אותה בפייסבוק ללא הפסקה, יום – ולילה!"
מק עם סטיקרים – הצהרה אופנתית!

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

ניתן "להעריץ" ספריות ומותגים ("גוגל", "HashiCorp"), גם מבלי להבין או להתנסות במוצר.
כיום אנו נוטים להעריץ חברות אינטרנט בעלות Scale גדול ושווי בורסאי גבוה ("חדי-קרן"). העתקת הארכיטקטורה של חברה שמטפלת בנתונים בנפח 20PB ביום – לא בהכרח ישרת את הצרכים העסקיים של החברה שלנו. הדרך להגיע ל Scale גבוה היא בפשרות כואבות. האם אנו מעתיקים פשרות שהארגון שלנו לא צריך לעשות?פידבקים חיוביים מחברים / עמיתים ("וואו! אתם עובדים עם unikernel? דוקר זה באמת כ"כ 2015!!!!") – תבצר את עמדתנו. האם לא נרצה עוד מהתחושה הטובה הזו?!

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

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

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

פירמידת הצרכים של המתכנת?!

בענייני אופנה ניתן לזהות עוד 2 תופעות:

ישנן אופנות מתפרצות, שקצב האימוץ שלהן הוא לא-רגיל. פעם זה היה Rails, מאוחר יותר אולי MongoDB והיום זה Docker וגם Node.js ו Angular.js. הן לרוב פורצות בהובלת מובילים טכנולוגיים (שהם גם כריזמטיים), אבל לא דיי בזה להסביר את התרחבות התופעה – אולי ניתן להסביר אותה בעזרת שילוב של כמה מרכיבים [ה].
בקצב אימוץ של אופנה מתפרצת, סביר שרוב האנשים שהולכים לאחר האופנה לא ידעו להסביר מדוע באמת הכלי המדובר טוב יותר עבור המקרה שלהם. הם גם כנראה לא יהיו בעלי הבנה עמוקה בטכנולוגיה. בעיניים נוצצות הם יאמרו ("אני? אני עוד לא כזה מבין… יש לי עוד הרבה מה ללמוד"). הם יידעו בעיקר לדקלם כמה סיסמאות נפוצות "ב node.js אפשר לפתוח מיליון (!!) connections מלפטופ פשוט. אתה יכול לעשות את זה בג'אווה?!?".

כמו בכל אופנה יש אנטי-אופנה. שימוש ב Erlang או שימוש ב MySQL כפי שמתארים Wix או Uber כ"בסיס הנתונים ה NoSQL-י הטוב ביותר". אנטי-אופנה זה לבחור את הכלים הכי לא-נוצצים, ולהוכיח לכולם שבעזרת קצת כשרון – ניתן לעשות איתם הכל, ואפילו יותר מ buzz.js@node.

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

מי בוחר את הטכנולוגיה?

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

"חצץ"?!
פעם עבדתי עם צוות שנדרש לשכתב את כל קוד הג'אווה שלו ל Stored Procedures בתוך בסיס הנתונים. זו הייתה החלטה של ארכיטקט מאוד מאוד בכיר – ולמרות שהייתה התנגדות רבה, ההחלטה עמדה בעינה.
הצוות היה מאוד מתוסכל (ראיתם פעם אלפי שורות קוד של stored procedures?!) – ולאחר כמה חודשים של עבודה מייסרת, הארכיטקט המאוד מאוד בכיר נאלץ לוותר על ה"אוטופיה ארכיטקטונית" שלו (שלא היה בה שום דבר להתגאות …).

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

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

מקור: Technology Radar של Thoughtworks

ומה אומרת הארכיטקטורה?

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

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

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

הטכנולוגיה? אולי אי-אפשר בלעדיה – אבל היא גם מפריעה!

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

בסופו של דבר חשוב בצד הארכיטקטורה:

  • לצמצם את מספר הטכנולוגיות החופפות – כי ריבוי טכנולוגיות מקשה על תחזוקת המערכת, והארגון שלה.
  • למצוא טכנולוגיות "מספיק טובות" – שלא יגבילו אותנו יותר מדי.
    • ריילס – מציבה Overhead גדול של ביצועים. היא לא מספיק טובה למערכות עם High Throughput.
    • כתיבה ב C לא תציב מגבלות High Throughput אבל הקוד ייכתב באטיות ויהיה קשה לארגון ותחזוקה. היא לא מספיק טובה לצרכים עסקיים של מערכת שתפותח מהר.
אבל לבחור IDE? לבחור Application Server? ארכיטקטורה שכוללת או לא כוללת Docker? להתעקש רק על שפת תכנות ספציפית? – זו כנראה לא ממש "ארכיטקטורה", אלא ארכיטקט שמנצל את כוחו – כי גם הוא רוצה לבחור טכנולוגיות! מה לעשות – גם הוא בן-אדם שחובב טכנולוגיה.
יש כמובן גם שיקולים מעשיים / פרויקטליים:
  • איזו טכנולוגיות אנשים כבר מכירים – ויש להן תמיכה רחבה?
  • אלו טכנולוגיות יעזרו למשוך לחברה את ה talent הנדרש – לכתיבת מערכת מורכבת ומאתגרת?
  • עלויות של טכנולוגיות שאינן חופשיות.
  • לא פחות חשוב: כיצד בונים מעורבות של הצוות הקיים? לרוב ע"י מתן משקל לדעתם בבחירת הטכנולוגיה. אפשר וחשוב להציב תנאי סף: מחיר, מגוון, או כל מגבלה שהיא עניינית וניתן לשכנע בה את "הרוב השקול".

זו איננה ארכיטקטורה!

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

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

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

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

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

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

סיכום

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

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

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

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

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

—-

לינקים רלוונטיים

על העוצמה הטמונה ב"טכנולוגיות משעממות" [דעה] (פוסט שלי)

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

—-

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

[ב] – בכנס GOTO; Berlin 2014 הייתי בהרצאה על big data של הוצאת ספרים גדולה. נתון מעניין שהם הביאו שספרי מחשבים נמכרים בעיקר בשנתיים-שלוש לאחר שיצאו. באופן דומה – גם ספרי ניהול אימצו דפוס דומה, אם כי שיטות הניהול לא משתנות משמעותית במחזורים הקצרים מ 10-15 שנים.

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

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

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

עקרון זה נקרא בפסיכולוגיה עיקרון העקביות (The consistency principle). הצורך של אנשים בעקביות – וההתנהגות שלהם שנוטה באופן טבעי לחזק את העקביות בעולם האישי שלהם.

מקור: Influence של ציאדיני, פרק 3.

[ד] הבהרה: אנשים רציניים שידברו על Polyglot Programming יציגו גישה לפיה עובדים עם מספר כלים – השונים מהותית זה מזה.

"node.js ו Java" או "פייטון, רובי, ו PHP" – הן לא דוגמאות ל Polyglot Programming בריא – אלו שפות דומות מדי זו לזו, וריבוי שלהן בעיקר יציב בעיות תחזוקה. דווקא שילוב כמו "PHP וג'אווה" או "רובי ו ++C" – הם שילובים רלוונטיים.

[ה] הספר "The Tipping Point" (המעניין!) של מלקולם גלוודוול מנסה לנתח את הדינמיקה של תופעות שכאלו.

מבוא ראשוני לשפת Go, חלק 3 – תכנות בעזרת אובייקטים

פוסט זה הוא פוסט המשך לפוסטים:

בפוסט זה נדון באספקט ה"אובייקטים" בשפה.

האם Go היא בכלל שפת Object-Oriented?

בכדי לענות על השאלה, בואו נבחן את שלושת התכונות העיקריות של שפות Object Oriented:

  • הכמסה (encapsulation): מוגבלת. יש הכמסה ברמת החבילה, אך לא ברזולוציה קטנה יותר.
  • ריבוי צורות (polymorphism): יש. בעזרת ממשקים – שנראה בפוסט זה.
  • הורשה (inheritance): אין. משום סוג. הדרך ללכת בה היא composition.
50% התאמה לעקרונות הבסיס => Go איננה שפת Object Oriented.
בכל זאת, מי שרגיל לתכנות OO, וארגון הקוד ע"פ אובייקטים הכוללים נתונים והתנהגות יחדיו – יגלה שעקרונות ארגון הקוד גם לא מאוד שונים.
אם אומרים על שפת JavaScript שהיא שפה "מבוססת אובייקטים" (היא לא מונחית-אובייקטים) כי יש בה אובייקטים, אך לא מחלקות – שפת Go היא רחוקה יותר מזה. ב Go ישנם "אובייקטים פתוחים", שרואים להם את השלד והאיברים הפנימיים: Structs ומתודות מקושרות.
הייתי מתאר את Go כשפה הנעזרת באובייקטים Object Aided Programming. היא מאמצת רק חלק מהרעיונות והחשיבה של תכנות מונחה-עצמים. האובייקטים הם כבר לא במרכז, אלא הפונקציות וה Structs – כשני אלמנטים נפרדים.
מקור: spf13

אובייקטים… מבנים ב Go

בואו נסתכל על קוד:

  1. אנו מגדירים טיפוס חדש בשם Rectangle שהוא בעצם struct עם 2 שדות: width ו height.
    1. שימו לב שאנו מתעדים את ה Struct ע"פ קונבנציית התיעוד של גו: …a Rectangle is
  2. בשלב זה אנו מגדירים מתודה, שמקבלת Rectangle ומחזירה int שהוא שטח המרובע.
    1. בהגדרה, מתודה היא פונקציה עם receiver (לפעמים נקרא גם handler).
    2. מתודה היא כזו שמשנה State (את זה של ה Struct = מבנה), פונקציה היא stateless.
    3. מתודה ניתן להפעיל רק דרך רפרנס למבנה (כמעט אמרתי… אובייקט) כלומר (…)myStuct.myMethod, בניגוד לפונקציה שמפעילים ללא תלות, כלומר (…)myFunction.
  3. הסוגריים המשונים הללו לפני שם המתודה הם ה receiver.
    1. גו מחברת את הפרמטר של ה receiver (תמיד יש אחד) לשאר הפרמטרים של הפונקציה – כך שהוא תמיד יהיה ראשון. זה קורה מאחורי הקלעים.
      1. כלומר: החתימה של המתודה area היא בעצם, מתחת למכסה המנוע:
        area(r * Rectangle) int.
      2. הרבה יותר נוח לקרוא קונטקסטואלית את הביטוי ()rect.area, מאשר את (area(rect – ולכן ה syntactic sugar הזה.
    2. הקונבנציה המקובלת היא לפרמטר של ה receiver באות הראשונה של הטיפוס (אות קטנה), או אולי קיצור של כמה אותיות קטנות. למשל: s או sb עבור stringBuffer.
      אם אתם מנסים לקרוא ל reference לטיפוס בשם גנרי – זה סימן שלא השתחררתם עדיין משפת התכנות הקודמת שלכם:

      1. this (ג'אווה או #C?)
      2. that (ג'אווהסקריפט, אולי?)
      3. self (רובי?)
      4. __self__ (פייטון! – תפסנו אתכם)
    3. אם בקוד המתודה אתם לא משתמשים בפרמטר של ה receiver (בדוגמה שלנו: r)  – זהו smell שאתם זקוקים כאן ל function ולא למתודה.
    4. אם בקוד המתודה אתם לא משתמשים ב fields פנימיים של ה struct (אלא רק מתודות אחרות) – ייתכן שעדיף להשתמש בפונקציה רגילה שמקבלת את ה struct כפרמטר.
  4. הנה שימוש בקוד. אנו יוצרים שני מבנים: r ו r2.
    שימו לב ש r2 הוא מצביע ל Rectangle – כי השתמשנו במילה השמורה new.
  5. אנו מדפיסים את תוכן ה Structs. הסימן & בהערה מציין שמדובר במצביע – אך שימו לב שאופן השימוש הוא זהה. בעזרת ה dot notation לא היינו צריכים לכתוב ()r2.area* מתוך ההבנה שזה פויינטר, וגם לא בקיצור כמו ()r2->area של ++C. הקומפיילר מזהה שהשתמשנו ב dot notation על מצביע, ומוסיף את ה * עבורנו, מאחורי הקלעים.
  6. עכשיו בואו נענה על השאלה: מדוע ה receiver (סעיף #3) קיבל את Rectangle כפונייטר (קרי Rectangle*)? האם חובה להשתמש בפויינטר?
    1. לצורך התרגיל נגדיר את המתודה Width (אות גדולה = שלפן) שמקבלת את Rectangle לא כפויינטר.
  7. כאשר אנו משתמשים במתודה – אנו רואים שהערך לא השתנה: הצבנו 4, אך הערך נשאר 0.
    מדוע זה קרה?! בגלל שהעברנו למתודה העתק של r2 עליו התבצע השינוי. r2 עצמו לא השתנה.
  8. הנה הבדיקה שאכן כך הדבר: כאשר אנו משנים את r2.width ישירות – הערך משתקף בהדפסה.
מפה עולה השאלה: מדוע בכלל לאפשר ליצור value receiver? מדוע הקומפיילר לא מגן עלינו ומחייב ש receiver יהיה רק pointer receiver? האם מתודה לא נועדה לשנות את ה state של המבנה המדובר?
התשובה היא לא-בהכרח.
  • value receiver הוא שימושי כאשר מעבירים פונקציה, map, או channel (שהם כבר בעצמם מצביעים). ההעתקה של הערך יכולה להיות יותר יעילה מגישה לפויינטר שמצביע לאזור שאינו ב cache של המעבד.
    slice הוא גם מצביע, אלא אם המתודה עשויה להקצות ל slice מערך חדש – ואז יש להשתמש בפויינטר.
  • value receiver הוא שימושי כאשר מדובר במתודה שהיא immutable, כלומר – העברה כ value (העתקה) מבטיחה שלא נוכל לשנות את ה state של ה struct.
בקיצור: אם אתם לא בטוחים – השתמשו ב pointer receiver. אם הזמן אולי תגלו את הצרכים ב value receiver גם כן.
אני רוצה שנייה לחזור למילה השמורה type ולהראות שאנו יכולים להגדיר aliases (כמו ב C) או מבנים שלא מבוססים בהכרח על struct. הנה דוגמה למבנה שמבוסס על slice:

הרכבת טיפוסים (Composition)

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

  • נניח שאנו רוצים לדמות מחלקה B שיורשת ממחלקה A.
  • במחלקה B נוסיף שדה a שיכיל מצביע לאובייקט מסוג A.
  • למחלקה B נוסיף את כל המתודות של מחלקה A, ובכל מתודה x נקרא בעצם ל a.x עם הפרמטרים שהעבירו לנו. זהו קוד boilerplate שמרגיש דיי מיותר – במיוחד ככל שלמחלקה A יש יותר מתודות.

בשפת גו יצרו תחביר של Composition שחוסך את ה boilerplate code. במבט ראשון – הוא נראה כמו הורשה, אך זו לא הורשה. מבנה B ש composes את מבנה A – לא יכול לגשת או להשפיע על מבנה A שלא דרך המתודות הציבוריות שלו. הבאגים שיכולים להיווצר בגלל תלות שכזו בהורשה – לא יתרחשו בצורת העבודה הזו. מצד שני צורת ה composition לא יכולה להשיג חסכון בקוד-כפול בכמה צורות שהורשה יכולה.

איך עושים זאת?

מבנה בשפת גו יכול להכיל fields שהם embedded (נקראים גם אנונימיים). יכולת זו נקראת Embedded Types.

  1. אנו מגדירים את המבנה Person שמכיל שם, ומתודה sayHello.
  2. אנו מגדירים את המבנה Student שמכיל Person ושם מחלקה, ומתודה sayHello.
  3. שימו לב שבהגדרת ה Person לא ציינו שם של המשתנה (רק טיפוס). זהו embedded type שגורם להרחבה (extend).
    1. בפעולה זו אנו גורמים לכל ה fields והמתודות של Person להופיע ברשימת ה fields והמתודות של Student – דומה מאוד ל extend של מודול בשפת רובי. ה fields והמתודות עדיין נשמרים במבנה Person ומתנהגים כפי שהיו מתנהגים לו Person היה field מהמניין. נראה את ההתנהגות המדויקת מייד.
    2. אם למבנה החיצוני (Student) יש מתודות / fields עם שם זהה למבנה הפנימי (Person) – הם תמיד יקבלו קדימות, ובפועל "יסתירו" את המתודות / fields של המבנה הפנימי.
    3. ניתן עדיין לגשת למתודות של המבנה הפנימי בצורה מפורשת, קרי: student.Person.Name.
    4. בשונה מהורשה, אם מתודה של Person תקרא ל field או מתודה שקיימים גם ב Student – היא תמיד תגיע ל field / מתודה ב Person, ואף-פעם לא ל Student. ניתן לומר ש invocation "הועבר" למבנה הפנימי (Person) ופועל עליו.
      חלק מהכוח של הורשה, בוא בכך שמחלקה היורשת מחליפה התנהגויות שמשפיעות גם על המתודות של מחלקת האב. התנהגות זו עלולה לגרום לבאגים מתסכלים, אם כי היא גם מאפשרת דפוסי-עבודה שיכולים לחסוך לא-מעט קוד.
  4. הנה אנו יוצרים מופע של המבנה Student, ובתוכו את המבנה של Person. שם ה field שמחזיק את המבנה הפנימי הוא כשם הטיפוס של אותו מבנה. הבחור שלנו, ג'ון, נרשם באקדמיה בצורה לא מוסברת בשם "פופה".
  5. תזכורת: ברשימות של איברים בגו יש להוסיף פסיק גם לאחר האיבר האחרון. זה מקבל על הוספת / הסרת איברים ללא שבירת הקומפילציה.
  6. הנה אנו מדפיסים את student, ורואים כיצד הוא מיוצג בזיכרון – כמבנה מקונן.
  7. כאשר אנו קוראים ל sayHello אנו מגיעים למתודה של המבנה החיצוני בהכרח (יש לה קדימות). הערך של שדה Name הוא זה של אותו המבנה.
  8. כאשר אנו קוראים ל sayHello של Person בצורה מפורשת – אנו מגיעים למתודה של המבנה Person. הערך של שדה Name הוא זה של אותו המבנה, קרי Person.

ממשקים (Interfaces)

ממשקים הם הכלי בשפת Go לריבוי-צורות. ממשקים נפוצים בשפה הם io.Reader ו io.Writer שמסתירים מהקוד שלנו את פרטי המימוש הקונקרטי שאנו עובדים איתו (למשל: אמצעי קלט/פלט – דיסק, רשת, מבנה בזיכרון). מי שמגיע משפת ג'אווה, בוודאי מורגל לממשקים דומים. ממשק מפורסם אחר הוא error, המאפשר לנו להתייחס לכל השגיאות, אם אנו רוצים – באותה הצורה.

הבדל חשוב של ממשקים בגו מ"הקלאסיקה של ה OO", היא הרעיון שהם מבטאים:
ב OO מלמדים אותנו לחפש קשר של is-a או לפחות a-substitute-of בין הממשק ומי שמממש אותו, ושפות שונות מספקים כלים שונים לאכוף שאובייקט באמת רשאי למממש ממשק מסוים.

הגישה של גו היא גישה של Duck Typing שאומרת כך: "אם זה נראה כמו ברווז, ומשמיע קול של ברווז – אז זה ברווז!"

Duck Typing. לצורך העניין – זה יכול להיחשב כברווז.

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

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

גישת ה Duck Typing אם כן, מציגה tradeoff עם יתרונות וחסרונות – אולם זו גישה מקובלת ושנחשבת בהחלט סבירה. בשפת פייטון גישה זו הוכיחה את עצמה כישימה, גם בפרספקטיבה של זמן.

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

  1. אנו משתמשים במילה השמורה type בכדי להגדיר טיפוס מסוג ממשק בשם statusReporter, המכיל 2 מתודות.
  2. אנו עכשיו מגדירים פונקציה שמקבלת statusReporter כפרמטר, ומבצעת עליה פעולה כלשהי – בהתבסס על מתודות של הטיפוס (Status ו LastErrors).
  3. הנה הגדרנו איזה מבנה (=~אובייקט) שיש לו, במקרה או לא במקרה – מתודות עם חתימה זהה.
  4. אנחנו יכולים לשלוח כל טיפוס שעונה לתנאי הממשק – כפרמטר לפונקציה printStatus שהגדרנו.

מבט מתקדם על Interfaces בשפת Go

עד כאן הכל נראה פשוט. אם אתם מגיעים מג'אווה / #C, אתם בוודאי אומרים לעצמם: "זהו: את העניין של ממשקים ב Go – כבר הבנתי!".

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

  1. אנו מגדירים interface בשם HelloSayer.
  2. אנו יוצרים מבנה בשם מר זאב, העונה לממשק (עם value receiver).
  3. אנו יוצרים מבנה בשם גברת נץ, העונה לממשק (עם pointer receiver).
  4. אנו מאחסנים 2 זאבים (מצביע, ומופע קונקרטי) ו2 נצים (גם מצביע ומופע קונקרטי – שתי האפשרויות הרלוונטיות) – בתוך slice מטיפוס HelloSayer. עצם ההשמה גורמת ל upcasting של ה references ל ממשק.
מה זה משנה אם יש לנו value receiver או pointer receiver?
האם בכלל לא צריך להתאים בין הערך ששולחים ל receiver?!
התשובה היא שמקרים A-C יעבדו בצורה תקינה, אך מקרה D יגרום לשגיאת קומפילציה "SayHello has pointer receiver" – שגיאה שבוודאי תתקלו בה בעבודה שלכם בגו, שגיאה שהיא קשה להבנה ללא הרקע המתאים.
מדוע דווקא מקרה D הוא זה שנכשל?
האם נחליף את הביטוי D ב "{}Hawke&", כלומר נשלח מצביע למבנה – הדברים ייסתדרו? (כן)
מדוע זה עובד כך?
יש יותר מדרך אחת להסביר זאת, אני אבחר בהסבר של הברירות שהיו למתכנני שפת Go – נראה לי שזה האופן הפשוט ביותר להסביר זאת.
בגדול אנו רואים שניתן לשלוח למתודה עם value receiver גם מופע (A) וגם פויינטר (C), אך למתודה עם pointer receiver ניתן לשלוח רק פויינטר (B).
שפת גו מנסה:
  • לחסוך למתכנת "עבודה שחורה" – כל עוד הדבר לא פוגע בביצועי ריצה או ביצועי ההידור בצורה מיוחדת.
  • להיות עקבית וצפויה, ולחסוך מהמתכנת "באגים חמקמקים".
כאשר שולחים מצביע ל value receiver הקופיילר יכול לתקן זאת בקלות – ולשכתב את הקוד כך שישלח את הערך עליו מצביע הפויינטר. מי שכתב את המתודה מצפה לקבל עותק – וזה מה שהוא קיבל.

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

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

{}interface

הטיפוס {}interface הוא הממשק שאינו מכיל שום מתודות. ע"פ Duck Typing – מי מספק ("מממש") אותו? כולם!

זה אומר שזהו טיפוס-על שמייצג את כל הטיפוסים בשפה. ניתן להשוות אותו לביטוי *void (הידוע כ generic data pointer) בשפת C, אם כי {}interface הוא בטוח יותר לשימוש.

אילו טיפוסים ניתן להעביר בעזרת {}interface? האם אפשר להעביר int?
בואו נראה את דוגמת הקוד הבאה:

אפשר!

  • האם {}interface מאפשר dynamic typing בשפת Go?
    • לא. יש לו טיפוס סטאטי ומוגדר היטב. אפרט מייד.
  • מהו, אם כן, הטיפוס של i בדוגמה לעיל?
    • הטיפוס הוא {}interface. גו מבצעת type conversion על 1 ל {}interface, ואז חזרה בהדפסה.
  • האם אפשר להחיל את הכלל הזה גם על מערך / רשימה? כלומר: להעביר int[] כ {}interface[]?
    • לא. ניתן להעביר int[] כ {}interface (כי הוא טיפוס בשפה – slice) – אך הוא שונה מהותית "מערך של טיפוסים". אם רוצים לבצע את ההעברה יש לעשות עבודת העתקה ידנית.

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

מצביע ראשון: interface-table (מסוג itab) – מבנה המכיל metadata של הטיפוס (שם הטיפוס, הגודל שלו בזיכרון) ואת רשימת המתודות המשויכות לטיפוס הזה.
מצביע שני: data – הכתובת בזיכרון מה מאוחסנים הנתונים. "200" עבור int או רצף התווים של struct מסוים.

הנה pitfall בשפה שניתן להיתקל בו:
  1. נגדיר קבוע שמגדיר האם אנו אוספים נתונים ל BI או לא.
  2. נגדיר את buf מטיפוס Buffer. הטיפוס Buffer מספק את הממשק io.Writer.
  3. אם אנו במצב של איסוף נתונים – ניצור את ה buf, אחרת – אין טעם.
  4. פונקציית Perform מבצעת איזו לוגיקה עסקית, וכותבת נתונים שנאספו ל io.Writer.
    יש הגנה במידה וה io.Writer שנשלח לה הוא nil.
מה תוצאת הריצה?
  • אם collectData == true – התוכנה תתנהג כצפוי.
  • אם collectData == false, אנו נקבל שגיאת panic שאומר כך: invalid memory address or nil pointer dereference. כלומר: ניסינו להשתמש במצביע שהוא nil.
איך זה קרה?
מה יכולנו לעשות טוב יותר??

התהליך שגרם לשגיאה הוא כזה:

  • במידה ו collectData הוא false, אין השמה ל buf – והוא נשאר nil.
  • כפי שאמרנו ממשק הוא לא מצביע. כאשר אנו קוראים ל perform סביבת הריצה מבצעת עבורנו type conversion מ Buffer ל io.Writer.
    out, שהוא מטיפוס ממשק io.Writer, מכיל עכשיו שני מצביעים: iTable שמצביע על Buffer (טיפוס + רשימת מתודות), ו data שהוא nil.
  • בשלב #4 בקוד לעיל אנו בודקים אם out הוא nil, אבל הוא לא. הוא מבנה תקין מסוג ממשק שמכיל מצביע אחד לטיפוס, ומצביע שני שהוא nil.
באסה… מה עושים? איך לא נופלים בפח הזה?
ראשית – אתם צודקים. זו התנהגות "מכשילה" של השפה. לזכותה של Go אציין שיש לה הרבה פחות התנהגויות מכשילות מרוב השפות שאני מכיר. בעצם: ככל הנראה זו השפה הכי פחות מכשילה שאני מכיר.
אז מה עושים?
  • לבצע בשלב #4 (וכל מקום אחר בקוד שמקבל רפרנס) בדיקה עמוקה יותר של nil. ג'אווהסקריפט מישהו? שיש בה גם null וגם undefined? – ממש לא! זו אופציה גרועה למדי.
  • ניתן ליצור את Buffer בכל מקרה, ולבצע עבודה שלא נדרשת בכדי להימנע מטעויות. זו גם אופציה גרועה.
  • הדרך הנכונה לתקן זאת היא להגדיר מראש (בשלב #2) את buf כממשק (קרי io.Writer ולא כ Buffer).
    buf יישאר nil עד שיושם לו ערך (שלב #3), ואז שני המצביעים של הממשק יתמלאו במקביל בערכים.

שימושים של {}interface

אז מה בעצם השימושים הפרקטיים של {}interface? כנראה שלא לעשות המרה מטיפוס ל {}interface, ואז מ{}interface לטיפוס בחזרה…

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

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

  1. למשל: לא ניתן לשלוח nil ולכן הפכתי אותו למה שבסיס הנתונים מכיר – "NULL".
  2. הביטוי (x.(int בוודאי נראה מעט מוזר. זהו ביטוי מיוחד בשפה הנקרא type assertion שבודק בו-זמנית שני דברים: ש x הוא לא nil, וגם ש x מתאים לטיפוס (או הממשק) שצוין – במקרה שלנו: int.
    הביטוי מחזיר שני ערכים כתשובה:

    1. הערך השני ("ok") הוא bool שמציין אם ה assertion הצליח – כלומר: x הוא int שאינו nil.
    2. הערך הראשון הוא הערך של x על פי הטיפוס שהגדרנו (קרי int). הוא יקבל ערך אפס – אם ה assertion אינו נכון.
  3. Sprintf או כמו Printf, אך במקום לשלוח את הקלט ל default output הוא מחזיר אותו כמחרוזת.
  4. עתה אנו עושים אותו תרגיל על bool, אך במקרה זה אנו רוצים לשמור את הערך.
    מדוע אנו משתמשים ב val? מדוע לא לשאול if x? – הקומפיילר לא יאפשר לנו ויכשיל אותנו בבדיקת טיפוסים. הוא יאמר שלא ניתן להגדיר תנאי if על {}interface. המשתנה val מוכר לקומפיילר כ bool.
  5. ב Go אין ternary if (כלומר: : ?). חבל.
  6. panic הוא סוג של Exception פנימי של סביבת הריצה של גו. שההנחיה היא לא להשתמש בו – אולי רק במקרים קיצוניים. האם יש במקרה זה צידוק להשתמש ב panic?
    מצד אחד: כאשר משתמשים ב type assertion מבלי לשמור את ערך ההחזרה השני ("ok") – גו תזרוק panic.
    מצד שני: זה לא המקרה אצלנו, ויש לנו בחירה מה לעשות.
    זהו דיון ראוי שאינני רוצה לנהל כרגע. בעיקר רציתי לציין את התנהגות ה panic של ה type assertion.
משהו היה מוזר לכם בדוגמה לעיל? כמובן – הקונבנציה בגו היא להימנע מ if else if!
בשפה יש אופרטור דומה מאוד ל type assertion הנקרא type switch.

הנה דוגמת הקוד כאשר אנו משתמשים ב type switch:

  1. הביטוי הוא דיי דומה, כאשר בתוך הסוגריים אנו מציינים את המילה השמורה type. ביטוי זה יכול לעבוד רק כחלק מ ל switch statement.
  2. זה idiom מעט מבלבל: יכולנו לכתוב (val := x.(type ואז להשתמש בהמשך הפונקציה ב val – מה שהיה מפחית כנראה את הבלבול.
    בשפת Go ביטוי switch מגדיר scope משלו, scope שמתחיל שמתחיל מייד לאחר המילה switch. לכן ניתן להגדיר (x := x.(type, כאשר ה x הימני הוא זה שהוגדר ב scope הפונקציה, מטיפוס {}interface, וה x השמאלי הוא ב scope של ה switch בלבד – ויוכר כטיפוס שהוא בפועל. זה ה idiom.
  3. המשך הפונקציה כנראה ברורה – וגם פשוטה יותר מהגרסה הקודמת.

סיכום קצר

ממשקים הוא אחד מהיסודות החשובים בשפת Go. אם אתם מגיעים משפת C – זהו אלמנט חדש. אם אתם מגיעים מ ++C, ג'אווה או פייטון – הוא כנראה מוכר יותר, אך עדיין מעט שונה.
מבנים והרכבת טיפוסים – גם הם חשובים, ומשמשים ככלי מרכזי לפתרון בעיות ב Go.

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

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

—-

לינקים רלוונטיים

Drill Down על ממשקים בגו:
פוסט של Jordan Relly (כיסינו את הרוב בפוסט) ופוסט של Russ Cox (ממש כיצד מומשו בשפה).