4 חטאים של פיתוח תוכנה בן-זמננו [דעה]

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

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

  • בספרי תכנות, למשל, הפרקים המאוחרים (לעתים מאוגדים כ "Advanced Topics") הם לרוב נושאים פחות שימושיים ביום-יום. ביום פקודה – אפשר להשלים את הידע נקודתית. זו גישה מאוד הגיונית.
  • היתרון מלהכיר עוד Frameworks הולך ופחות ככל שאתם מכירים יותר Frameworks. אם אני מכיר כבר שני Web Frameworks בצורה טובה – איזה יתרון באמת יהיה לי מללמוד את השלישי?!
  • אפשר ללמוד אינספור כלים וספריות, אבל אם לא עובדים בהם בצורה משמעותית – זה יידע שלא יעשה בו שימוש ו/או יישכח במהרה.
  • ישנם נושאים קצת יותר רחוקים מכתיבת הקוד עצמו, אך מספיק שונים בכדי לספק לנו "קרקע בתולית ללמידה". הרבה פעמים יש להשקיע בהם השקעה משמעותית מאוד – עד שנראה תמורה אמיתית ביום-יום שלנו. למשל: Machine Learning, מערכות מבוזרות, או Big Data. לא בטוח שזה אפיק משתלם עבור רוב אנשי-התוכנה.
 

—-

אני רוצה להציג תוכן משמעותי ללמידה, בדמות 4 מיומנויות שנמצאות בחסר בתעשייה.

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

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

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

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

אז מה יש לנו?

TDD – איך כותבים בדיקות מוצלחות, ואיך כותבים קוד שקל לכתוב לו בדיקות מוצלחות.

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

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

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

מה הן בדיקות טובות? יותר קל לי להציג אלמנטים נפוצים של בדיקות לא טובות:
  • אנשים מגזימים בכמות הבדיקות המערכתיות (System Test, Integration Tests) על חשבון בדיקות ממוקדות. גלידה ולא פירמידה. זה כ״כ נדוש ושחוק, אך עדיין – טעות שממשיכה ונעשית.
  • אנשים לא מבודדים Pure Business Logic משאר הקוד לצורך unit tests – ואז באמת קשה מאוד מאוד לכתוב ולקבל את היתרונות של unit tests.
    • נתקלתי הרבה פעמים במצב הזה, וזו בעיה שיחסית קל לתקן, ברגע ש״נופל האסימון״ – ומשנים גישה.
  • אנשים כותבים יותר מדי קוד בדיקות – מה שמאט את העבודה שלהם, ומקשה על ביצוע שינויים במערכת:
    • גם בדיקות שהן overfit למימוש ארעי (situational), כלומר תנאי שמתקיים – אך אינו חשוב ועקרוני לפעולת המערכת / הביזנס. בהמשך הוא ישתנה, לא תהיה בעיה עסקית – אך הבדיקות יפלו וידרשו עדכון.
    • גם בדיקות שהן יתירות (בודקים את אותו הדבר שוב ושוב באופנים שונים). כל שינוי של מימוש קוד – ידרוש סדרה של שינויים בקוד הבדיקות – מה שיגרום לנו לרצות לעשות פחות שינויים.
      • יעילות מגיעה מניהול סיכונים נכון: האומץ לצמצם את כמות הבדיקות (לא לכתוב בדיקות מסוימות), מתוך הבנה אלו בדיקות חשובות ומשמעותיות יותר.
  • אולי הכי גרוע: בדיקות ועוד בדיקות שנכתבות (ומתוחזקות!) מבלי שהן מגרדות את פני השטח. הן בקלות יכולות לעבור – בזמן שמשהו עקרוני ולא טוב קורה ב flow. בקיצור: בדיקות לא-משמעותיות.
    • זכרו: אם הבדיקות שלכם אף פעם לא נשברות – זו לא סיבה לגאווה. זה אומר שבזבזתם את הזמן בכתיבת בדיקות שלא אומרות כלום.
  • אנשים שהתייאשו מבדיקות ו״למדו״ (אבוי!!) – שבדיקות הן נושא overrated ומיותר.
    • זהו מצב שמאוד קשה להתאושש ממנו.
לסיכום: Programmer Testing הוא כלי כ"כ חשוב, כל כך יישומי, ושמביא תוצאות כ"כ טובות ומהירות (כשעושים אותו נכון), ועדיין – רק אחוז נמוך בצורה מבהילה של אנשי-תוכנה באמת שולט בפרקטיקה הזו.

 

המחשבה שאם אתם מכירים את הספרייה שאיתה עושים בדיקות (JUnit5, Jasmine, RSpec, Sinon), אזי אתם יודעים "לכתוב בדיקות טובות" – היא שגויה מיסודה. חפשו את העלות/תועלת: כמה השקעה יש בכתיבת ותחזוקת הבדיקות – מול כמה זמן הן מקצרות בתהליכי ה debug ותיקון שגיאות.
 

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

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

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

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

Refactoring אקטיביסטי

הנה עוד דבר שעשוי להישמע מעליב: ״אני לא יודע לעשות Refactoring טוב מספיק? יש לך מושג כמה פעמים כבר עשיתי Refactoring? מה הבעיה בלעשות Refactoring?״

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

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

המון! מן הסתם.

ברגע הזה, שקורה לרובנו על בסיס יומי, עומדות בפנינו שתי ברירות:

  • להמשיך הלאה.
    • יש לי מנהל עם ״סטופר״ שיבוא בתלונות אם לא אדלוור פיצ'רים מהר.
    • יותר גרוע: שינוי בקוד הוא סיכון ליצירת באג. אם יש באגים שנוצרו על ידי – אני יוצא לא-טוב. (איפה ה Unit tests שלכם עכשיו, הא?)
  • לבצע Refactoring ולהחזיר את הקוד לרמה אופטימלית X (כלומר: רמה טובה, אבל לא מושלמת. שלמות היא בזבוז).
    • Refactoring אינו צריך, ועדיף שלא יהיה "פרויקט ענק". הוא יכול להיות בכל commit שלישי.
    • אם שומרים על רמת קוד טובה לאורך הזמן – יהיה הרבה פחות צורך בפרויקטי ענק.
אז מה אתם בוחרים?
 
לרוע המזל רוב אנשי-התוכנה בוחרים ב"דרך הבטוחה". זה עובד? – אז לא לגעת! 
אשמה גדולה היא בקרב המנהלים, שהם קצרי רוח לזמני ביצוע של פיצ'רים והופעות של באגים – אך יש להם מספיק סבלנות ל"פרויקטי תחזוקה", ופיצ׳רים פשוטים / חקירות באגים שמתארכות לאורך ימים.
 
החטא של המפתחים הוא שהם תורמים את חלקם למעגל המזיק הזה – ובעצם פוגעים באינטרסים שלהם.
התמריץ לשמר את הקוד ברמה "אופטימלית X"  הוא לא רק עניין של ערכים "אני בעד קוד יפה", חלילה!
יש פה אינטרסים מעשיים:
  • קוד שמתוחזק ברמה גבוהה – יאפשר להוסיף פ'יצרים נוספים בצורה קלה ומהירה יותר, ועם פחות תקלות.
    לאורך הזמן השאלה צריכה להיות: האם אתם רוצים לעבוד בקוד מתוחזק, או בקוד "עולם שלישי"? באיזו סביבה אתם חושבים שתתפתחו, אישית – בצורה טובה יותר?
  • כאשר בוחשים בקוד – רמת העומק וההבנה האישית שלנו את הקוד, ומה שקורה בו – צומחת בקצב אחר לגמרי.
    • אני לא יכול להדגיש זאת מספיק: מי ששובר את הקוד (או לפחות מסתכן בשבירה) – הוא מי שמבין אותו לעומק. "לשבת על הברזלים" זו אסטרטגיה נוחה לטווח הקצר – אך נחותה לטווח הארוך.
עוד אלמנט חשוב הוא היכולת שלנו לראות כיצד הקוד יכול ללבוש צורות שונות – והיכולת להעביר את הקוד בקלות מצורה לצורה: אולי functional? אולי לולאת foreach? אולי break ואולי exceptions.
  • בעיות שונות בקוד יפתרו באלגנטיות רבה יותר בעזרת צורות שונות של קוד. 
    • כאשר אנשים מקובעים לתבנית אחידה / סגנון אחיד – זה מגביל!
    • לאנשים רבים, גם כאלו עם ניסיון של שנים – חסרה ממש הגמישות הזו: קשה להם לקרוא ולהבין קוד בסגנון שונה, והם חוזרים וכותבים קוד בצורה "שהם רגילה אליה" – גם במקרים בהם היא מסורבלת וקשה לקריאה.
  • Refactoring תכוף – הוא דרך נהדרת ללמוד ולהתנסות בצורות קוד שנות. זה האינטרס האישי שלכם!
  • שווה לציין גם טכניקה בשם "Coding Dojo״ שאמורה לפתח מנעד רחב יותר של סגנונות קוד:
    • מתכנסים כמה אנשים בחדר ופותרים תרגיל קוד קטן כאשר מחליפים ידיים כל פרק זמן נתון (מעבירים את המקלדת מאדם לאדם). עוד נוהג הוא לעשות את אותו התרגיל – מספר פעמים. בכל פעם – תהיה תוצאה קצת אחרת.
    • נ.ב. אני נוטה להאמין שיעילות המפגש שכזה היא ביחס ישיר לאדם המוכשר ביותר בסגנונות קוד שנוכח בו.
