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

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

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

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

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

ליאור

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

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

הפעם נדבר על 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") אף יותר, ולחייב שירש / יממש יותר ממחלקה – כלומר: גם ממשקים. את האכיפה הזו עושים בתחביר המשתמש במילה where:
  1. רק טיפוס שגם יורש מ Entity וגם מממש את הממשק Comparable – יוכל להיקשר למופע של המחלקה.
  2. where הוא המקביל של קוטלין לצורה  של ג'אווה.

מדוע משתמשים ב "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

אנו משתמשים ב ? ולא 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

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

מה ניתן לעשות במתודה ()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, הגדירו את המבנה הגנרי  – וכך ניתן לשלוף 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 היה קשור ל .

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

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

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

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

Dropwizard internals: Bootstrapping flow

הפעם, במקום לכתוב פוסט – תרמתי ישר לתיעוד של פרוייקט Open-Source, בשם Dropwizard.

Dropwizard הוא פרוייקט ש"מדביק ביחד" כמה ספריות Java מוכרות, בכדי ליצור lightweight HTTP container – עבור מיקרו-שירותים בג'אווה (או כל שפת JVM אחרת):

הנה החלק שתרמתי – על ה Internals של המערכת: כיצד מתבצע ה bootstrap של אפליקציית Dropwizard:

יש המשך…

קישור למסמך בגיטהאב

התוספת תצא בתיעוד הרשמי של גרסה 1.2 שתשוחרר בקרוב.

אם אתם מכירים מישהו שעובד עם Dropwizard – אנא שתפו איתו 🙂

מקור שם הספריה, אגב, הוא הקומיקס הזה:

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

קוטלין (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)

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

סיכום

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

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

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