יום שישי, 8 בדצמבר 2017

להפיק קצת יותר מ ELK ו Kibana


ELK Stack (ראשי תיבות של Elastic Search, Logstach, and Kibana) הוא אוסף של שלושה מוצרי קוד-פתוח:
  • ElasticSearch - בסיס נתונים NoSQL המבוסס על מנוע החיפוש Lucene (נקרא: "לוסין")
  • LogStash - כלי לאיסוף לוגים ("log pipeline"),פענוח (parsing), סינון, טיוב, ושליחה שלהם הלאה (ל ElasticSearch, למשל).
  • Kibana - שכבת UI/Visualization ל ElasticSearch.

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


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

בתחום ה "ELK המנוהל" יש מספר אלטרנטיבות כמו Logit, LogSense, או Scalyr - אך בישראל לפחות, ללא ספק המוצר המקובל ביותר הוא Logz.io - מוצר ישראלי, שגם סיים סבב גיוס לאחרונה.


שווה לציין של-ElasticSearch ישנם גם שימושים מעבר לאיסוף לוגים - אך זה איננו נושא הפוסט.

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

להעלות מ "רמה 3" - ל "רמה 6": משימוש מוגבל בכלי - ליכולת להפיק קצת תובנות משמעותיות.



"זה לא קשה בכלל, זה דווקא קל!"



בסיס


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