בקיצור: האם יש ״סגנון שנוח לכם איתו״ ורק בו אתם כותבים?
האם אתם עסוקים ב"לשמור על הקונבנציות" יותר מאשר לחשוב ולהבין איזו צורה של קוד היא הטובה ביותר לבעיה?
האם אתם "חכמים מספיק לא להתעסק עם קוד שעובד". מעתיקים מדוגמאות קוד אחרות במערכת – מבלי לצלול לעומק מדוע הקוד הזה עובד?

 

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

האם אתם משקיעים 10% מהזמן שלכם, בשיפור קוד קיים? – אני ממליץ לחתור לשיפור קטן כל יום.

Design to Go

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

  • עבודה ב Small Batches.
  • יצירה של Short and Effective feedback cycles.
  • בחינת אלטרנטיבות – מתוך ההבנה שיש יותר מדרך משמעותית אחת לסדר קוד ו/או לפתור בעיה.
    • ״קו האפס״ הוא פתרון יחיד שעובד – ומשם משפרים. 
    • אחרת: אנחנו עובדים על עצמנו. לא משנה כמה מלבנים ציירנו בדרך.
  • כאשר ״תקיעה״ בתהליך הדזיין, מובילה אותנו לוותר עליו – במקום לעבור ל Exploration.
כבר דיברתי הרבה בנושא בהרצאה שלי ברברסים. אין טעם לחזור.
 
מקור: Integrating and Applying Science" (pg. 136) – http://ian.umces.edu/press/publications/259/

 

 

Modeling

 

Modeling היא לא פרקטיקה נפוצה בקורות החיים של אנשים. 

המונח ״Medling״ כנראה מובן לרוב האנשים, אך הוא לא נתפס כנושא בעל חשיבות עליונה – שכדאי לפתח.
  • הזכרנו כבר שנקודת מפתח ב Design היא בחינת אלטרנטיבות.
  • החלק המשמעותי באלטרנטיבות הללו הוא לא ״אובייקט גדול״ מול ״שניים קטנים״ – אלא מידול שונה של האובייקטים העסקיים. למשל: ״תשלום, הכולל ניסיונות תשלום״, מול ״נסיונות תשלום הכוללים תוצאה״.
  • ״גמישות לדרישות עתידיות״, ו״פשטות״ הם BuzzWords – אך הם גם סופר-משמעותיים במבחן התוצאה. 
    • מודל פשוט וטבעי לביזנס – יכול בהחלט להכפיל את התפוקה של הצוות.
      מיומנות מעטות בעולם התוכנה עשויות לגרום להשפעה (impact) רבה שכזו!
  • היכולת לעשות modeling נכון נובעת מניסיון תמידי להבין את הביזנס והצרכים + הפעלה של חשיבה ביקורתית.
    • קל לצייר בראש מודל – שלא ממש מתאים לביזנס. חשוב לתקשר ולאמת אותו.
    • לא תמיד אנשי הביזנס יתחברו למודל – וחשוב גם לנסות ולאתגר אותם.
  • Modeling לא נעשה רק בשלב דזיין – אלא גם כתהליך refacotring, שינויים קטנים כל הזמן.
  • Modeling מתקשר בד״כ למידול של אובייקטים עסקיים, אך הפרקטיקה נכונה גם למודל טכני (מודל concurrency, מודל eventual consistency, מודל sevurity):
    • שואלים ומאתגרים כל הזמן מה הם הצרכים
    • מנסים למצוא מודל פשוט ואלגנטי ככל האפשר, פשוט ע״י איטרציות של שיפורים במודל.
    • מתקשרים את המודל – כך שיהיה רעיון משותף, ולא ״מחשבה פרטית״.
  • איך לומדים לעשות מודלינג?
    • ע״י צפיה בדוגמאות של מודלים. למשל הספר PEAA (דוגמה יפה: המודל של Money), או הספר המעולה (אך קצת מיושן): Analysis Patterns – של אותו המחבר.
    • ע״י בניית מודלים והפקת לקחים אישיים.

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

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

 

ה Killer instinct

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

כשאתם נתקלים ב״Killer Instinct״ – קשה להתעלם ממנו.

  • זה השילוב של הבנת ביזנס, חשיבה ביקורתית, קריאה נכונה של הארגון (מי מדבר שטויות, מי יודע), קצת חוצפה (ממי להתעלם, למי להתייחס), והאומץ לבצע שינויים / לכתוב קוד שיש לו חסרונות ברורים – לצד יתרונות ברורים, כמובן.
    • תמיד נתקלתי ב Killer Instinct בצמידות לנטייה לגעת בקוד ולשנות אותו. חוסר פחד, ביחד עם סקרנות ורצון לחולל שינויים.
      אני נוטה להאמין שיש פה גם אלמנט של סיבתיות: הניסיונות הקטנים לשפר את הקוד -> יוצרים הבנה עמוקה של הקוד (עם הזמן). הבנה עמוקה של הקוד -> מאפשרת את ה״מאסה הקריטית״ של העומק – הדרושה בכדי לבצע שינויים משמעותיים במערכת בזמן קצר.
  • ״להתעסק״ עם הקוד בלי שיש בדיקות טובות – לא כדאי. הקוד ישבר, וההתעסקות תהפוך לעניין כואב ומתסכל.
  • הבנה עמוקה של הקוד, ללא הבנה של הביזנס – עשויה לפספס את האימפקט:
    אתם עושים שינוי עמוק במערכת, שאף אחד לא האמין שאפשרי – אבל אז גם לאף אחד לא אכפת הוא נעשה, כי הוא פשוט לא מעניין.
  • בכדי ליצור אימפקט, חשוב להבין את הביזנס. הבנה של הביזנס נבנית מתוך Modeling.
  • בכדי שהתוצר יהיה טוב יותר, ומשמעותי גם לאורך זמן – חשוב גם לדעת איך לעשות Effective Design.
 
האם זה מספיק? האם זה המתכון הסודי והמובטח לשחרור ה״Killer Instinct״?
לא. מן הסתם זה גם עניין של אופי: חוצפה/תעוזה, הרבה אכפתיות ורצון עז להשפיע.

 

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

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

קוטלין (Kotlin) למפתחי ג'אווה ותיקים – חלק ה': DSLs

פוסט זה הוא חלק מסדרה:

אחד החלקים המתקדמים בשפה (באופן טיפוסי בשפות-תכנות?) הן היכולות לייצר DSL – כלומר: Domain Specific Language.
DSL היא "תת-שפה" המשמש בחלק מהמערכת לתיאור יעיל יותר של Domain מסוים. למשל: תחביר לייצור JSON, לגישה לבסיס הנתונים, לניהול חוקים עסקיים, תיאור UI, וכו'.

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

FluentInterface – המעט שג'אווה יכולה להציע

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

קוד של חברת ענק ש"דלף" לרשת. שם החברה שונה – על מנת להתגונן בפני תביעות פטנטים.

ראינו את ה DSL המוגבל של ג'אווה,  ו DSL … גרוע (ודמיוני) של Enterprise Java. כיצד נראה DSL יותר מרשים?

יוצרי קוטלין ראו מה שפת Groovy מאפשרת בתחום ה DSL… ראו והתרשמו. הנה דוגמה:

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

מעוז סמלי של יכולות ה DSL של גרובי הוא כלי הבילד Gradle – מפתחי ג'אווה רבים (ברובם אנדרואיד?) משתמשים ב DSL של Gradle בכדי להגדיר build scripts פשוטים, ויעילים – תוך כדי שהם נהנים מבדיקת-התחביר של הקומפיילר.

אם ה API של Gradle היה מבוסס על שפת ג'אווה – לא היו מצליחים להגיע תחביר כ"כ פשוט ומינימליסטי.
לפני כשנה Grade החלה לתמוך בקוטלין כשפת ממשק First Citizen ל Gradle.
השימוש בקוטלין כשפת הממשק ל Gradle עדיין לא נפוץ, אך קהילת הקוטלין בקרוב תעקוף את קהילת הגרובי בגודלה – ודברים עשויים להשתנות

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

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

הנה רשימת "יכולות ה DSL" העיקריות של קוטלין:

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

ברוכים הבאים לעולם היפה והמתעתע של הגדרת DSL 😄

Infix Functions

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

אפשר להפעיל את המתודה foo על האובייקט A בתחביר רגיל (1) – או מקוצר.
התחביר המקוצר אפשרי רק כאשר הפונקציה מוגדרת כ infix, ו modifier של infix ניתן להוסיף לפונקציה רק כאשר היא מקבלת פרמטר בודד.

מתי זה שימושי?
זה יותר שימושי על טיפוסי בסיס: String, Boolean, וכו'. הנה דוגמה מ KotlinTest:

אבל… טיפוסי הבסיס הם לא שלנו – הם שייכים לשפה או לספריות הסטנדרטיות. מה עושים?

כאן באה לידי ביטוי יכולת חשובה של השפה בשם Extension Functions.

Extension Functions

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

קוטלין הושפעה מיכולת של שפת #C בשם Extensions Methods – ויצרה יכולת דומה הנקראת Extension Functions. אפשר פשוט להרחיב מחלקה, מבחוץ – מבלי "לפתוח" אותה:

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

בואו נשלב פונקציות הרחבה עם infix extension בכדי לבנות את הבסיס לספריית הבדיקות העתידית של קהילת הקוטלין:

  1. הנה הפעלה של extension function ל String – בתחביר הרגיל.
  2. הנה הפעלה בתחביר ה infix. רצף הקריאה הוא קולח יותר.
  3. נשלב את הקריאה לפונקציית למבדה.
  4. ניתן לשרשר קריאות infix אחת על השנייה.
    1. הרחבתי את האב הקדמון Any להכיל מתודה thenAdd – שתהיה זמינה לכל אובייקט בשפה.
    2. לא כל מה שאפשר להגדיר כ "DSL" הוא באמת קריא יותר: זו דוגמה לרצף קריאה מבלבל, שכנראה רק מי שהגדיר את ה DSL – יבין…

כדי לא לבלבל, כדאי להקפיד על הכללים הבאים בעת הגדרת infix functions:

  • על שם הפונקציה להיבחר בקפידה – עבור רצף הקריאות (readability flow). לא לבלבל ולא להפתיע.
  • אם אתם מתכננים לשרשר קריאות Infix נסו שהטיפוס יישמר, ולא לעבור בין טיפוס א' (Boolean) לטיפוס ב (מחרוזת) – כמו בדוגמה (הרעה) למעלה.
  • אל תזרקו exceptions מתוך infix functions.
    • infix function נראית כמו מילה שמורה בשפה – והמשתמש לא יצפה לאפשרות של exception (כפי שהוא לא מצפה ממילה שמורה בשפת קוטלין)

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

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

יש. הנה הם לפניכם:

  1. יצרנו מחלקה עם המתודה ()foo – ואז הרחבנו אותה. "מי תיקח"?
  2. בקוטלין יש כלל מאוד ברור: member תמיד קודם ל extension function.
    1. אתם אולי יכולים לראות של ()C.foo – יש warning ב IDE שאומר: הפונקציה הזו מוחבאת ע"י ה member – ולעולם לא תוכל להיקרא.
  3. עכשיו יש לנו 2 מחלקות היורשות זו מזו: B יורשת מ A. לכל אחת – הרחבה שונה לפונקציה foo.
  4. הכלל בקוטלין הוא שה resolving ל extension function הוא סטטי: ממשק האובייקט יקבע איזה פונקציית הרחבה להפעיל בפועל – ולא טיפוס האובייקט בפועל.
    1. הנה דוגמה: הפונקציה ()printFoo מצפה ל A (ממשק), אך מקבלת מופע של B (טיפוס האובייקט בפועל). מכיוון שה resolution הוא סטטי – בודקים אם ל A יש פונקציית הרחבה, ומכיוון שיש – מפעילים אותה.
  5. כאשר אין הרחבה לטיפוס / ממשק נתון – מחפשים בעץ ההיררכיה את המחלקה הראשונה שיש לה הרחבה מתאימה.

ניתן להגדיר extension functions גם על nullable types – מה שמאפשר להפעיל את הפונקציה גם כאשר הערך של האובייקט הוא null:

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

Extensions, extensions, extensions

Extension Properties

ניתן להרחיב לא רק פונקציות של אובייקט, אלא גם תכונות של אובייקט.
עבור השמירה על ה encapsulation, קיימת מגבלה שלתכונה הזו לא יהיה  backing field:

כלומר: לא ניתן להוסיף initializer ולאתחל את הערך של extension property.
כן ניתן להגדיר את התכונה כ var, ולהוסיף לה setter.

Extension Functions הן כלי חשוב, אבל הן גם מועדות לשימוש-יתר.

מה יותר נחמד להוסיף עוד ועוד הרחבות לאובייקטים קיימים, לייצר "DSLs", ולהשתמש ב"יכולות המתקדמות" של השפה? אבל:

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

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

fun String.toJson()

מול

fun UtilsClass.toJson(string)
  • שאלה טובה היא: האם סביר שבעתיד יוסיפו את פונקציית ההרחבה שלי למימוש הסטנדרטי? האם ההרחבה שלי באמת כללית ומשרתת יפה מופעים שונים מהמחלקה?
    • אם התשובה היא "לא" – כנראה שיש פה מימוש ספציפי שלכם, ולא כדאי להשתמש בפונקציית הרחבה.
    • דוגמה חיובית היא למשל ההרחבה kotlin-jackson. לא דימיוני ש Jackson ירחיבו את ספריית הבסיס לתמוך כמו שצריך באובייקטים של קוטלין, וההרחבה הזו היא שמימושית לכולם.
  • כאשר אנו רוצים להרחיב אובייקטים בג'אווה / ספריות של ג'אווה לתמוך יותר טוב בשפת קוטלין (למשל: nullable types, הגדרת אופרטורים) – יותר הגיוני להשתמש בפונקציות הרחבה.
  • כאשר מחלקה היא גדולה ומורכבת – נסו ליצור אבסטרקציה חדשה, ולא להעמיס עוד הרחבות על המחלקה.
  • אם המחלקה היא שלכם – נסו להוסיף לה members, ולא הרחבות.

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

Lambda Extension Functions (או: Lambda with receivers)

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

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

את היכולת הזו משלימים עם היכולת להגדיר receivers לפונקציות למבדה – מה שהופך אותן בפועל ל extension (lambda) functions:

  1. נפתח את הדוגמה בהגדרת מחלקה בשם RoutingMap. בשלב הזה – זהו בעצם alias. דמיינו שיש יותר.
    1. נ.ב: אם באמת הכוונה שלנו היא רק להציע alias – הצורה האידיומטית (idiomatic) לעשות זאת בקולטין היא בעזרת המילה השמורה typealias:
    2. מה שנחמד ב typealias הוא שאפשר להשתמש בו מול מחלקות שהן final – שלא ניתן לרשת אותן, כמו String או Int.
  2. אני יכול להגדיר DSL, בעזרת הגדרת פונקציה שמקבלת כפרמטר פונקציית למבדה.
  3. הנה ההפעלה: הפונקציה route:
    1. יוצרת RoutingMap.
    2. מפעילה את הלמבדה ("צריכת התוכן" ב DSL)
    3. רושמת את ה routes במקומות השונים, או במקרה שלנו – מדפיסה את ה map.
  4. בעזרת פונקציית למבדה עם receiver – הקוד יכול להראות נקי יותר.
  5. הנה ההפעלה של הפונקציה (route(2:
    1. יוצרת RoutingMap.
    2. שימו לב להבדל: בזכות ה receiver – בעצם הלמבדה מרחיבה (משמשת כ extension function) את מחלקת ה RouteMap – ולכן אנו פשוט מפעילים את הפונקציה של המחלקה.
      כמובן שההרחבה הזו טובה רק ל scope של פונקציית ה route2 – ולא מעבר לה.
    3. רושמת routes / מדפיסה.
  6. התחביר של פונקציית הלמבדה עשוי להראות מעט מוזר –  בואו נפרש אותו:
    1. התחביר הוא בעצם : .() ->
    2. לדוגמה: String.(Int, Int) -> Int
      אנו מרחיבים את המחלקה String, בעזרת פונקציית למבדה שמקבל שני פרמטרים מסוג Int ומחזירה Int.
    3. בד"כ / בפונקציות למבדה "רגילות" – פשוט אין Receiver.
    4. בדוגמה שלנו – פשוט אין לפונקציה פרמטרים.

רוצים להקדם קצת יותר ב DSL? – הנה שתי תוספות שניתן לעשות:

  1. אפשר להחליף את השם set לשם נוח יותר: למשל: addRoute – ע"י הוספת פונקציה ל RoutingMap שרק עושה delegation ל set.
  2. אפשר אפילו להרחיב את הביטוי כולו כדי שהיה "בשליטתנו". בדוגמה הבאה (הזריזה) החלפתי את ה Map ל ArrayList של Pairs משלי כדי להגדיר את respondWith עליו.
    1. יכולתי להרחיב את Any – אבל זו פרקטיקה רעה למדי. דמיינו אלו תקלות יכולות לקרוא שמישהו מוצא ב autocomplete פונקציה בשם שנראה לו הגיוני.
    2. מצד שני, אני לא בטוח שהפתרון של MyPairs הוא מוצלח גם כן: ויתרתי על יכולות ה hash של ה HashMap. כאמור: זו לא המלצת מימוש – רק משחק ביכולות של שפת קוטלין.

הרחבה אחרונה: אני רוצה לבנות מבנה מקונן, בו יש type safety ל response.

בדוגמה הבאה שיניתי כמה שמות – לצורך קריאות הקוד.
MyPair הוא בעצם RouteEntry. עברנו את השלב להסביר שהוא מחזיק בעצם זוג משתנים.
RoutingRegistry הוא לא באמת Map – אז לא נכון להצהיר עליו ככזה.
את HttpStatus – המחלקה המקוננת המתארת את ה response, יצרתי כ data class. ב RouteEntry דרסתי את toString בעצמי.

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

Operator Overloading

Operator Overloading היא היכולת להגדיר התנהגויות של Operators (+, -, ==, וכו') למחלקות שלנו. למשל:

  1. בחרתי במחלקה דיי פשוטה: data class.
  2. האופרטור + הוא בעצם המימוש של הפונקציה plus, והאופרטור * – של הפונקציה times.
  3. ההפעלה של a+b היא דיי ברורה, אבל מי החליט מהי מחרוזת כפול מחרוזת?
    1. פה מתגלה הסיכון הגדול של שימוש ב Operator overloading: בניית סמנטיקה לא-צפויה.
    2. בכלל בהגדרה של DSL, לסמנטיקה ולמינוח המדויק – יש חשיבות רבה. בדריסת אופרטורים – על אחת כמה וכמה: חשוב מאוד לבחור סימן שיתאר התנהגות צפויה, גם ליישות =! self, גם למי שלא קורא את מימוש האופרטור.

בניגוד לסקאלה, לא ניתן להגדיר בקוטלין כל סימן אפשרי כאופרטור.
אולי אפשר להבין ש אבא + אבא = סבא, אבל מה זה לעזאזל אבא £ אבא, או אבא ^_^ אבא? (כל אלה אפשריים בסקאלה).

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

Destructing Operator

אופרטור מיוחד אחד של קוטלין הוא ה destructing operator ("פירוק המבנה"?).

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

נתחיל במילה הקצת מוזרה to המשתמשים בה להגדרה inline של Maps:

הא! זה "כולה" infix function, שעושה הרחבה (extension) לכל טיפוס (?Any) שייקרא A, עם פרמטר יחיד מכל סוג (?Any) שיקרא B – ומחזירה Pair של שניהם.
השורה:

val x = null to null

היא אם כן, ביטוי לגיטימי השומר במשתנה x אובייקט Pair עם שני nulls.
פשוט!

מה משמעות התחביר (char, index) אומר?
זו פעולת destructing declaration המציבה ערכים בשורה של משתנים (v1, v2, …​, vn) באופן הבא:

v1 = .component1()
v2 = .component2()
vn = .componentn()

לדוגמה:

מאיפה מגיעות הפונקציות ()component1 ו ()component2?

זהו פשוט operator overloading שנעשה במחלקה Pair, מה שנקרא deconstructing operator.

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

Invoke

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

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

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

סיכום

צללנו ליכולות ה DSL של קוטלין. מקווה שיצאתם מזה בשלום 🙂

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

>> בחור אחד שאל אם יש בקוטלין משהו דומה ל (rand(0..n של רובי, ביטוי המחזיר מספר רנדומלי מהטווח.
>> פתרון יפה שהציעו לו התבסס על extension function למחלקת ה CustomRange של קוטלין:

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

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

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

קוטלין (Kotlin) למפתחי ג'אווה ותיקים – חלק ד': עוד על מחלקות, אובייקטים, ועוד…

פוסט זה הוא חלק מסדרה:

בואו נתחיל!

היררכיית הטיפוסים של קוטלין

בקוטלין, האב הקדמון של כל האובייקטים הוא Any – המקבילה של Object בשפת ג'אווה.

כך נראה אובייקט ה Any (השמטתי את ה comments):

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

המחלקה Any מכילה גם כמה extension functions (החשובות שבהן: apply, let, run, also) – המוגדרות בקבצים אחרים. נדון בקונספט ה extension function ובמתודות הללו ספציפית – בהמשך הסדרה.

האם Any הוא באמת האב הקדמון של כל המחלקות בקוטלין?
לקוטלין יש את מערכת ה Nullable, שבעצם אומרת ש:

Any? = Any || null

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

ניתן לשרטט סכמה של היררכיית הטיפוסים בקוטלין בערך כך:

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

הזכרנו כבר שבקוטלין אין פרימיטיביים, ולכן כל ה wrappers (כמו Int) – יורשים בעצם מ Any.

Nothing הוא מקרה קצת מיוחד – אותו גם הזכרנו: Nothing מתאר ביטוי שלעולם לא יסיים evaluation. למחלקים Nothing לעולם גם לא יהיו instances.

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

השימוש ב Nothing עוזר לסמן לקופיילר מקרים ש"הקומפיילר לא צריך לדאוג מהם". למשל, המימוש הבא של EmptyList (זהו ה pattern של NullObject – למי שמזהה):

מקור

הורשה בקוטלין

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

  1. עלי להצהיר על המחלקה open, אחרת תהיה לי שגיאת קומפליציה שתצביע על המחלקה SpecialPerson שיורשת אותה. מחלקות הן final בקוטלין, כברירת מחדל. זו גישה הגנתית המספקת פחות הפתעות מהורשה לא צפויה.
  2. כנ"ל לגבי פונקציה: כל הפונקציות הן final כברירת מחדל – ויש "לאפשר" לרשת אותן במפורש.
  3. חובה להגדיר override בפונקציה שדורסת מימוש של מחלקת-האב, בניגוד לאנוטציה Override@ בג'אווה – שהיא רשות.
  4. התחביר של הורשה הוא : – כמו ב #C. סוגריים נדרשים כדי לדעת אם להפעיל את בנאי-ברירת-המחדל של מחלקת האב – או בנאי אחר.

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

  1. הגדרתי את המתודה a כ final במחלקה SpecialPerson. לולא ההגדרה – הגדרת ה "open" מהמחלקה Person – הייתה ממשיכה הלאה במורד ההיררכיה.
  2. …ולכן – יש לי שגיאת קומפליציה בבואי לדרוס אותה.

מחלקות חתומות (Sealed Classes)

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

  1. אני מגדיר את המחלקה InsuranceResult כ sealed (במקום Open). זה יתיר רק למחלקות שהוגדרו באותו הקובץ (private scope) – לרשת ממנה.
  2. הנה יצרתי 2 מחלקות פשוטות. העימוד בא להדגיש את הקשר.
  3. שימוש נפוץ הוא בכדי להחזיר סט סגור של ערכים. למשל: פעולה של אישור ביטוח שיכולה להסתיים בהצלחה (תשובה מסוג אחד – Approved) או בכישלון (תשובה מסוג אחר – Denied).
    1. המחלקות Denied, Approved, ו InsuranceResult – הן public, וניתן להשתמש בהן מכל מקום.
  4. הנה הקוד שמקבל את התשובה, גם הוא – פשוט ואלגנטי.

מחלקות מופשטות (abstract), וממשקים (interfaces)

בקוטלין יש מחלקות מופשטות (abstract):

  1. עצם ההגדרה של מחלקה כ abstract אומרת ש:
    1. המחלקה יכולה להכיל members מופשטים (abstract).
    2. לא ניתן ליצור instances ממנה.
  2. הנה פונקציה מופשטת. פונקציה מופשטת היא "פתוחה" (open) בהגדרה.
  3. אפשר גם להגדיר פונקציות קונקרטיות – עם מימוש. הם יהיו "סגורות" כברירת מחדל.
  4. כשאני דורס פונקציה מופשטת, עדיין עלי להצהיר על דריסה בעזרת override.
  5. יש לי שגיאת קומפילציה, מכיוון וניסיתי לרשת פונקציה ממחלקה מופשטת, בלי המילה override. מוסיפים override – ויש שגיאה נוספת. מה הפעם?
    1. כברירת מחדל פונקציות (ומחלקות) הן final. עלי לשנות את ה פונקציה collect ב InventoryItem להיות open – ורק אז הכל יסתדר.

ממשקים בקוטלין הם דיי דומים לג'אווה – עם כמה הבדלים קטנים.

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

  1. הנה הגדרה של ממשק פשוט: יש בו פונקציה אחת מופשטת (ללא מימוש), ואחת קונקרטית – עם מימוש.
  2. בממשקים של קוטלין ניתן להגדיר תכונות, אבל ללא state (חייבים להגדיר getter, ולא יהיה  backing field מאחורי התכונה).
    1. שימוש אפשרי לתכונות על ממשק – היא כחלופה לקבועים על הממשק – שאין בקוטלין.
  3. הנה אני יוצר אובייקט שיורש מ Person ומממש את הממשקים Happy ו Shiny. סדר ההגדרה של הממשקים / מחלקת אב – לא משנה.
  4. אני מחויב לדרוס את המתודות המופשטות של הממשקים, כמובן – ולספק להן מימוש קונקרטי.
  5. מה קורה כאשר לכמה ממשקים שירשתי מהם, יש פונקציה קונקרטית עם חתימה זהה (בדוגמה שלנו: reflect)? כיצד לא נגררים לבעיות של הורשה מרובה?
    הפתרון של קוטלין אלגנטי: הקומפיילר מחייב אותי לממש מחדש את הפונקציה.

    1. שימו לב שאם החתימה היא שונה (פרמטרים שונים) – אז אין בעיה, ואני לא אחויב לממש מחדש.
  6. super הוא מילה שמורה המתייחסת ל super type שלי.
    שנייה! יש פה בעיה: הקומפיילר לא יודע איזה מימוש של reflect אני רוצה לקבל: של Shiny או של Happy? זו שגיאת קומפליציה שתתרחש רק במידה ויש קונפליקט / דו-משמעות.

    1. הפתרון הוא לבאר לקומפיילר מה אני רוצה, בעזרת התחביר הבא: (super<Happy>.reflect(s 
      , קרי לספק למילה השמורה super את הטיפוס הספציפי שאני בוחר להתייחס אליו. כשאני משתמש בתחביר הזה – הקומפליציה תקינה, וקריאה ל ()reflect של S.H.Person – תפעיל את המימוש הקונקרטי של Happy.
השאלה המתבקשת עכשיו, היא: מה ההבדל בין מחלקה מופשטת בקוטלין, לממשקים?
אם אפשר להגדיר פונקציות קונקרטיות על ממשק – מה בעצם הצורך במחלקה מופשטת? האם מחלקות מופשטות הן לא "מיותרות"?אפשר לאפיין ארבעה הבדלים בין מחלקות מופשטות לממשקים:

  1. במחלקה מופשטת כל ה members הם final כברירת מחדל (כמו כל מחלקה) – בממשק הם open, ולא ניתן להגדיר אותם כ final.
  2. בממשק לא ניתן להגדיר members כ internal. ורק פונקציות קונקרטיות ניתן להגדיר כ private. בקטנה.
  3. מחלקה יכולה לרשת ממשקים רבים – אך רק מחלקה מופשטת אחת – זה כנראה ההבדל הכי חשוב.
  4. זה מעט מורכב: ממשק לא יכול להחזיק state, אחרת עלולים להיווצר קונפליקטים במימוש-מרובה.
    לכן, השפה מציבה כמה מגבלות על תכונות (properties) שהוגדרו בממשקים:
  1. בממשק אני יכול להגדיר val או var:
    1. val – ניתן להגדיר לו getter בלבד, או להשאיר את התכונה מופשטת (abstract).
    2. var – התכונה תמיד תהיה מופשטת. ניסיון להוסיף getter יגרור לשגיאת קומפילציה.
  2. מכיוון שהתכונות של הממשק Koo הן מופשטות – המחלקה המממשת חייבת לממש את התכונות הללו: x ו y (להלן override), ואז עלי לנקוט באחת מהגישות הבאות:
      1. אני יכול להציב בתכונות ערך התחלתי (מה שאי אפשר לעשות בממשק)
      2. אני יכול לממש getter/setter בעצמי.
        1. הערה: val הוא readonly – ולכן אין צורך ב setter.
      3. אני יכול לדרוס את התכונות ב primary constructor. הערכים שהוגדרו שם "יחביאו" את אלו שהוגדרו על הממשק.
כלומר: בסופו של דבר היתרון הגדול של הממשקים הוא ביכולת של מחלקה לממש מספר ממשקים, והשפה תגן עלי בפני קונפליקטים אפשריים. היתרון של מחלקות מופשטות הוא כנראה ההרגל (של מפתחי ג'אווה ותיקים) – והיכולת להגדיר state.

מחלקות מקוננות (Nested Classes)

בקוטלין יש 2 סוגים של מחלקות מקוננות:

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

    1. יוצרים אותה בנפרד.
    2. היא לא יכולה לגשת לתכונות / פונקציות של מחלקת האב.
  2. בצורה השנייה, משתמשים במילה השמורה inner, בכדי לחבר את המחלקות ברמת המופעים (instances)
    1. לא ניתן ליצור מחלקה פנימית ללא החיצונית. ביצירת מופע של המחלקה החיצונית – נוצר גם מופע, מקושר, של המחלקה הפנימית.
    2. אם יש התנגשות בשמות, ניתן להשתמש ב qualified this" expression" – להגיע למחלקת האב. דומה מאוד ל OuterClass.this בג'אווה.
      1. ניתן גם להשתמש ב qualified super, בתחביר ()super@Father.foo – בכדי לגשת לפונקציה של המחלקה ממנה האובייקט החיצוני יורש.

מה קרה ל static ובן-אל?

בקוטלין ביטלו את המילה השמורה static. אין קונספט של מתודות סטטיות במחלקה.
מצד שני, יש קונספט של singleton מובנה בשפה – בעזרת המילה השמורה object:

  1. בעזרת המילה השמורה object, אנחנו בעצם מגדירים מחלקה – ומייד יוצרים לה מופע.
    1. אובייקט הוא מופע יחיד (singleton). השימוש הנפוץ בו הוא ליצירת Factory או אובייקט שמכיל איברים סטטיים – המשותפים לכולם.
    2. ניתן להגדיר על האובייקט תכונות, ופונקציות – אבל לא בנאים. הרי: לא יוצרים אותו. המופע נוצר בעצם ההגדרה.
    3. ניתן לרשת ממחלקה אחרת / לממש ממשקים.
  2. אם היינו רוצים להגדיר קבוע בג'אווה היינו משתמשים ב static final. בקוטלין קיימת במילה השמורה const שמצביעה לקומפיילר שמדובר בערך שידוע בזמן קומפילציה. השימוש ב const היא הצהרה שזהו ערך סטטי שלא הולך להשתנות. המשמעות:
    1. אי אפשר להציב תשובה מפונקציה, קרי ()const val x = foo – זו שגיאת קומפילציה.
    2. אי אפשר להוסיף getter – הרי getter יוכל "לשנות את הערך, ע"י החזרה של ערך אחר", ולעולם בעתיד לא יהיה ניתן להוסיף getter. זו אולי התכונה בעלת הסמנטיקה העמוקה ביותר.
    3. אפשר לאתחל את התכונה רק ערכים פרימיטיביים (מספרים, מחרוזות), ולא אובייקטים – אחרת, ייתכן אי אפשר להבטיח שכלום לא ישתנה.
    4. בפועל: מאחורי const לא יהיה backing field.
סיבה מרכזית שבגללה השתמשנו בג'אווה במילה השמורה static היא לצמצם מספר מופעים: אם יש לי קבוע מסוים במחלקה, שיש לה אלפי מופעים – לא ארצה לשכפל את הקבוע אלפי פעמים בזיכרון.
בקוטלין, ניתן להגדיר קבועים או פונקציות במרחב הגלובאלי, אולי בתוך אובייקט – בכדי לנהל את ה scope בצורה מסודרת יותר.
מה עושים כאשר אנו רוצים את ההתנהגות של ה static methods בג'אווה? לקרוא לפונקציה בלי לייצר מופע של האובייקט? לכך יש כלי בשם Companion Object:
  1. לצורך הדוגמה יצרתי אובייקט מקונן בתוך המחלקה.
    1. זו צורת עבודה לגיטימית. תחביר-ההפעלה הוא קצת יותר מסורבל, כי יש לציין את שם האובייקט.
  2. כאשר אני מגדיר companion object, אני יכול לפנות לפונקציות שלו, ישירות תחת ה namespace של המחלקה. ל companion object:
    1. אין צורך להגדיר לאובייקט שם.
    2. ניתן להגדיר רק companion object יחיד בכל מחלקה.
    3. ה companion object ייווצר בזמן שטוענים את קוד המחלקה – מה שמקביל את הפונקציות שלו ל static methods בג'אווה.
  3. ה annotation של JvmStatic@ היא רשות, והיא תגרום ל bytecode שנוצר לגרום לג'אווה להתנהג עם ה companion object בצורה דומה לאופן הטיפול בקוטלין.
    1. ללא ה annotation יש לקרוא בג'אווה: ()Log.Companion.createLogger
    2. עם ה annotation יש לקרוא בג'אווה: ()Log.createLogger
  4. אם המחלקה עושה shadowing למשתנה / פונקציה של ה companion object, אני יכול בצורה מפורשת לגשת ל member של ה companion object בעזרת השם Companion.

שונות

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

late init

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

  1. הפתרון ה"פשוט" הוא להציב ערך null בשדה – עד האתחול ה"אמיתי".
    1. בקוטלין זה דיי מעצבן, כי אז עלי להפוך את כל הטיפול באותו משתנה ל nullable – התעסקות מיותרת.
  2. הפתרון: להוסיף modifier בשם lateinit, המורה לקומפיילר: "אחי, עלי! המשתנה הזה יאותחל לפני שיקראו לו". השימוש ב lateinit מאפשר קוד נקי יותר, ללא טיפול ב nullability.
    1. מצד שני: אם טעינו (והטענו את הקומפיילר) – תיזרק שגיאת …UninitilizedProperty.
      שם השגיאה, מכוון למהות הבעיה, יותר טוב מאשר לקבל סתם Kotlin)NullPointerException).
  3. כאשר מגדירים lateinit, השדה של התכונה (במקרה שלנו: repository2) ייחשף לקוד הג'אווה. זוהי החלטה תכנונית של מקסום התאימות לג'אווה.
    1. אפשר למנוע את חשיפת השדה ע"י ה annotation האופציונלי: field:JvmSynthetic@

ה runtime של קוטלין קובע ערך null בתכונות lateinit – לסמן לעצמו שהן עדיין לא אותחלו. לכן – לא ניתן להגדיר lateinit לתכונות מטיפוס שהוא Nullable.

גישה לאובייקט ה Class

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

ספריות שונות משתמשות ביכולת הזו, למשל: JUnit:

@RunWith(RobolectricTestRunner.class)

בקוטלין, ניתן לגשת לאובייקט ה reflection המתאר את המחלקה בעזרת הקריאה object::class – זהו אובייקט של ספריית ה runtime של קוטלין מסוג KClass (על הכוכבית – בהמשך הסדרה).
הוא מכיל מתודות ותכונות reflection של קוטלין, למשל companionObjectInstance – המחזירה מצביע ל companion object, אם קיים.

רוצים לקבל את אובייקט הג'אווה המתאר את המחלקה שלנו (אובייקט ה Class, שהוא בעל מבנה שונה מאובייקט ה KClass)? – קראו ב object::class.java.

ברוב הפעמים דווקא נדרש לאובייקט-המטא של קוטלין. למשל:

@RunWith(RobolectricTestRunner::class)

זה תלוי מה הספריה עושה איתו. זה עניין של ניסוי וטעייה.

סיכום

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

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

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

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

פוסט זה הוא חלק מסדרה:

הגדרת מחלקה פשוטה

בואו נתחיל עם המחלקה הפשוטה ביותר האפשרית בקטולין:

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

מה השימוש במחלקה ריקה?

לא הרבה.
אפשר לייצר ככה custom exception או Marker interface או Marker Annotation (בתחביר מקוצר הרבה מג'אווה).

טוב, עכשיו נתחיל יותר ברצינות:

  1. בקוטלין לא ניתן להגדיר שדות (fields) למחלקה. במקום זאת – מגדירים תכונות (properties). מה זה אומר?
    1. השורה שלנו תגרום לקומפיילר לייצר גם שדה, וגם accessor methods לשדה הזה.
      1. כאשר כותבים  בקוטלין – הגישה לתכונה (property) תעשה בעזרת person.age – כאילו זהו field שהוא פשוט public.
      2. כאשר כותבים בג'אווה – הגישה לתכונה של מחלקה הכתובה בקוטלין תעשה בעזרת ()person.getAge  ו ()person.setAge – כמקובל.
    2. מאחורי כל תכונה (property) באמת קיים שדה (field). לעתים – לא יהיה בשדה הזה בכלל שימוש. נראה דוגמה בהמשך.
    3. הסיבה מאחורי הוספת properties לשפה, היא כנראה Item 14 בספר "Effective Java" – המזהיר בפני חשיפת שדות שאז לא יהיה ניתן לשנות, לבקר, ולהגביל את הגישה אליהם. "אחרי שנחשפו – זהו!… התלות קיימת וזו יכולה להיות עבודה קשה להסיר אותה…"
    4. בקוטלין נתנו לנו את האפשרות להוסיף custom getter/setter בכל יום שנצטרך – אבל בלי הסרבול של לכתוב getters / setters בעצמנו, בכל פעם, כי "אולי בעתיד יהיה צורך".
      להיות עם – ולהרגיש בלי.
  2. בקוטלין יש primary constructor, וייתכן שיהיו secondary constructors.
    ה primary constructor מוגדר באותה שורה עם הגדרת המחלקה.

    1. מגדירים אותו בעזרת המילה השמורה constructor.
    2. מגדירים אלו פרמטרים הוא יקבל.
    3. אם לא מגדירים primary constructor אזי נוצר default primary constructor – ללא פרמטרים.
  3. ה init block נקרא בכל יצירה של instance של המחלקה, והוא בעצם משמש כגוף הפונקציה של הבנאי הראשי (primary constructor) – אם צריך כזה.
    1. כל התכונות של המחלקה צריכות להיות מאותחלות. אם נסיר את השורה "age = 0" – נקבל שגיאת קומפילציה.
      הקומפיילר דורש שאנו נאתחל את התכונות באותה השורה (מה שנקרא initializer, כמו ב name) – או שנאתחל אותם ב init block / בבנאי הראשי.
  4. הנה הגדרה של בנאי משני למחלקה.
    1. ההגדרה שלו נעשית בעזרת המילה השמורה constructor.
    2. מגדירים את הפרמטרים של הבנאי המשני.
    3. חובה להפעיל את הבנאי הראשי:
      1. או ישירות – ע"י קריאה עם הפרמטרים המתאימים.
      2. או ע"י קריאה לבנאי משני אחר – שהוא יפעיל את הבנאי הראשי.
      3. כאשר קיים default primary constructor – אין צורך ב ()this :.
  5. הנה יצירת מופע של המחלקה בעזרת שימוש בבנאי הראשי.
  6. הנה יצירת מופע של המחלקה – בעזרת שימוש בבנאי המשני.
  7. הנה קריאה לתכונה – היא באמת נראית כמו קריאה לשדה בג'אווה – מה שהופך את הקוד למעט יותר "נקי בעין".
יש לא מעט מידע בפסקה הזו – אולי תרצו לעבור עליה שוב.
אתם בוודאי רואים שיש פה השפעה לא מעטה #C וגם קצת מסקאלה.
האמת, שיש דרך פשוטה יותר לכתוב את המחלקה Person. הנה היא לפניכם:
  1. הצלחנו לפשט את המחלקה דרמטית, בעזרת הגדרה "נבונה" יותר של הבנאי הראשי.
    1. המילה constructor בשורת המחלקה, לתיאור הבנאי הראשי – היא אופציונלית, ואפשר לוותר עליה.
    2. אם מגדירים val או var על הפרמטרים של הבנאי הראשי – הרי זה שקול להגדרת תכונות בגוף המחלקה. הארגומנטים שנשלחו לבנאי – יאתחלו את התכונות הללו. קצר ונוח.
      יכולת זו שמורה לבנאי הראשי בלבד.
    3. השימוש ב default value בחתימה של הבנאי הראשי – ייתרה את הצורך בבנאי משני.
      1. במקרים לא מעטים, השימוש ב default values – יכול לחסוך שימוש ב Builder Pattern, ולחסוך לא-מעט קוד.
  2. הנה ההרצה: אין שום שינוי, כי לא היה שום שינוי. הקוד שקול לחלוטין.

אמנם כך עדיף לכתוב את המחלקה, אך היה חשוב לי לעבור בדרך הארוכה – עבור הלמידה.

עוד קצת על תכונות (Properties)

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

  1. הגדרנו למחלקה תכונה בשם age – ודרסנו את ה getter. ממש דומה לתחביר של #C.
    1. כעקרון, כאשר דורסים רק שלפן (accessor) אחד, במקרה שלנו: getter, השלפן השני – עדיין קיים במימוש ברירת המחדל שלו. כלומר: ל age עדיין יש default setter.
    2. ספציפית במקרה שלנו, מכיוון ש age הוגדר כ val (כלומר "immutable") – לא ייווצר setter.
  2. רק לצורך ההדגמה יצרתי תכונה מאוד דומה, אותה הגדרנו כ var. במקרה כזה, אנחנו מחויבים לאתחל את השדה של התכונה – גם אם לא יעשה בו שימוש לעולם (במקרה הזה: עם הערך אפס).
  3. כאשר ה getter שלי הוא פשוט – אני יכול להשתמש בתחביר shorthand, בעזרת הסימן =.
    1. יכולתי להשתמש בתחביר shorthand גם עבור age.
  4. אם אני לא רוצה שיגשו ל setter של התכונה – אני פשוט אגדיר אותה כ private.
    התחביר הזה אומר לקומפיילר: צור default setter – אך עשה אותו private.
  5. הנה – סתם בשביל ההדגמה: אני יכול להשתמש ב setter של התכונה בתוך המחלקה.
  6. אגדיר תכונה נוספת בשם nickname – אין לי בעיה לאתחל אותה בערך שנגזר מתכונה אחרת – name.
  7. כאן מימשתי setter לדוגמה. ה getter ה default-י עדיין קיים וזמין.
  8. מה קורה כאשר אנחנו רוצים, ב customer accessor שלנו לגשת לשדה שמאחורי התכונה?
    1. עושים את זה בעזרת המילה field.
    2. field היא מה שנקרא soft keyword, כלומר: keyword שקיים רק ב context מסוים. ממש כמו it – שראינו בפוסט הקודם.
  9. בניסיון ההרצה, שלושת השורות הללו יגרמו ל compilation error.
    1. את לשלושת התכונות הללו: age, name, ו ageNextYear – לא ניתן לקבוע ערך מחוץ למחלקה.
הנה תוצאת ההרצה:

Visibility Modifiers

בקוטלין, visibility ברירת המחדל היא תמיד public. יש שוני קטן בהגדרות כאשר ה visibility modifier מוגדר:

  • בתוך מחלקה.
  • ב top level, כלומר: עבור הגדרה של מחלקות, משתנים, או פונקציות שאינן שייכות למחלקה.

הנה ההגדרות:

  • private 
    • יתנהג כמו ג'אווה בתוך מחלקה
    • יגביל גישה לאותו קובץ בלבד, אם השתמשו בו ב top-level.
  • protected
    • יתנהג כמו ג'אווה בתוך מחלקה
    • לא ניתן להשתמש בו ב top-level.
  • internal
    • היא נראות חדשה לקוטלין, שמגדירה אפשרות לגשת לכל מי שנמצא באותו המודול.
      • כיצד מוגדר מודול? ע"י תהליך הקומפליציה: קבצים שקומפלו ביחד.
        זה יכול להיות מודול של Maven, של Gradle, או מודול ב IntelliJ – למשל.
    • היא מתנהגת אותו הדבר בתוך המחלקה, וב top-level

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

  1. הגדרתי את המשתנה כ private – לא יוכלו לגשת אליו מקובץ אחר.
  2. הגדרתי מחלקה שהיא internal – זמינה רק בתוך ה module.
    1. כל המחלקות בקוטלין הן final – לא ניתן לרשת מהן, אלא אם מרשים זאת במפורש בעזרת השימוש ב open. אהבתי!
  3. מה עושים כאשר רוצים להגדיר "שדה פנימי" בקוטלין? רק לשימוש המחלקה?
    – מגדירים תכונה שהיא private, ומשתמשים בה.
  4. פה נראה שאולי עשיתי איזה טריק: הגדרתי getter ו setter על התוכנה – כך שאי אפשר לגשת ל accessors שלה. האם הפכתי אותה ל private?
    1. בפועל: אין תחביר כזה:
    2. הקומפיילר מתעלם מ get ומגדיר רק את ה default setter כ private.
    3. תראו למטה – אמנם הקוד מתקמפל, אבל אני בהחלט יכול לגשת ל someHiddenField ממחלקה אחרת. אופס!
  5. הנה דוגמה תקינה ל default setter שהוא private. אפשר כמובן גם להגדיר custom setter באותו האופן.
  6. ניסיתי להגדיר internal getter – אך קיבלתי שגיאת קומפילציה: ה getter של תכונות חייב להיות באותה נראות כמו התכונה עצמה. חשבו אילו בעיות יכלו להיות אם לא…
  7. באופן הבא אני יכול להגדיר נראות (private) על primary constructor.
    כאשר אני משתמש ב visibility modifier על הבנאי – אני חייב להשתמש במילה constructor.
  8. על השורה הזו אני מקבל warning בקומפליציה: ה class לא הוגדר כ open – אז לא ניתן לרשת ממנו. אין טעם או משמעות להגדיר נראות של protected. צודק הקומפיילר.
נמשיך הלאה, ל"סוגים מיוחדים" של מחלקות בקוטלין.

Data Class

אחד הבזבוזים הגדולים של boilerplate code בג'אווה הוא ביצירה של data classes – מחלקות שכל תפקידן להחזיק כמות מסוימת של נתונים.

אני תמיד הייתי משתמש ב public fields, אבל יש כאלו שהקפידו על getters ו setters, וגם hashCode ו equals וכו'… הרבה קוד – עבור משהו מאוד סטנדרטי.

בקוטלין אפשר להגדיר data class, מחלקה שמספקת לנו:

  • תיאור כוונות ברור: המילה data מצביעה בבירור על הכוונה מאחורי המחלקה.
  • מימוש ל ()equals(), hashCode(), clone, ומימוש default-י וסביר לחלוטין של ()toString.
    בערך כל מה שמחלקה כזו צריכה.
  1. כך מגדירים data class, שהיא מחלקה לכל דבר. את התכונות של ה dataclass הגדרתי בתוך הבנאי הראשי – כמו שהראנו קודם. זה הכי נוח.
  2. מכיוון שזו מחלקה לכל דבר, אני יכול להגדיר לה member function. כמובן שבד"כ לא יהיו פונקציות ל data class.
  3. אמנם קיבלתי מימוש סטנדרטי של כמה מתודות, אבל אם צריך – אני יכול לדרוס אותן.
    בקוטלין override היא לא optional annotation, אלא mandatory keyword. טוב מאוד!
  4. הנה אני עושה השוואה בין אובייקטים של ה data class שלי. הם לא שווים, כי המימוש שמסופק ל equals – משווה את כל התכונות של המחלקה.
  5. לאחר שעשיתי השמה (שימוש ב ()clone) – האובייקטים שווים. זו אכן ההתנהגות הצפויה מ data class.

שני Data Classes שימושיים שמסופקים כחלק מהשפה הם Pair ו Triple, המאפשרים לנו להעביר זוגות או שלשות של פרמטרים.

אלו באמת רק Data Classes. למשל, הנה המימוש של Triple:

Enum Class

בדומה לג'אווה, לקוטלין יש enum… class. בואו נראה כיצד הוא נראה:

  1. הנה דוגמה ל enum פשוט. כל איבר הוא בעצם אובייקט – מופע של המחלקה.
    1. זהו אחד המקרים המעטים בהם צריך בקוטלין לכתוב יותר קוד מאשר בג'אווה: "enum class" במקום "enum"  😏
    2. מכיוון שהאיברים הם אובייקטים, אני יכול להרחיב אותם, למשל: לדרוס פונקציה של המחלקה (או להוסיף פונקציה חדשה). הפונקציות הללו שייכות לאובייקט, ולא למחלקה!
  2. אני יכול הוסיף גם פונקציה למחלקה, שתהיה זמינה לכל אחד מהאובייקטים ב enum.
  3. יש מקרה דיי נדיר בו עלי להשתמש בנקודה-פסיק בקוטלין:
    1. אם הוספתי ל enum class פונקציות, הקומפיילר יבקש עזרה לדעת היכן נגמרה רשימת האיברים, והיכן מתחילה המחלקה. ההפרדה מסומנת בעזרת נקודה-פסיק.
    2. שימו לב שב TriColor לא היינו צריכים להוסיף נקודה-פסיק (אבל יכולנו – זה תקין).
  4. מכיוון ש BLUE הוא אובייקט – כאן תופעל, באופן טבעי, הפונקציה ()toString.
  5. לכל enum class יש את הפונקציה ()valueOf, המחפשת איבר ע"פ השם שלו.
    1. ניתן להציץ בקוד המקור של ה Abstract Enum Class כאן.
  6. לכל אובייקט ב enum יש 2 תכונות מובנות:
    1. name – (ששווה לשם האיבר), בחוסר תלות מ ()toString.
    2. ordinal – שהוא האינדקס של האיבר.
  7. תכונה אחרונה חשובה של enum היא הפונקציה ()values – המחזירה מערך של האיברים. לצורך הפלט – המרתי את המערך לרשימה.
  8. אני יכול לשנות את המחלקה ולהוסיף לה תכונות דרך הבנאי הראשי.
    הנה ל TwoColor הוספתי תכונה בשם value – שיש לכל אחד מהאיברים שלה.
הנה הפלט:

סיכום

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

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

קוטלין (Kotlin) למפתחי ג'אווה ותיקים – חלק ב': פונקציות

פוסט זה הוא חלק מסדרה:

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

כרגיל, ננסה ללמוד עוד קצת על הדרך:

  1. הנה כמה קל להשתמש בקוד ג'אווה: אני מבצע import מתוך java.lang.Math לפונקציית החזקה (power).
    בג'אווה, בכדי לייבא מתודה הייתי צריך להדגיש: import static. בקוטלין – ויתרו על התחביר המיותר.
    שווה לציין שכל שלכל קובץ של קוטלין מתבצע מאחורי הקלעים import לשורה של חבילות: *.java.lang וכמה חבילות של קוטלין.
  2. כאשר מספר הוא עשרוני, הקומפיילר אוטומטית מניח שהוא מסוג Double. לא צריך לכתוב d בסוף המספר (אבל כן צריך לכתוב F אם אתם רוצים שיוגדר Float).
  3. כך נראית פונקציה – שוב: מאוד דומה ל Go, מאוד "מודרני".
    שימו לב לטבעיות בשימוש בפונקציה pow שהגיעה מספריה בג'אווה.
  4. הדרך להפעיל (invoke) פונקציה – היא כמו בג'אווה.
  5. זו הדרך הפשוטה להציג רק 2 ספרות עשרוניות למספר – בעזרת פונקציית format של ג'אווה.
  6. אני יכול לקרוא לפונקציה תוך כדי שאני מציין את שמות הפרמטרים כמו שהוגדרו בפונקציה (ואז סדר הארגומנטים לא משנה).
    זה אולי קריא יותר, אבל ה IntelliJ מוסיף תגיות מאוד דומות בכל מקרה (כיביתי אותן כרגע).
  7. זו הדרך לבצע formatting לאלפים + 0 ספרות לאחר הנקודה – שוב, בעזרת פונקציית format של ג'אווה.
הנה הפלט:

Nullable Types

בוודאי אתם מכירים שה Exception הנפוץ ביותר בג'אווה הוא NullPointerExcpetion.

כמו שפות מודרניות אחרות, יוצרי קוטלין החליטו להגדיר את השפה כ "null-safe by default". כעיקרון, אין nulls, אלא אם אתם מתירים אותם.

  1. זו תהיה שגיאת קומפליציה. מה פתאום לקבוע ערך null? אין מצב!
  2. אם אנחנו מגדירים ש y הוא מסוג Nullable String – אז אפשר לקבוע בו ערך null. בסדר.
  3. גם זו שגיאת קומפילציה: השתגעתם? לקרוא למתודה ()length למשהו שעשוי להיות null? אנחנו לא רוצים nullpointerException!
  4. פה הקומפיילר חכם: אם כבר בדקתי שהערך איננו null – אז יאפשרו לי לקרוא למתודות על האובייקט ה nullable.
  5. דרך אחר לעשות זאת, להשתמש באופרטור ?. במקום . – שמגן עלי בפני nullPointerException.
    אם y הוא null – אזי תוצאות כל הביטוי תהיה גם היא null.
  6. מה קורה עם מחלקה שאני מביא מג'אווה? בג'אווה – כל האובייקטים הם Nullable.
    אתם רואים שאני צריך לבנות מבנה דיי מורכב (ולא מעניין) על מנת להגן על עצמי מ NullPointerException.
    החלטה תכנונית של שפת קוטלין היא לא להגן עלי בכוח בפני אובייקטים של ג'אווה – כי כך אני רגיל. ההגנה ברמת הקומפילציה היא רק בפני אובייקטים של קוטלין שהם Nullable.
  7. בכל זאת, קוטלין מספקת לי תחביר מקוצר ובטוח לעבודה עם אובייקטים שהם Nullable. התחביר הזה תופס גם לאובייקטים של ג'אווה, וגם לאובייקטים של קוטלין שהם Nullable (ואולי יש להם פונקציה שערך ההחזרה שלה הוא Nullable).
    במקרה הזה, מכיוון ש ()getExtID מחזיר null (נניח), כל הביטוי יוערך כ null – ולא ייזרק NullPointerException.
  8. לקוטלין יש כמה כלים לעבוד עם רשימות שחלק מהאיברים בהן עשוי להיות null.
    גישה ראשונה היא השימוש בפונקציה let שתפעל רק אם הערך הוא לא null.
    אפשר להשתמש ב let גם ללא קשר לרשימת איברים.
  9. גישה שניה, היא להשתמש בפונקציה / פילטר בשם ()filterNotNull, כלומר: להשאיר רשימה הכוללת רק איברים שאינם null.
  10. אני יכול לבדוק אם משתנה הוא null ולהגיב בהתאם.
  11. או בגלל שזו פעולה שכיחה, להשתמש באופרטור ה 😕 (נקרא Elvis Operator) – בכדי להשיג בדיוק אותו הדבר בכתיבה מקוצרת.
  12. אם אני ממש מתעקש, אני לומר לקומפיילר: "עזוב אותי באמאשלך! אני אסתדר עם ה null-ים שלי בעצמי", בעזרת אופרטור ה .!!.
    1. שימו לב שאת האופרטור הזה אפשר להפעיל רק על משתנים שהם immutable (למשל: val) – אחרת, יכול תאורטית thread אחר להיכנס בין "שורות" ההפעלה שלי – ולשים בו null.
      1. אופס!, חטפתי KotlinNullPointerException.

הנה הפלט:

ערכי החזרה מיוחדים

שימו לב לחבר'ה הבאים:

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

2. ניתן להגדיר את אותה הפונקציה כ Unit בצורה מפורשת.
3. הפונקציה fail היא מסוג Unit, אבל בעצם היא לעולם לא תחזיר Unit – כי תמיד היא זורקת Exception (שימו לב שבקוטלין לא משתמשים ב new).
4. ניתן להגדיר פונקציה שלעולם לא תגיע ל return – כבעלת ערך החזרה מטיפוס Nothing.

את Nothing לא ניתן להציב במשתנה – זו בעצם הערה לקומפיילר.

5. כאן נקבל שגיאת קומפליציה מסוג Conflicting declaration – הקומפיילר לא יודע איזה טיפוס להגדיר ל data:
Int – בגלל Zoo?
או אולי Any (ה root בהיררכיית האובייקטים בקוטלין, כולל null) – בגלל ש fail הוא מטיפוס Unit?

6. במקרה הזה הקופיילר רגוע: מ ()gail לא יכול לחזור ערך (הוא מסוג Nothing), ולכן data יוגדר כטיפוס Int.

מעניין אולי לציין ש null הוא singleton מטיפוס ?Nothing.

default value ו vararg

  1. אני יכול להגדיר בפונקציה פרמטרים עם ערך ברירת מחדל – למשל: cores.
  2. אני יכול לקרוא לפונקציה עם כל הארגומנטים (ה annotations הם של ה IDE).
  3. אני יכול לקרוא לפונקציה ולהשמיט את הארגומנט – ואז לקבל את ערך ברירת המחדל.
  4. הנה הגדרתי פונקציה לחישוב ממוצע ה CPU utilization במכונה – ע"י חישוב הממוצע של ה cores.
    שימו לב שניתן להשתמש בכתיב מקוצר (=), אם הפונקציה גוף הפונקציה הוא ביטוי (expression) בודד – גם אם אין ערך החזרה. הגדרה כזו נקראת shorthand function.
  5. אני יכול גם להגדיר מספר משתנה של פרמטרים – בעזרת המילה vararg (המקבילה של … בג'אווה).
    המשתנה core הוא מטיפוס <Array<out T  – אסביר את הנושא של out, כשנגיע ל Generics.אם יקראו ל printStats עם 2 פרמטרים – הפונקציה הראשונה תופעל.
    אם ל printStats עם מספר שונה של פרמטרים – הפונקציה השנייה תופעל.

התוצאה:

פונקציות מסדר גבוה

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

  1. הנה אנחנו מגדירים פונקציה בשם profile המקבלת פונקציה אחרת (func) כפרמטר.
    התחביר הוא סוגריים עם רשימת משתנים, חץ (<-) וערך החזרה.
    מעבר לכך, זהו פרמטר לכל דבר.
  2. בכדי להוסיף ערך מוסף כלשהו, השתמשתי בפונקציית ה profiling של ג'אווה, המחזירה את הזמן הנוכחי בננו-שניות.
  3. הפעלה של הפונקציה func נעשית בצורה טבעית למדי.
  4. עכשיו אגדיר 2 פונקציות דומות המתאימות בחתימה לפרמטר func של profile: מקבלות 2 מספרים שלמים – ומחזירות ערך של מספר שלם
  5. כעת אני סקרן לדעת איזו פונקציה יעילה יותר מבחינת ביצועים!
    כך אני מפעיל את הפונקציה "הגבוהה" profile:
    הקומפיילר צריך לדעת שאני רוצה לשלוח רפרנס לפונקציה ולא להפעיל אותה, את זה עושים בעזרת האופרטור :: – המחזיר reference לפנוקציה.
    הקפדתי על ארגומטים עם ערך זהה, על מנת לא להטות את בדיקת הביצועים 😉
    אם אני רוצה רפרנס למתודה של אובייקט, התחביר הוא object::method.
  6. מה קורה כאשר אני רוצה להגדיר חתימה של פונקציה שלא מחזירה כלום?
    אם אגדיר רק (Int) – הקומפיילר יתעלם מהסוגריים ויניח שמדובר בפרמטר מסוג Int.
    עלי לציין ערך החזרה מסוג Unit (כפי שראינו מוקדם יותר בפוסט) – ואז הקוד יתנהג כמצופה.
הנה הפלט:

אלף ננו-שניות הן מיקרו-שניה (ולא מילי-שניה, לא לבלבל!).
הממ… משהו מוזר לי בזמנים של הפונקציה add – הייתי מצפה לכ 50,000 ננו-שניות (5 הפעלות של פונקציה) ומטה (אופטימיזציות של הקומפיילר)…

Lambda Expressions

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

לא היה נחמד יותר לכתוב את גוף הפונקציה inline?

  1. הנה אני יכול לכתוב את הפונקציה inline, כ anonymous function. זה התחביר – ללא שם. זה עדיין לא מאוד אלגנטי.
  2. יותר אלגנטי יהיה לכתוב את אותה הפונקציה inline כ lambda expression. זה התחביר.
  3. מכיוון שהקומפיילר יודע מה החתימה של profile – זה מיותר לציין לו טיפוסים על הפרמטרים, ולכן ניתן לכתוב את הפונקציה בצורה קצרה יותר.
  4. חשוב לציין שאם אני עושה extract variable ל lambda expression, והיא כבר לא inline – הקומפיילר לא יידע להסיק את הטיפוסים של הפרמטרים – ויש לציין אותם בחזרה.
  5. לצורך ההסבר, נעבור לעבוד עכשיו עם פונקציה דומה ל profile בשם profileOne, המצפה לפרמטר אחד בלבד.
    שימו לב שהחלפתי את סדר הפרמטרים בפונקציה, כאשר func הוא הפרמטר האחרון. מייד אסביר מדוע זאת היא הקונבנציה ב higher order functions בקוטלין.
  6. כאשר יש פרמטר יחיד לפונקציה, לא צריך להגדיר אותו. אפשר להשתמש בצורה מקוצרת המדלגת על ההגדרה (<- num) ישר לגוף הפונקציה. אם לא הוגדר שם משתנה – ההתייחסות אליו תהיה כ it.
    זה יכול להיות קיצור נחמד, אבל הייתי ממליץ לכם להימנע ממנו כאשר יש פונקציות מקוננות – אז זה נהיה מבלבל.
  7. הנה הסיבה מדוע הקונבנציה בקוטלין היא שהפונקציה תהיה הפרמטר האחרון: אם ורק אם הפונקציה היא הפרמטר האחרון, אפשר לכתוב את גוף הפונקציה מחוץ לסוגריים.
  8. למה זה טוב? זה שימושי אם אתם רוצים לייצר משהו שדומה ל DSL (יש עוד מה להרחיב בנושא בפוסטי המשך).
    למשל: הגדרתי את הפונקציה enhance (שאולי מוסיפה לוגים, בדיקות אבטחה, ניטור, וכיוצ"ב) ואז אני פשוט שולח לה קוד. זה דומה מאוד ל DSL.
  9. אשנה קצת את ה layout של הקוד בכדי להדגיש: אתם יכולים להגדיר פונקציות שלכם, שנראות "כמו" מילה בשפה. כמה מבלבל הוא לראות enhance שכזה (מבלי להכיר את קוטלין) – ולא למצוא בגוגל מה הוא עושה 😏
אז עם כל התחביר הנחמד הזה של למבדה, מה הטעם להשאיר את ה Anonymous Function?
יש מקרים שבהם הוא בכל זאת שימושי, למשל: כאשר אתם רוצים לעשות return מתוך מקומות שונים בתוך הפונקציה.
מקרה אחר: אני יכול להגדיר return type – שאולי הוא subtype של מה שמגדירה הפונקציה מסדר-גבוה, המארחת.
Closures
פונקציה אנונימית ו lambda expression מסוגלים לגשת למשתנים של הפונקציה שעוטפת אותם – וגם לשנות אותם (זה שוני מג'אווה).

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

משתנה שניגשנו אליו בתוך Closure לא "יוקפא הערך שלו" – כמו בג'אווהסקיפט, למשל.

לאחר הסרת השורה הבעייתית, הנה הפלט:

סיכום

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

האם סיימנו לכסות את נושא הפונקציות?
עוד לא.

יש עוד Local Function, Infix Functions, Inline Functions, Extension Functions – ואולי עוד שכחתי משהו.

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

הנה פוסט ההמשך.

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