(1 - הוספת פילטר, פוזיטיבי או נגטיבי, הכולל את השדה, 2 - הוספת השדה לתצורה, 3 - הוספת פילטר המחייב את קיום השדה)


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

  1. אני מניח שאתם יודעים לחפש ע"פ ערך של שדה מסוים
  2. אני מניח שאתם יודעים להוסיף תנאים לוגים, וגם זוכרים שאין case sensitivity בערכים.
  3. אני מניח שאתם זוכרים ששמות השדות הם דווקא case sensitive (אחרת לא היה ניתן לזהות אותם חד-חד ערכית).
  4. ... שאתם זוכרים שאפשר להשתמש ב wildcard (למשל הביטוי הנ"ל יתאים ל Khrom וגם ל Chromium)
  5. ... אולי אתם יודעים שאפשר להשתמש ב wildcards גם בשמות של שדות (אם כי צריך escaping)
  6. .. ואני מניח שאתם זוכרים שכל הסימנים + - = && || ! ( ) { } [ ] ^ " ~ * ? : \ / - דורשים escaping. ובעצם הביטוי שאני מחפש הוא: 2=(1+1)
    1. שווה גם לציין שאין דרך לבצע escaping ל < ו >. באסה.
  7. אולי אתם מכירים גם Proximity Search, ששימושי כאשר יש חשש לשגיאת כתיב או הבדלים בין אנגלית אמריקאית לבריטית/אוסטרלית. המספר 5 מציין את "המרחק", וכמה שיהיה גבוה יותר - יותר ערכים דומים יכללו בתוצאת השאילתא.
  8. ובטח אני מניח שאתם זוכרים שאפשר לסנן ע"פ טווחים, כי בערכים מספריים (או תאריכים) - זה מאוד שימושי!


אני מניח שאתם יודעים להשתמש ב "surrounding documents" על מנת לפלטר את ההודעות שקרו לפני/אחרי ההודעה הנבחרת:

או יותר טוב - שהוספתם trackingId אפליקטיבי - שפשוט יהיה יותר מדויק.


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



אני גם מתאר לעצמי שהלכתם ל Management / Index Patterns, מצאתם את שדה הזמן (timestamp?) ושיניתם את הפורמט למבנה קצר יותר.

אין סיבה שיופיע לכם התאריך כ "December 2nd 2017, 21:41:51.155" בכל שורה - מבנה התופס שטח מסך יקר, בזמן שאתם יכולים לעבוד עם מבנה כמו "21:41:51.155 ,12/02/17" - שגם קל יותר לקריאה.


בעצם, אני רואה שיש לכם קצת בסיס!

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




Visualizations & Aggregations



בדרך ליצירת ויזואליזציה יש שלושה שלבים עיקריים:
  • Sampling - בחירת הערכים עליהם נרצה לבצע את הוויזואליזציה. יש לנו הרבה רשומות מאוחסנות ב ElasticSearch - וחשוב שנסנן את אלו הרלוונטיות לוויזואליזציה.
  • Clustering - אני רוצה לקבץ את הרשומות הרלוונטיות ל"אשכולים" החשובים לתובנה העסקית. למשל: ע"פ microservice, סוג משתמש, דפדפן שבשימוש, וכו'.
  • Reduction - גם לאחר כל הסינונים הללו, ייתכנו בכל אשכול אלפים (או מאות אלפים?) של איברים - שזה יותר מדי לצרוך. נרצה להשתמש בפונקציות כגון count או average - על מנת לתמצת את המידע.

ב Kibana:
  • Sampling נקרא Query
  • Clustering נקרא Buckets 
  • ו Reduction נקרא Metrics.
נו, בסדר.

הנה דוגמה להגדרה של ויזואליזציה פשוטה, המראה את ההתרעות (warnings) הנפוצות ביותר בפרודקשיין:

  1. אני רוצה לדגום רק הודעות מסוג Level=Warning וב Production (שהן פחות מ 0.1% מכלל ההודעות במערכת).
    1. אני משתמש בשאילתא שמורה - שזה נוח וחוסך טעויות / maintenance.
    2. אני יכול להוסיף, לצורך הוויזואליזציה, פילטורים נוספים על שאילתת הבסיס. שימושי.
  2. אני מאגד (clustering) את הנתונים ע"פ מחלקה (class בקוד ממנו יצאה ההתרעה) וע"פ שם השירות ממנו יצאו ההודעות.
    1. לסדר בו אני מגדיר את האשכולים - יש משמעות, אותה אסביר בהמשך.
  3. בכל דלי ("Bucket") יש לי עכשיו עשרות או אפילו מאות הודעות, וייתכן שמסוגים שונים. על מנת להתמודד עם כמות המידע - אני סוכם את ההודעות, להלן count" aggregation" (פעולת reduction).

הערה: מבחינת UI, קיבנה שמה את ה Metrics (כלומר: ה reduction) לפני ה buckets (כלומר: clustering).
יש בזה משהו הגיוני, לכאורה - אך אני מוצא שזה הרבה יותר נוח להתחיל מ count (כלומר: reduction ברירת המחדל), לראות שיש בכלל תוצאות ושהגדרתי את ה buckets נכון - ורק בסוף לגשת להגדיר את ה reduction אם הוא מורכב יותר (למשל: ממוצע הזמנים ע"פ שדה אחר xyz). לכן סימנתי את המספרים 1,2,3 בסדר שאינו תואם לסדר ב UI.



והנה התוצאה:


  1. הנה האשכול הראשי: שם המחלקה שממנה נזרקה ההתרעה.
    1. כאן אני רואה את מספר האיברים בכל אשכול ראשי.
      בקיבנה הגדרות שונות של סכימה - מתארות גם את ה UI שיווצר. מצד אחד - זה מקצר תהליכים, מצד שני - מקשה עלי להשיג בדיוק את ה UI שאני רוצה.
  2. האשכול השני שהגדרתי הוא ע"פ ה micro-service ממנו נזרקה ההתרעה.
    1. כאן אני רואה את מספר האיברים בכל אשכול שני.
      האשכול השני מתאר חיתוך שני. כלומר: ה count שאני רואה הוא מספר האיברים שגם שייכים למחלקה שהופיעה בעמודה #1, וגם לשירות בעמודה #2 - ולכן, זהו המספר שמעניין אותי.
  3. בחוכמה, הזמן הוא ציר שאיננו מקובע בשאילתא (או sampling), ואני יכול לשחק עם הערכים מבלי לבצע שינויים בשאילתא. כלומר: אני יכול בקליק לראות את הערכים עבור שעה, יום, שבוע, וכו'. מאוד שימושי.


הנה תרשים, שמנסה לעזור ולהמחיש את מה שקורה (זה חשוב):

  • לקחנו המון נתונים - ובחרנו רק חלק מהם (מתוך כל המרחב, או חלקו). זה שלב ה Sampling.
  • ה cluster הראשון כולל חיתוך ראשוני. 
    • ויזואלית - לא נראה שה clusters לא מכסים את כל מרחב ה sampling. אפשר שכן ואפשר שלא - לצורך פשטות התרשים - ציירתי אותו כך.
      בדוגמה למעלה - ה cluster הראשון מכסה את כל המרחב, כי לכל הודעה יש ערך לשדה ה class.
  • ה cluster השני מבצע חיתוך נוסף על החיתוך הראשון.
    • ה aggregation שלו (במקרה שלנו: count), מייצג את האיברים באזור החיתוך (הסגול).
  • אפשר להוסיף גם cluster שלישי ורביעי. לוגית - זה אמור להיות מובן, אבל פשוט קשה לצייר את זה 😏.


סוגי הויזואליזציות העיקריים בקיבנה 5.5. אלו המסומנים בורוד הם אלו שאני מוצא כשימושיים ביותר.
Timelion ו Visual Builder הם יותר מורכבים - כמעט נושא בפני עצמו.



עוד כמה פרטים חשובים על Aggregations


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

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



  1. ישנן כמה שיטות לקבץ נתונים לאשכולות, חלקן שימושיות יותר - חלקן פחות.
  2. השיטה השימושית ביותר, לדעתי, היא ע"פ Term.
קצת היסטוריה:
עד גרסה 5 של Elastic Stack, הטיפוס הנפוץ לשדות היה String - מחרוזת.
על שדה מסוג מחרוזת היה ניתן להגדיר תכונה בשם analyzed.

מחרוזת שהיא analyzed הייתה מפורקת למונחים (terms) בודדים, למשל "elastic stack" היה מפורק ל "elastic" ול "stack" ולכל term היה נרשמת רשומה באינדקס הפוך האומר לכל מילה - באילו מחרוזות היא נמצאה. זה חלק ממנגנון שנקרא "Full Text Search".

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

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

term הוא בעצם non_analyzed string ו text הוא בעצם analyzed string. לפעמים אתם תמצאו שדה המופיע בשתי הצורות. למשל: message מסוג text, ו message.term או message.raw מסוג term.

clustering ע"פ term אומר שאנו יוצרים אשכול / דלי לכל ערך אפשרי של השדה.

למשל עבור השדה class ייווצרו כ 220 אשכולות, כי יש לי במערכת לוגים מ 221 מחלקות שונות של המערכת.
220 הוא מספר גדול מידי להציג בוויזואליזציה ולכן עלי לציין כמה אשכולות אני רוצה לאסוף (3). בחרתי להתייחס בוויזואליזציה ל 25 האשכולות שמכילים הכי הרבה פריטים (descending) - כלומר רק 25 המחלקות ששלחו הכי הרבה התרעות - יכללו בוויזואליזציה.
שימו לב שב Tab ה Options יש הגדרות שעשויות להגביל את מספר הערכים המוצגים בוויזואליזציה.


עוד מושג שרק אזכיר הוא Significant Term המתייחס ל term שהנוכחות שלו בתוך ה sampling היא בולטת. למשל: ה sampling שלי מצמצם את הנתונים רק להודעות מסוג התרעה (Warning). המחלקה com.seriouscompany.Server לא שלחה הרבה הודעות לוג (רק 0.009% מכלל ההודעות), אבל בהודעות מסוג התרעה - חלקה הוא 0.4%. יש מחלקות שאחראיות גם ל1% ו 2% מכלל ההתרעות - אבל הן גם אחראיות 0.5% ו 0.7% מכלל הודעות הלוג.

יחסית לכמות ההודעות שהיא שולחת באופן כללי, המחלקה com.seriouscompany.Server שלחה אחוז גבוה ביותר של הודעות מסוג התרעה - ולכן כ significant term היא תחשב כבעלת הערך הגבוה ביותר.

significant term הוא כלי שימושי להבלטת אנומליות בהתנהגות המערכת.

אני מקווה שזה ברור.


בקצרה, ולקראת סיום, כמה מילים על סוגי ה clustering השונים בקיבנה:
  • Histogram פועלת על שדה מסוג Number ותקבץ לדליים intervals קבועים שהוגדרו. אם שדה "זמן הריצה" מכיל ערכים בין 0 ל 6553 מילי-שניות, ונקבע histogram עם internal של 100 - ייווצרו 66 clusters שיכילו טווחים כמו 0-100, 101-200, וכו'.
  • Date Histogram - היא היסטוגרמה על זמנים. היא יכולה להיות ע"פ דקות / שעות / ימים או מצב auto - בו קיבנה בוחרת interval שייצור כמה עשרות בודדות של clusters.
  • Range - טווחים קבועים שמוגדרים על ידכם. למשל 0-100, 500-101, ו 1000-10000 (תוך התעלמות מכוונות מכל הערכים בין 501 ל 999).
  • Date Range - רשימה קבועה של טווחי זמנים שהוגדרה על ידכם. לדוגמה: 9 עד 11 בבוקר, ו 16 עד 18 אחה"צ.
  • Filter - רשימה קבועה של תנאים לוגים על שדה, שכל תנאי מייצג cluster. למשל: *error, warning*, או nullPointerException.
  • IPv4 ו GeoHash הם clustering מאוד ספציפיים על השדות שבהם הם עובדים. GeoHash למשל יכול לקבץ ע"פ מיקומים גיאורגפיים (למשל: מהן מגיעות הבקשות). אפשר לייצר איתו ויזואליזציות מאוד מגניבות - ולא תמיד כ"כ שימושיות.

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


אזהרה חשובה: ב metrics עומדים לרשותכם aggregations ע"פ אחוזונים: האחוזון ה 95 של ערך מסוים, ה 96, ואפילו ה 96.5.
זה נשמע מגניב, אפילו New Relic לא מאפשר לחתוך נתונים (למשל: זמני ריצה של פונקציה) ע"פ כל אחוזון שרירותי.

אליה וקוץ בה: LogStash לא באמת אוסף את האחוזונים השונים. הוא אוסף כמה נתונים (min, max, median, ועוד מספר סופי של "ריכוזי נקודות" על הטווח) - ויוצר קירוב של התפלגות. יהיה האלגוריתם שבשימוש TDigest או HDR Histogram - ההתפלגות היא רק קירוב להתנהגות הנתונים האמיתית, ולא משהו שבאמת אפשר להסתמך עליו לניתוח ביצועים אמיתי.

ריבוי אוכלוסיות (למשל: המשתמשים שמשתמשים בתכונה x מתפלגים נורמאלית A, ואלו שלא משתמשים בתכונה x - המתפלגים נורמאלית A') - עשוי בקלות להתפוגג תחת הקירובים, ו"להעלים מהראדר" התנהגויות חשובות.

אם אתם רוצים למדוד ביצועים - השתמשו ב New Relic או ב TimeSeries DB (גם ELK+Kibana מנסים להיכנס לתחום, להלן Timelion visualizations), לא ב percentiles של ELK.


מקור נוסף: Calculating Percentiles.



סיכום


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

כמובן שרב עדיין הנסתר על הגלוי.

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


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




----

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

על Heatmaps & Point Series - ויזואליזציות נחמדות!

Filebeat vs. logstash - אם אתם תוהים מהו "Filebeat", ומה תפקידו בכוח.

grok (כלי לפירסור לוגים) שייתכן שתתקלו בו.

Kibana official Reference - שהוא לא-רע



יום רביעי, 22 בנובמבר 2017

Evolutionary Design - הרצאה מתוך רברסים 2017


כמעט שכחתי!!

הנה ההרצאה שלי על Evolutionary Design מברברסים האחרון.

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


יום שבת, 18 בנובמבר 2017

ג'אווה, גרסה 3 - להתעורר מהתרדמה הדוגמטית


שפת ג'אווה. קהילה אדירה, אימוץ משמעותי וארוך שנים...

- אבל גם שפה שהגיל ניכר בה:
  • בעיקר, אבל לא רק: תחביר verbose - מרבה-במילים, שכבר לא מקובל בשפות חדשות (למשל: swift, go, וכו') - ומסיבה טובה.
  • Generics מבוסס "מחיקת טיפוסים" - שהופך את הדברים לקשים יותר, עניין שצץ מחדש בשימוש ב Steams. מחיקת הטיפוסים נעשתה במקור על מנת לשמר תאימות לג'אווה 1.4. למי אכפת היום מקוד שכתוב בג'אווה 1.4?!
  • רעיונות טובים שהתבררו כשגויים לחלוטין לאורך השנים - כמו checked exceptions.
  • פרימיטיביים מול אובייקטים ו auto-boxing.
  • חתימת זיכרון גדולה למדי של ה JVM.
  • ועוד...

הוותיקים שביננו בוודאי זוכרים, ששפת ג'אווה התחילה בתור ״Project Oak״ - כשפה ל"טלוויזיות חכמות" איי שם בשנות ה-90.

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

ואז הבינו שהיכולת של "Write Once, Run Anywhere" (או בקיצור: WORA) היא יכולת שמצוי הפוטנציאל שלה הוא לא בטלוויזיות חכמות (יחסית לשנות ה-90), ולא בדפדפנים - אלא דווקא בצד השרת.

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

היום, "שרתים רציניים" מריצים Linux, ופיתוח הכי נוח לעשות על Mac (ה Linux Subsystem על "חלונות 10" מנסה קצת לאזן את התמונה), אבל היתרון שב WORA - חשוב כבעבר.


את התובנה הגדולה זו - הציגו בכך שהכריזו (ובצדק) שג'אווה עברה לשלב הבא: Java 2!

נוספו מאז ה J2EE, ה JVM נפתח כקוד פתוח - והשאר היסטוריה.



מקור: https://plumbr.io/blog/java/java-version-and-vendor-data-analyzed-2017-edition




לעשות שינוי


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

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

פרויקט ה Jigsaw ("פאזל", ה Java Module System) החל בשנת 2005. הוא תוכנן להשתחרר כחלק מג'אווה 7 (שהתעכבה ל 2011) - אבל שוחרר עכשיו, רק בג'אווה 9 - עשור ושלוש שנים מאז שהחלה העבודה עליו.

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


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


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


הפתרון הברור הוא להציג את שפת Java גרסה 3: גרסה מחודשת מהשורש, ולא תואמת-לאחור - של השפה.

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


הרבה מימים אפשר לייצר עם המספר 300


חבלי מעבר



המעבר מ"גרסה 2" ל"גרסה 3" - אמור לצלצל לכם מוכר. מזהים משהו?


לא? ... נסו שוב.



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

המעבר היה מתבקש, מכיוון שהיה כמו תכונות "ישנות" של פייטון שכבר היו "בלתי-נסבלות":
  • מחרוזות לא היו Unicode (אלא ANSI?)
  • התנהגויות בעייתיות של הטיפוס הכ"כ בסיסי int:
    • אופרטור החלוקה / היה מעגל את התוצאה למספר שלם - מקום נפוץ לשגיאות.
    • int היה כמו BigInteger של ג'אווה - שזה נהדר, חוץ מהעובדה שקוד פייטון מרבה להשתמש בספריות ++C/C (עבור יעילות - זה חלק מהמודל ההנדסי של השפה) - אבל אז ההמרה יצרה בעיות.
  • אופרטור השווה (=, <, >, וכו') שהיה מופעל על טיפוסים שונים - היה משווה את ה id של האובייקטים, אם לא הייתה מוגדרת צורת ההשוואה - מקור לצרות אמיתיות.
בדרך הכניסו בגרסה 3 גם שיפורים שימושיים (super ללא פרמטרים, set literal, וכו'). המודעות לחשיבות התאימות-לאחור הייתה קיימת , ובהחלט נמנעו משינויים חשובים, אך לא הכרחיים. למשל: בנאי (constructor) בפייטון עדיין נקרא __init__, עוד מהתקופה שמפתחי השפה הניחו שרק הם זקוקים לבנאים - אבל משתמשי השפה לא יזדקקו להם. זהו שינוי מתבקש, אך לא "הכרחי" - ולכן לא נכלל בפייטון 3.


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

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


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

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

וכך - כל העגלה נתקעה. קריאה לקהילה לבצע את המעבר לא נענתה בחיוב, או לפחות לא בקצב מהיר. הוקמו אתרים כמו http://py3readiness.org ו http://python3wos.appspot.com שמדדו ועודדו - את האימוץ של פייטון 3.


פייטון 3 שוחררה ב 2008.

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


מקוור: http://blog.thezerobit.com/2014/05/25/python-3-is-killing-python.html

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

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

הגרסה הנוכחית של פייטון 3 היא גרסה 3.6 - וקראתי כתבה שטענה ש 80% מהקוד החדש נכתב כיום בפייטון 3.
95% מהספריות הנפוצות כבר תומך בפייטון 3, וחלקים מהן מפסיק לתמוך בפייטון 2 (דוגמה: Django). במצב הזה, כנראה - קיוו יוצרי פייטון להיות בשנת 2010. זה קרה "רק" 8 שנים מאוחר יותר.


"הגרסה הבאה של פייטון תהיה פייטון 8, והיא לא תתאם לאחור בכלום" - מתיחת 1 באפריל

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

מקור: https://snarky.ca/why-python-3-exists

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

נכון, ג'אווה 9 שברה תאימות לאחור לג'אווה 8 - וזה לא עניין של מה בכך. בכל זאת, השינוי נוגע לניהול packages ותלויות - ולא לשפה עצמה. מכיוון שג'אווה היא strongly typed ו static - לא קשה ליצור כלים שיגלו וינחו בשינויים הנדרשים, להלן jdeps. כן צפוי קושי עם ספריות שעושות שימוש כבד ב reflection, ספריות כמו Hibernate או AspectJ.
שינוי בתחביר של השפה - הוא סיפור אחר לגמרי.

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



יש פתרון!


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

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

במקרה שלנו: נספק שני סוגי סיומות קבצים:
  • java. - עבור ג'אווה 2.
  • java3. - עבור ג'אווה 3.
כל עוד התחבירים השונים יוכלו לחיות זה לצד זה - גם באותו הפרויקט, ולתקשר זה עם זה (להפעיל פונקציות / לקבל ערכי חזרה) ללא קושי מיוחד - הפתרון יעבוד. תמיכה ל Java.V2 יש לספק לעוד עשור, לפחות. 

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

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





האם נרצה לבצע את השיפורים רק בתחביר השפה, או גם ב JVM?
יש כמה דברים טובים שאפשר לשפר ב JVM:
  • היכולת לצרוך פחות זיכרון (תהליך של ג'אווה מגיע לצריכת זיכרון של חצי GB - דיי בקלות) - הוא שיפור חשוב למיקרו-שירותים, ול FaaS (למשל: AWS lambda).
  • שמירת טיפוסי Generics בזמן ריצה (להלן "reified generics") - הוא שיפור מבורך שיתיר קוד מסובך במקרים רבים. הוא גם יאפשר לכתוב קוד שלא ניתן היום לכתוב בג'אווה.
מצד שני, שינוי ב JVM עלול להיות קשה וארוך לביצוע. לא נרצה לסיים במצב בו עלי להריץ שני תהליכים עבור Interoperatiblity: תהליך של JVM2 ותהליך של JVM3. אני אסיים עם יותר צריכת זיכרון ו overhead משמעותי (IPC) בתקשורת שבין התהליכים.

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



תוכנית עבודה



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

אם נתחיל עכשיו, ונוציא את המטלה מידי ה Java Committee - אנחנו יכולים להגיע לשם כבר בסוף שנת 2023!


הבשורות המשמחות הן שתהליך כזה כבר החל בשנת 2011!

ג'אווה 3 כבר מוכנה ועובדת. יש עשרות אלפי מתכנתים שעובדים בה במשרה מלאה. היא production-ready ועם interoperability מוצלח למדי לג'אווה 2!
אני יכול להעיד זאת לאחר עבודה לאורך החודשיים האחרונים עם JavaV3 בסביבת פרודקשיין.

לא פחות משמח: חברת אורקל תמשיך לתמוך ולפתח את Java.V2 באהבה ובמסירות עוד שנים רבות. זה לא פחות חשוב - כי סביר שקוד Java.V2 עוד יחיה שנים רבות.


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

ולכן, זה הזמן לקהילה החופשית לעשות את השינוי.

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

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

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

מקור: http://pypl.github.io/PYPL.html - בעת כתיבת הפוסט


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

בעקבות האימוץ הטבעי של קוטלין בקהילת האנדוראיד, וגם בשל הלחץ שהופעל עליה מצד קהילת ה iOS (מול Objective-C Java.V2 נראתה טוב, אבל בהשוואה ל Swift - היא כבר נראית עייפה ומסורבלת) - גוגל הפכה את קוטלין לשפה רשמית של אנדוראיד.

קוטלין היא שפה מודרנית בהחלט: תחביר פשוט, פונקציות הן 1st Class citizens בשפה עצמה, יש לה הגנה בפני nulls, שיפורים לספריות, extension functions, ועוד). קוטלין גם מאפשרת Interoperability מוצלח מאוד מול Java.V2, ויש לה IDE חינמי, הרץ על שלושת מערכות ההפעלה - עם תמיכה מעולה בשפה.

בקיצור:

קוטלין היא המועמדת הטובה ביותר לתפקיד Java.V3!

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

מה נותר? נותר לנו - הקהילה, לאמץ אותה!

אני באמת לא רואה סיבה מדוע לא לאמץ אותה בחום בצד-השרת. עשינו זאת בחברת Next Insurance - בהצלחה רבה!

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


האם באמת מפתחי הג'אווה שבעו מחידושים, והם מסתפקים בזחילה לכיוון ג'אווה 9, או ב Spring Boot - כהתקדמות בסביבת העבודה שלהם?!

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

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

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



לסיכום (עודכן)


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

בכלל, כמה וכמה חידושים ברמת השפה בג'אווה 9 נראים דומים בצורה מחשידה ליכולות שיש כבר בקוטלין: Stream API improvements? - כבר יש. private interface methods - כבר יש. REPL? - ברור שיש. גם default interface methods הגיעו לקוטלין לפני שהגיעו לג'אווה.
אני חושד שהיוצרים של קוטלין מסתכלים על ה JSRs המתגלגלים של ג'אווה, ומביאים אותם לקוטלין - פשוט יותר מהר.

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


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

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

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


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

אשמח לתגובות :-)




יום שבת, 28 באוקטובר 2017

Plan/Execution Segregation

כאשר אנו כותבים תוכנה שאמורה לשרת תהליכים עסקיים מסוימים, חשוב מאוד שמבנה ("מודל") התוכנה יתאם למודל העסקי.

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

כל זה מוכר לרוב הקוראים, אני מקווה. אני חוזר על התובנה (החשובה) הזו שוב ושוב.

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


איך זה מתחיל?


ההתחלה המודעת של הסיפור היא, בד"כ - כבר לאחר כברת-דרך.

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

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

מידלנו את ההזמנה כאובייקט Order, המכיל רשימה של אובייקטי ListItem.

ListItem הוא "Pattern" נפוץ ושימושי למקרים כאלו.

מכיוון שהעלות של פריט (Item) היא תכונה המשתנה לאורך הזמן (temporal) זו תהיה טעות להצביע מההזמנה ישירות לפריט. מחיר הפריט השתנה לאחר שנה? גם מחיר ההזמנה השתנה, לא משנה שהיא כבר הסתיימה.

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

עד כאן - טוב ויפה!


הבעיה מתחילה (לכאורה) כמה חודשים לאחר ההשקה:

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

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

דרישות כמו הנ"ל מריחות כמו בקשות ל"פיצ'ר פירוס" (Pyrrhic Feature) - פיצ'ר שלא באמת יפתור את בעיה, אלא רק יקבע ויחמיר אותה. על פיצ'ר כזה נאמר: "עוד פיצ'ר כזה - ואבדנו".

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

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



איך זה באמת התחיל? (סיפור ה Prequel)


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

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

למה לתחזק נתונים כפולים ב Database? למה סתם לשכפל קוד? הרי אומנו "לחפש ולהשמיד" כל כפילות קוד אפשרית.

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

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


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

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


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

השאלה המהותית היא: האם האובייקטים במקרה זהים כרגע, או האם הם באמת מייצגים את אותו הדבר לטווח ארוך?



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


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

פריטים (lineitems) שקיימים בהזמנה עשויים לא להיות במלאי - ולכן לא להיכלל במשלוח.
  • "אין בעיה! נמחק אותם מהרשימה"

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

אולי המשלוח יתבצע למקום אחר מאשר מה שהוזמן במקור? ("כן, אדוני השליח! אני לא בבית. תביא את זה בבקשה לחמתי או לשכן").
  • "נעדכן את הכתובת. זה קל!".

התנהגות המערכת לגבי החזרת פריטים במשלוח, וביטול פריטים בהזמנה היא כנראה שונה.
  • "השאלה בקוד: (if(deliveryTimeStamp != null תספק את ההבחנה, מה הבעיה? טודו-בום, הכל בסדר!".

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


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



הכל בסדר שם?

שלוש בעיות



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


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

Smell נפוץ הוא משפטי if בקוד המנסים לבדל בין הזמנה ומשלוח ← ולהתנהג בצורה אחרת.
התוצאה היא קוד יותר מסובך, שקשה יותר לתחזק.

במקום לפצל את הקוד ליחידות קטנות יותר שלכל אחת אחריות קטנה ("divide and conquer") - יצרנו ערמת קוד גדולה יותר להתמודד איתה ("unite and endure").



בעיית הטווח הבינוני: חוסר בהיסטוריה

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

Smells נפוצים לבעיה הזו הם:
  • יש לנו כל מיני שדות שמנסים לחפות על חוסר בהיסטוריה AmountIfDelivered, או cancellationDate ביחד עם realCancellationDate.
  • יש לנו audit (לוג או טבלה) - דרכו אנו מנסים לדבג תקלות, ובעצם ליצור "היסטוריה חלופית".
    בני אדם (מפתחים, support) יכולים לקרוא את הלוג - אך קשה מאוד לקוד שרוצה לבצע פעולה להסתמך עליו (כי הוא לא well-structured והפורמט עלול להשתנות לאורך הזמן).
  • אנו מוסיפים טבלאות ייחודיות ל BI - כי הם זקוקים להבנת ההיסטוריה, שאנחנו לא מסוגלים לספק להם עם נתוני הליבה שלנו (core data, לא של אפל).
במקרים הללו הקוד גולש, תחת הלחצים לתקן עוד ועוד באגים ולטפל במקרי קצה שלא דמיינו בתחילת הפיתוח - ל"קוד Job Security", כזה שרק מי שכתב - יכול באמת להבין ולשנות. שינוי משמעותי - לרוב כרוך ב rewrite של כל המנגנון.

האם לארגון שלנו יש מספיק משאבים ל re-writes הללו?



בעיית הטווח הארוך

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

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

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




Plan Execution Segregation



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

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

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

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

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

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


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

כאשר מבצעים הפרדה בין Plan ל Execution חשוב שתהיה דרך לקשר בין האובייקטים. בד"כ יהיה על אובייקט הביצוע (Execution Object) רפרנס (id?) לאובייקט התוכנית (Plan Object).






עוד המלצות נלוות:



מה השלב שבו הופכים Plan ל Execution? האם זה מועד הביצוע? זמן קבוע לפניו (למשל: חצי שעה לפני)?
  • כלל נפוץ הוא לעשות זאת על בסיס Resource Allocation: לרוב execution לא יוכל להתבצע ללא הקצאה של משאבים. כאשר המשאב הוקצה (ונרשם) - יש סיכוי טוב שתרצו לעשות את המעבר. למשל:
    • Order של מונית הופך ל Ride כאשר הוקצה נהג. זה יכול לקרות מיד (ואז הלקוח ממתין עוד כמה דקות להגעת המונית) או אולי כמה שעות לפני הנסיעה - כאשר זו הזמנה לנסיעה עתידית.
    • כאשר אין משאב ברור, המעבר לרוב יקרה ברגע תחילת הביצוע. למשל: PaymentPlan הופך ל Payment , ברגע בו התחלתי לבצע את התשלום. 
      • תשלום אגב יכול לארוך שניות, דקות, ואף שבועות: קיבלנו reject מסולק אחד, ואנו פונים לסולק אחר, אין מספיק כסף בחשבון כרגע וננסה "לנדנד" עד שיהיה, כרטיס אשראי בוטל - אך עדיין רוצים "לרדוף" אחרי התשלום, וכו'.


    תיאור ה Execution כהצלחה (SUCCESS) או כשלון (FAIL) לעתים רבות אינו מספיק:
    כאשר הביצוע מורכב מכמה פעולות ו/או כמה שלבים:
    • יש פעולות שלא הורצו בכלל - כי הן תלויות בפעולה אחרת. האם הן הצלחה, או כישלון?
    • יש פעולות שמתמשכות לאורך זמן, ולמרות שהמשכנו מהן הלאה - עדיין לא ברור אם הצליחו. למשל: המשלוח הסתיים - אבל רק מחר בבוקר נקבל טלפון שחבילה לא הגיעה ללקוח.
    • אולי קרו כמה כישלונות, כמה לקוחות קיבלו פריטים לא נכונים. האם יש הבדל בין בעיה אחת לשלושה?
    • פתרון נפוץ הוא לחשב את ה state בצורה דינאמית:
      • הרבה פעמים נוח להחזיר רשימה של בעיות, במקום סיכומם כערך יחיד. יש צרכנים של הפונקציה שיספרו רק אם מספר הבעיות גדול מאפס (או ()errors.isEmpty), ויש כאלו שיכנסו יותר לפרטים.
      • לעתים שווה גם לציין פעולות pending או suspension - שעדיין לא התבצעו, ולכן לא ברור מה המצב שלהן. יש הבדל גדול בין "0 תקלות משלוח!" לבין "0 תקלות משלוח, אבל 6 חבילות עדיין לא סופקו". 
      • השם pending מרמז יותר על פעולות שמתבצעות כסדרן אך לא הסתיימו, ו suspension על פעולות שהתעכבו / הסתבכו - אך עדיין לא ברור אם יצליחו לתקן ולסיים אותן בהצלחה.
        • אם יש suspensions - לרוב כדאי לקבוע גם deadline שהופך אותן לכישלון. לא נרצה להיות ב state לא ברור לאורך זמן.

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



    סיכום


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


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



    יום רביעי, 18 באוקטובר 2017

    Evolutionary Design - מצגת מרברסים 2017

    היי,

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

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





    ליאור

    יום שבת, 16 בספטמבר 2017

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

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


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



    Scope Functions


    נתחיל בפונקציות ה"חדשות" ביותר למפתחי ג'אווה - או כך לפחות נדמה לי: ה scope functions.
    הדמיון הרב ביניהן - הוא דיי מבלבל!

    נפתח בהגדרות:

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

    את הפונקציה ()with, אני מניח שכולם מכירים. אני זוכר אותה עוד מימי Object Pascal...

    הפונקציה ה"כמעט-תאומה" שלה היא apply:

    1. כפי שאתם רואים הן דיי דומות: משתמשים בהן כאשר רוצים לבצע שורת פעולות על ביטוי מורכב (או סתם משתנה עם שם ארוך), כאשר:
      1. ב with שולחים את הביטוי כפרמטר.
      2. ב apply - כ extension function על הביטוי.
    2. יש גם הבדל בערכי ההחזרה:
      1. הביטוי של with יחזיר את ערך (כלומר: ה evaluation) של הבלוק.
      2. הביטוי של apply יחזיר את האובייקט עליו הופעלה apply.
    3. זה כל ההבדל? בשביל זה יצרו שתי פונקציות כ"כ דומות?
      האם apply היא פשוט עבור עצלנים שלא מסוגלים לעשות extract variable?!
    4. ובכן... דווקא ערך ההחזרה הוא החשוב - המאפשר ב apply לשרשר את הפעולה. זה מתאים לשרשרת פעולות שכבר אין לכם "ביד" את ה reference לאובייקט המדובר - ואז apply מאפשרת את המשך השרשור.


    הפונקציה הבאה שנפגוש, ()run - עשויה להישמע קצת מוזרה: היא רק מריצה בלוק.
    את הבלוק שתתנו לה - היא תריץ.

    מה הטעם בכזו פונקציה? למה היא שימושית?!

    טוב... הדוגמה הראשונה באמת מעוררת השתוממות.

    הדוגמה השניה - מסבירה את העניין:
    כאשר אתם מריצים את run - אתם יוצרים scope חדש/נוסף להרצה.

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

    זה עובד עבור הדוגמה הקטנה הזו - אבל יכול לעבוד גם בדוגמאות מורכבות יותר.


    קיימת גם פונקציית run שרצה כ extension function, הדומה קצת apply:


    run המקבלת למבדה מתאימה, כמו apply, לפעולות שרשור - אבל ערך ההחזרה שלה הוא ה evaluation של הבלוק.
    היא שימושית כאשר יש שרשור, ואנו רוצים לבצע חישובים על האובייקט ואז להחזיר ערך - למשל: פונקציית ה ()genrate בדוגמה שסיפקתי.
    כמו apply - היא "חיה" ב scope של האובייקט (כי היא extension function), ולכן קיימת גישה לפרמטרים של האובייקט.

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


    שאלה: האם ()x.applyAndReturn היה יכול להיות שם מוצלח יותר ל ()x.run?



    הגענו לזוג האחרון: let ו also.

    הפונקציה let דומה לפונקציה map, כאשר היא פועלת על איבר יחיד.


    במקום להיות extension function, היא מעבירה את האובייקט עליו היא פועלת - כפרמטר (it).
    היתרון שבכך?

    במידה ואתם רוצים בבלוק להתייחס ל this - האובייקט החיצוני. פונקציות כמו apply עושות shadowing ל this המצביע לאובייקט בו רצים. let לא עושה זאת.

    כמו כן, let מחזירה את ה evaluation של הבלוק.

    שימוש נפוץ ב let הוא כתיבה קצרה להגנה בפני null:

    1. הדוגמה הזו נכשלת בקומפילציה: מכיוון שמדובר ב property ולא משתנה "אטומי", ייתכן ומאז בדיקת ה null ועד להפעלת הבלוק - ייכנס ל property ערך אחר null-י ש"יפיל" אותנו.
    2. דרך אחת בטוחה היא להעתיק עותק מקומי למשתנה - ולבדוק אותו. הכי טוב val.
    3. דרך יותר קצרה ואלגנטית, היא השימוש ב let: הפונקציה מוערכת ברגע אחד מסוים - כשה evaluation של הביטוי עליה פעלה כבר בזיכרון:
      1. אם ה evaluation הוא null - כל הבלוק לא ירוץ.
      2. אם ה evaluation אינו null - הבלוק ירוץ, וניתן להתייחס ל it בבטחה כ not-null.

    שם אפשרי אחר לפונקציה ()let היה יכול להיות ()ApplyItAndReturn.


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

    היתרון: היכולת לשרשר.

    אני אשאיר לדימיון שלכם לתת לה שם יותר משמעותי....


    בקרוב תצא קוטלין 1.2 עם פונקציות ה scope החדשות: ()due(), just  ו ()bound.  

    סתתתאאאם! 😉








    Streams


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

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

    בהשוואה בזכוכית מגדלת, כל מימוש "פונקציונלי" היה קצר פי כמה - מהמימוש המקביל בג'אווה.

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

    אי אפשר היה להתעלם מצהלת השמחה בקהילת הג'אווה, שחשה גאווה רבה:


    אמנם צריך להוסיף את המילה המעצבנת stream, וגם Collector לפעמים - אבל זה היה בהחלט נסלח, מול היתרונות.


    האמת?

    קוטלין הוציאה גם פה את ג'אווה באור לא מחמיא. היא מצליחה לפשט משמעותית תחביר ה stream של ג'אווה לתחביר נקי הרבה יותר.

    למשל, הדוגמה הלא-מחמיאה הבאה:


    יכולה להיכתב בקוטלין כך:


    השילוב של ה Collections של קוטלין, ויכולות השפה (פרימיטיביים הם אובייקטים, extension functions, ועוד) - הופכות את היתרון -> למשמעותי מאוד.

    הערה של קורא: נכון: הדוגמה של קוטלין עושה רק print ולא println ולכן היא קצרה יותר. טעות שלי.



    חזרה על עקרונות הבסיס של Streams

    ביטוי סטרימי (Stream-י) יהיה בנוי מ:
    • מקור: מקור נתונים. בדר"כ מבנה נתונים מאולס של Java שהופעלה עליו הפונקציה ()stream או Stream שנוצר במיוחד.
      • ה Stream עשוי להיות "אינסופי" (למשל: רצף מספרים אקראיים) ולכן ניתן להגביל את מספר האלמנטים בהם רוצים לטפל בעזרת הפונקציה ()limit.
      • רצפים אינסופיים ניתן לייצר בעזרת פונקציית (Stream.generate(lambda - כאשר lambda מספקת את הערך הבא, או (Stream.iterate(lambda - כאשר lambda מספקת את הערך הבא, תוך כדי שהיא מקבלת את התוצאה הקודמת כפרמטר.
    • פעולות ביניים (Intermediate Operations)
      • אלו פעולות שמקבלות Stream ומחזירות Stream - כך שניתן לשרשר אותן, ולהרכיב אותן זו על זו - בכל הרכב שנבחר. למשל: (...)filter(...), map, או (...)limit
      • באופן מעשי, הפעולות לא מחויבות לפעול ברגע (או סדר) ה evaluation שלהן - כך שמתכנני מגנון ה Streams יכולים להוסיף אופטימיזציות שונות. 
      • מה שיגרום לשרשרת הפעולות להתחיל ולפעול - הוא המצאות פעולת הסיום.
    • פעולת סיום (Terminal Operation) היא התוצאה המצופה מן כלל ביטוי ה Stream.
      • זוהי פונקציה שמקבלת Stream אבל לא מחזירה Stream (בהכרח). למשל: ()sum(), findFirst, או ()findAny.
        • השם findFirst הוא קצת מבלבל: למה צריך "לחפש" את האיבר הראשון?
          • בפועל: לא מחפשים אותו (זמן הריצה יהיה (O(1) - אבל זהו אילוץ שמחייב את ה Stream לשמור על סדר האיברים.
          • כאשר מפעילים את ()findAny - אין אילוץ כזה. בד"כ יחזור האיבר הראשון, אבל לפעמים יחזור איבר אחר מהרשימה (אם הופעלה איזו אופטימיזציה).
      • פעולות סיום נפוצות אחרות הן:
        • (forEach(lambda - שיכולה לבצע פעולה שרירותית כמו הדפסה של האיברים, אבל אחד אחרי השני ולפי הסדר.
        • (reduce(lambda - שיכולה לבצע "סיכום של תשובה" כאשר מגיעים אליה 2 פרמטרים: תשובה חלקית, והאיבר הבא (נניח: חישוב ממוצע מסוג מסוים). בשימוש בה - ניתן לבצע אופטימיזציות על ה Stream.

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

    קוטלין מימשה מנגנון Steam משלה, שנראה מאוד דומה - אך בנוי באופן מובנה על ה collections של השפה.
    המימוש ה default-י של streams בקוטלין אינו כולל lazy evaluation ואופטימיזציות או יכולות מקבול כמו בג'אווה. אפרט עוד על ההבדלים בהמשך.
     את תחביר ה Streams בקוטלין מפעילים ללא פעולת ה ()stream - בכדי להתחיל stream, ולא צריך את פעולות ה ()collect על מנת להמיר אותו חזרה ל collection ולטפל בטיפוסים שונים:

    בקוטלין אפשר פשוט לסיים את פעולת ה Stream ב ()toList בכדי לקבל רשימה.
    רוצים מערך? השתמשו ב: ()toList().toTypedArray.

    מפה? השתמשבו ב ()associate  או ()associateBy:


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

    האמת: עברתי על 10 השאלות הנפוצות של התג "java-stream" באתר stackoverflow כדי לראות במה כדאי לעסוק בפוסט - ובקוטלין כיסו בצורה אלגנטית את כל הבעיות שהופיעו ב 10 השאלות הללו. נראה לי שגם הם הסתכלו - על אותה הרשימה בדיוק.


    הפעולות בקוטלין בעלות שמות זהים ברוב המקרים. הנה כמה הבדלים:
    • findFirst ו findAny נקראות first ו any - בהתאמה.
    • limit נקראת בקוטלין take.
    • peek (כמו forEach, רק שמחזירה Stream) נקראת בקוטלין onEach (שם יותר ברור- לטעמי).


    מעבר לכך - הטיפול ב Streams הוא ממש דומה.

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

    • flatmap היא שימושית כמובן כאשר אנו מפעילים פונקציה שמייצרת רשימה - אף אנחנו רוצים את האיברים שבה, או כאשר אנחנו רוצים להפוך איבר אחד ב stream - למספר איברים.
    • חשוב לזכור ש filter משאיר (ולא מסיר) - את מי שעומד בתנאי.
      • filterNot - מסיר.

    • ()takeLast הוא ההופכי ל take, ו ()drop - הוא המשלים.
    • ()takeWhile ימשיך לקחת איברים כל עוד הפרדיקט נכון. ברגע שנתקל בתנאי שלילי - הוא יעצור.

    הנה כמה פעולות סיום נפוצות:

    • כמה פעולות סיום, כמו ()last ו ()first מופיעות ב 2 צורות: כפונקציה ללא פרמטרים, או כפילטר עם הפעולה מובנה. 
      • הצורה האידיומטית היא צורת הפילטר - כאשר זה אפשרי.
    • ()single תזרוק Exception אם לא נמצאו איברים, או שנמצא יותר מאיבר אחד. 
      • יש גם גרסאת ()singleOrNull - שפשוט מחזירה null.
    • ()fold היא ()כמו reduce, רק שהיא מקבל כפרמטר ערך התחלתי לעבוד עליו. במקרה שלנו - אפס.
      • יש גם ()foldRight שפשוט תפעיל את הפעולה בסדר הפוך: מהאיבר האחרון - לראשון. במקרה של חיבור התוצאה תהיה זהה.


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

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



    Late Evaluation


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

    לצורך כך בקוטלין יש מנגנון דומה לזה של ג'אווה של lazy evaluation הנקרא Sequences (שם שונה על מנת למנוע התנגשות בשם מחלקות).


    בקוד הקוטלין, כל מה שצריך להוסיף הוא ()asSequence בתחילת הביטוי.
    הפונקציה asSequence  ממירה את ה collection ל lazily evaluated sequence, בדומה ל Steam של ג'אווה.

    לאובייקט ה Sequence יש מימושים מתאימים ל filter, map, first ועוד - כל הפונקציות שיכולות לאפשר מצב של אופטימיזציה.

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

    המחיר של השימוש ב sequence הוא שלא יהיו לנו זמינות סט הפעולות שלא יכולות לעבוד במוד של lazy eval כמו ()takeLast או ()foldRight. במקרים מעטים, בהם יש עבודה אינטנסיבית שנהנית מ memory / resource locality - ה Sequence עלול להיות פחות יעיל.


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

    במידה ואתם כותבים תשתית חישובית ל big data, רוצים parallel streams - עליכם להשתמש בתשתית ה Streams של ג'אווה (עדיין אפשר לכתוב את הקוד בקוטלין).



    סיכום



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


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



    יום שישי, 15 בספטמבר 2017

    רברסים 2017 - הרצאה על Software Design


    ההרשמה לרברסים 2017 נפתחה!

    לעניות דעתי - זה הכנס הטוב ביותר בהייטק הישראלי.


    אני מרצה על Software Design (ראשי התיבות הם סתם המצאה - אין ראשי תיבות כאלו מקובלים), ובמקביל לחיים ידיד - שעובד איתי!


    אם אתם מגיעים לכנס - קפצו להגיד שלום!


    ליאור

    יום שני, 4 בספטמבר 2017

    קוטלין (Kotlin) למפתחי ג'אווה ותיקים - חלק ו': Collections ו Generics

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


    הפעם נדבר על Collections ו Generics - נושאים שעברו כמה התאמות מהגרסה הג'אווה-ית.


    Generics - תזכורת


    מהם בעצם Generics ("חסרי ייחוד")?
    הותיקים-באמת שביננו זוכרים את הימים של Java 1.4 בה כל collection בשפה היה מטיפוס Object. בכל שליפה של איבר מתוך הרשימה - היה צריך לבצע פעולת Down-Casting.


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

    בשל שיקולים של תאימות-לאחור, ה generics בג'אווה (וליתר דיוק: ב JVM) הם ברמת הקומפילציה (ולא ה runtime). יש שלב של הקומפיילר בשם Type Erasure בו הוא מוחק את ה generics, ומחליף אותם במבני-נתונים מסוג Object עם down-castings מתאימים + מוסיף בדיקות שפרמטרים שהוזנו למתודות הם מהטיפוס הנכון.

    זהו. זה כל מה שמפתח צריך לדעת על Generics, לא?!


    יש קצת יותר.
    אפשר להשתמש ב generics במחלקות שלנו, ולא רק ב Collection המסופקים ע"י ג'אווה.

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

    1. מי ששולף Entities מה Repository צריך לעשות downcasting - בקוטלין, בעזרת המילה השמורה as.
    2. אין בדיקה ברמת הקומפילציה שאני שולח ערכים רלוונטיים לפונקציות... אאוץ.
    3. אני לא יכול בתוך המחלקה Repository להתייחס לתכונות הספציפיות של האובייקט שאני רוצה להשתמש בו.

    כאשר אני משתמש ב Generics - הדברים נראים אחרת:

    1. אני מגדיר ב scope של ה class טיפוס בשם T, ממש לפני הגדרת המחלקה שבסוגריים המסולסלים. 
      1. אני יכול להשתמש עכשיו ב T במקום טיפוס, בכל מקום בקוד של המחלקה.
      2. ברגע שייווצר instance של המחלקה הזו, טיפוס מסוים היה קשור אליה, וכל התייחסות ל T - בעצם "תוחלף" ע"י הקומפיילר בטיפוס שהוגדר.
    2. אין צורך להצהיר על downcasting מתודות שליפה. הקומפיילר דואג לכך.
    3. הקופיילר יאכוף שהערכים שנשלחים הם מהטיפוס הנכון.
    4. עדיין אני לא יכול לגשת בתוך המחלקה לתכונות של הטיפוס הספציפי.
    5. זה נפתר ע"י כך שאגדיר את הטיפוס: T היורש מ Entity.
      1. הקומפיילר יוודא שטיפוסים שנקשרים למחלקה יורשים מ Entity.
      2. כך בתוך קוד המחלקה, אוכל להניח של T יש את כל התכונות / פונקציות הזמינות של Entity.
    6. שימו לב ש T כברירת מחדל הוא מטיפוס ?Any. אם ארצה שהטיפוס יהיה לא-nullable יהיה עלי להגדיר: <T : Any>

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

    1. רק טיפוס שגם יורש מ Entity וגם מממש את הממשק Comparable - יוכל להיקשר למופע של המחלקה. 
    2. where הוא המקביל של קוטלין לצורה <T extends ClassA & InterfaceB> של ג'אווה.

    מדוע משתמשים ב "T" לתאר את הטיפוס הלא ידוע? מתי יש שמות אחרים?
    הקונבנציה אומרת ש:
    • T - אם יש משתנה אחד.
    • S - אם יש משתנה שני, U - אם יש משתנה שלישי, ו V - אם יש משתנה רביעי.
      אפשר לזכור את הסדר כ "SUV" - השם האמריקאי ל"ג'יפון עירוני".
    • K ו V - אם יש צמד key  ו value, למשל ב Map.
    • E - כדי לתאר אלמנט במבנה נתונים.
    • N - לתאר טיפוס שהוא מספר.
    • R - לתאר טיפוס החזרה (return value).



    "חורים" ב Generics


    בג'אווה קיימת הבעיה הבאה:


    אני יכול להגדיר מבנה נתונים מסוג <List<String, בכדי לקבל הגנה של הקומפיילר.

    אבל... אם המתודה שלי, במקרה הזה ()unsfaeAdd (שעשויה להימצא במקום אחר ומרוחק בקוד), מצהירה על ממשק כללי List (להלן "raw type") - הקומפיילר יאשר את הקוד: הרי <List<String הוא List - חייבים זאת עבור תמיכה לאחור.

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


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



    Variance


    הפתרון המקורי של ג'אווה היה להוסיף כלי שנקרא wildcard (מסומן כ ?).


    במתודה שלי (נניח שהיא במקום מרוחק בקוד) אני מצהיר שאני מקבל רשימה - אבל לא יודע באמת מה הטיפוסים שמאוחסנים בה (<?>List נקרא כ "List of some type"). הקומפיילר לכן יאפשר לי לבצע רק פעולות שליפה - אך לא פעולות השמה. זוהי בעצם הגנה בפני התנהגות לא צפויה.

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

    בכלל, כמה אנשים נוחים עם ההגדרה הבאה?


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

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


    אם נדבר בשפה פורמאלית, אזי String הוא variance של Object - כי הוא יורש ממנו, אבל <List<String הוא invariant של <List<Object - כי הוא לא יורש ממנו (הוא יורש מ <Collection<String).


    הפתרון של קוטלין לבעיה הנ"ל הוא מעט אחר, ומוממש כחלק מהספריה הסטנדרטית.
    הספריה הסטנדרטית (stdlib) של קוטלין היא קטנה בהגדרה (כ 750KB) - ומכילה התאמות לג'אווה.

    בגזרת ה collections, קוטלין לא מציגה collections חדשים מאלו של ג'אווה (Set, Map, Array, List) - אלא רק עוטפת ומרחיבה אותם (פעמים רבות - בעזרת extension functions).

    קוטלין מספקת ממשקים למבני-נתונים (Map, List) מ-2 סוגים:
    • Immutable Interfaces - שהם ברירת המחדל, כאלו שניתן רק לשלוף מהם.
      • למשל:<List<E ו <Map<E
    • Mutable Interfaces - כאלו שניתן לבצע בהם גם שינויים.
      • למשל: <MutableList<E  ו <MutableMap<E


    התחליף של קוטלין, אם כן, ל wildcard של ג'אווה הם immutable interfaces. 
    בהגדרת הפונקציה unsafeAdd קוטלין לא מרשה לי להשתמש ב Raw type כמו List - אלא רק במבנים עם הגדרה גנרית.



    הנה אנסה כמה תצורות נוספות:

    1. כאן יש שגיאת קומפילציה: ניסיתי להוסיף איבר למבנה נתונים שהוא immutable - אסור. זוהי ההגנה המקבילה ל wildcard.
    2. כאן הגדרתי שאני רוצה מבנה נתונים שניתן לבצע בו שינויים. אבל מה? מכיוון שהגדרתי את list מטיפוס String - הקומפיילר לא מוכל לקבל any.
    3. הנה התיקון - ביצעתי המרה מסודרת של o למחרוזת - והכל תקין.

    הפתרון של קוטלין, להגדיר immutable collections הוא פשוט יותר מהפתרון של ג'אווה, הוא לכאורה "לא מפורש".
    הסמנטיקה של immutable collections שימושיים למדי גם ל "functional-like programming" ול concurrency.

    corner case שכן הפסדנו בקוטלין, הוא היכולת לעשות ()clear או ()remove ל collection המכיל איברים מסוג לא ידוע. אין סכנה להסיר איברים מסוג "לא ידוע", ולכן ניתן לעשות זאת ב <?>List, אבל לא ניתן לעשות זאת ב immutable list.

    Tradeoff הגיוני, לדעתי.



    אוקיי. פתרנו מקרה אחד בעייתי של Generics, אבל יש עוד מקרה בעייתי:

    נ.ב. - קוד דומה גם לא יתקמפל בג'אווה
    הרי: Int הוא מספר (יורש מ Number) - ולכן אני מצפה שהקוד תעבוד.
    הבעיה: <Array<Int אינו יורש מ <Array<Number - הם invariants.

    Immutable collection לא יעזור כאן. מה עושים?



    Covariance & Contravariance


    נפתח בהגדרה.

    מבנה גנרי כלשהו Something המקיים ש:
    • טיפוס T הוא  subtype  של טיפוס A
    • וגם ניתן להתייחס ל <Something<T כ  subtype  של <Something<A
    נקרא covariance.

    בג'אווה אפשר להגדיר קשר של covariance בצורה הבאה:

    Something<? Extends A>

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

    הנה דוגמה:


    מה ניתן לעשות במתודה ()foo?

    קריאה
    • אפשר להתייחס לכל איבר בשלושת הרשימות כ Number - כולם כאלה.
    • אי אפשר להתייחס לכל איבר בהכרח כ Integer - כי אז "אפול" בטיפול ב list3.
    • אי אפשר להתייחס לכל איבר בהכרח כ Double - כי אז "אפול" בטיפול ב list2.
    כתיבה
    • לא ניתן להוסיף לרשימה Integer - כי אז "אפול" ב list3.
    • לא ניתן להוסיף לרשימה Double - כי אז "אפול" ב list2.
    • לא ניתן להוסיף לרשימה גם Number - כי אז "אפול" ב list2 וב list3 המחייבות טיפוסים ספציפיים (אחרת ניפול בשליפה + casting, כמו בדוגמה למעלה).



    Generics. הקומפיילר יעזור למנוע טעויות.




    Contravariance

    הקשר בו מבנה גנרי כלשהו Something מקיים ש:
    • טיפוס T הוא  supertype  של טיפוס A
    • וגם ניתן להתייחס ל <Something<T כ  supertype  של <Something<A
    נקרא contravariance.

    בג'אווה אפשר להגדיר קשר של covariance בצורה הבאה:

    Something<? Super A>

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


    מה ניתן לעשות במתודה ()goo?

    קריאה
    • אי אפשר להתייחס לכל איבר בהכרח כ Integer - כי list2 ו list3 לא מכילים Integers בהכרח.
    • אי אפשר להתייחס לכל איבר בהכרח כ Number- כי list3 מכיל אובייקטים שונים.
    • ניתן רק להתייחס לאיברים כ Object - כי תמיד הם יהיו כאלה.
    כתיבה
    • ניתן, מן הסתם, להוסיף לרשימה Integers - כי כל הרשימות יכולות להכיל Integers - בהגדרה.
    • ניתן להוסיף subtypes של Integer לו היו: למשל, אם היה PositiveInteger שהיה subtype של Integer.
    • לא ניתן להוסיף Double או Number, וגם לא Object - כי תהיה לנו את list1 שבה מתבצעת בדיקה שנכנסים רק Integers (או subtypes), כדי להימנע מהבעיה של שליפה + casting שראינו למעלה.


    Generics. הקומפיילר יעזור למנוע טעויות [א].



    ובחזרה לקוטלין...


    הסמנטיקות של ג'אווה,  extends A ? ו super B ? הן מוצלחות בלהזכיר מתי ? יורש מ A, או מתי הוא אב של B - אבל לא כ"כ מוצלחות בלהזכיר לנו את ההתנהגות הצפויה: מה מותר לקרוא ומה מותר לכתוב. זה לא self-explanatory.

    בכדי לעזור לזכור, ג'ושוע בלוך הציג את הכלל הבא: "Producer Extends, Consumer Super", או בקיצור PECS.

    הווה אומר:
    • אם המבנה הגנרי מספק ערכים (Producer / אנו קוראים ממנו) - השתמשו ב extends, ויהיה לנו אסור להוסיף פריטים לרשימה.
    • אם המבנה הגנרי צורך ערכים (Consumer / אנו כותבים אליו) - השתמשו ב super, אך לא נוכל להסתמך בקריאה על איזה טיפוס יצא.

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

    • Something<out T>   ==> producer
    • Something<in T>   ==> consumer

    במקום לחשוב איזה טיפוס לא ידוע ירחיב או יירש מ T - אנו פשוט מצהירים:

    • האם אנחנו מתכוונים לשלוף ערכי T (או בנים שלהם) - בשימוש ב out.
    • או האם אנחנו הולכים להכניס למבנה ערכי T (או אבות שלהם) - בשימוש ב in.

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

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

    בואו נראה את in ו out בשימוש. הנה למשל ההגדרה של הממשק List:


    מכיוון ש List הוא Immutable, הגדירו את המבנה הגנרי <out E> - וכך ניתן לשלוף E או sbutypes של E בצורה בטוחה.


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

    אם ג'אווה (ליתר דיוק: ה JVM) היה תומך ב reified generics, כאלו שנושאים metadata ב runtime - ההתעסקות הזו הייתה נחסכת מאיתנו. זה המחיר ששילמו בג'אווה 5 על מנת לספק generics עם תאימות לאחור לקוד ישן יותר.


    ה Variance בקוטלין הוא declaration-site variance, כלומר: כזה שנקבע בשלב ההגדרה - כמו ב  < List<out E שראינו למעלה. הקומפיילר "קשר" את הטיפוס E (או בנים שלו) למופע הרשימה - ואין צורך להצהיר על זה יותר.

    בג'אווה ה variance הוא use-site variance, כלומר יש מגדירים את ה variance על השימוש - על המתודה. למשל.
    הנה המתודה ()addall של המחלקה Collection:


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

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

    מה עושים בקוטלין?



    Type Projections


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


    ל SomeStructure קשור טיפוס T כלשהו - אבל אני יכול להחליט שבפונקציה copy אני מצפה למבנה של T או supertypes שלו - לקריאה בלבד.  הדוגמה הייתה עובדת גם אם SomeStructure היה קשור ל <in T>.

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

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


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


    if (x is Collection<*>) ...

    הכלי הזה נקרא star projection והוא מקביל להגדרה ?out Any וגם in Nothing.




    Reified Generics in Kotlin



    היינו רוצים reified generics - אך החלטות design של ה JVM לא מאפשרות זאת.
    בקוטלין, בכל זאת, התירו שימוש ב reified generics בפינה קטנה, שעשויה להיות שימושית לפעמים.

    כאשר יש פונקציה שהורינו לקומפיילר לעשות לה inline - הקומפיילר יכול לאפשר בה שימוש ב reified generics - כאלו שיהיו זמינים ב runtime. למשל:

    1. כאשר הערך T קשור לפונקציה, מה יותר טבעי מלבדוק אם משתנה מסוים הוא מאותו הסוג?
      1. אופס! ... T קשור רק בזמן קומפילציה ואז הוא נמחק. הוא לא זמין ב runtime ולכן לא ניתן לבצע reflection: הקומפיילר פשוט לא יכול לנתח איזה ערך יישלח בזמן הרצת התוכנה.
    2. כאשר אני מגדיר את T כ reified - הקומפיילר יודע לבצע את האנליזה המתאימה כאילו יש לי את המידע ב runtime.
      1. זה יכול לעבוד רק על פונקציה שהיא inline.

    לא ניתן לקרוא מקוד ג'אווה לפונקציה שהוגדרה כ reified: בכל מקרה הפונקציה היא inline והקומפיילר של ג'אווה לא ימצא הגדרה של פונקציות inline ב class files.




    ולקינוח...


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


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

    1. ראשית קושרית בסוגריים משולשים טיפוס או טיפוסים ל scope של הפונקציה.
      1. האם יש טעם להצהיר T extends Object? זה לא מיותר?
        1. לכאורה כן: ההגדרה <T> שקולה ל <T extends Objects> כשהיא מופיעה לבדה.
        2. כאשר יש הגבלות (&), אם לא נצהיר על Object, ה erasure יתבצע להגבלה (Comparable) - שחסרה כמה מהתמודות של Object. בקוד הזה החליטו לקבוע erasure ל Object (שהוא גם Comparable שאליו קשור טיפוס לא ידוע שהוא supertype של T).
    2. ערך ההחזרה של המתודה (...)min הוא T. פשוט מאוד.
    3. שם הפונקציה.
    4. רשימת הפרמטרים. במקרה שלנו אנו מקבלים Collection אחר, של איברים לקריאה בלבד - שהם T או subtypes של T.

    נראה פשוט, לא?



    סיכום


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

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


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



    ---


    [א] שווה לציין: