להכיר את גריידל (Gradle)

Gradle (מקור השם: עריסה, Cradle – כאשר האות הראשונה שונתה ל G עבור Groovy) הוא כלי Build נפוץ בעולם הג'אווה, וכלי ברירת-המחדל לפיתוח באנדרואיד.

Gradle הוא כלי Build מודרני, מלא פיצ'רים ואופטימיזציות – שנמצא דור או שניים לפנים מהחלופה העיקרית שלו: Maven. גריידל יודע לבצע Caching ו Incremental Build ברמות הפשטה שונות – ומסוגל לשפר מאוד את זמני ה Build של פרויקטים גדולים.
 
מצד שני, Gradle מורכב יותר לשימוש, וכנראה לפרויקטים פשוטים – מייבן הוא כנראה עדיין כלי עדיף, בשל הפשטות שלו.
 
בניגוד למייבן המשתמש ב XML להגדיר את ה buildscript – בגריידל משתמשים ב DSL מבוסס שפת-תכנות. גרדייל תומכת ב Groovy-DSL ו Kotlin-DSL, כאשר בשנתיים האחרונות היא עושה מעבר לכיוון קוטלין, מכיוון שזו הולכת והופכת לנפוצה יותר ויותר (בניגוד ל Groovy – שקצת "נתקעה" במקום).
 
לו היו נותנים שם היום לפרויקט, נראה ש Gradle היה נקרא Kradle.


לגריידל אין מערכת Repositories משלה, והיא משתמשת בזו של מייבן, של ivy, או פשוט בתיקיה של מערכת הקבצים המקומית. זה יתרון גדול, מכיוון שכך אין תחרות בין "Maven Repositories" ל "Gradle Repositories".
 
 

מה גריידל יכולה להציע מול מייבן?

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

 

הדבר הראשון, וכנראה החשוב ביותר הוא ביצועים:

  • בהרצה ראשונה, גריידל ומייבן יהיו דיי דומים. אולי גריידל תרוץ טיפה יותר מהר.
  • בהרצה השנייה והלאה, ייכנסו לשימוש מנגנונים ייחודיים ל Gradle:
    • Incremental Build – גריידל עוקבת אחר מערכת הקבצים ומקמפלת רק קבצים שהשתנו, או קבצים שתלויים בהם.
    • Config Cache ו Build Cache – שימוש חוזר בתוצרים (ברמות שונות) מהפעולות בילד קודמות.
      • תאורטית גם מייבן עושה זאת – אך בצורה משמעותית פחות יעילה.
    • Gradle Daemon – גרדייל מחזיקה תהליך (process) של מערכת ההפעלה שממשיך לחיות ברקע, להחזיק caches מיידיים בזכרון ולהנות מאופטימיזציות של Java JIT compilation.
גריידל מספקת יותר גמישות:

  • למייבן יש מחזור-חיים קבוע וקשיח, validate > compile > test > package > install, שלא ניתן לסטות ממנו, אלא ב"תרגילים". בגרדייל יש עץ של Tasks (כמו Compile או Test) שניתן לקנפג וקל הרבה יותר להתאים את שלבי הריצה לצרכים שלכם.
    • למשל: בכדי לדלג על שלב הבדיקות או הקומפילציה ב Maven יש לעשות "תרגילים" שלא תמיד מצליחים. בגרדייל דילוג על שלבים זו אופציה מובנית.
    • קל להגדיר Custom Tasks בגריידל – וזו גמישות רבת-עוצמה. המקבילה במייבן היא למצוא Plugin שמתאים בדיוק לצרכים, או לכתוב Plugin בעצמכם – שזה הרבה יותר מסובך
  • גריידל מספקת ניהול תלויות עשיר וחכם הרבה יותר ממייבן – מה שמשמעותי בפרויקטים גדולים ו/או מורכבים.
    • למשל: במייבן תלות במודול יגרור קבלת כל התלויות של אותה ספריה (propograted dependencies, שלעתים נקרא בטעות transitive dependencies). הדבר לא מאפשר לעקוב ולנהל במדויק תלויות של מודולים.
      בגריידל אפשר לבחור או בגישה זהה למייבן, או בגישה מדויקת ונשלטת יותר.
  • גריידל מממשת את העיקרון של Conventions over Configuration, ואין צורך באמת להשתמש בכל הכלים המתקדמים יותר שלה. קושי נפוץ של newcomers הוא להבחין אלו מנגנונים הם פשוטים ו straightforward, ואלו מנגנונים הם רבי-עוצמה, אך דורשים גם הבנה מעמיקה יותר.
גריידל היא מודרנית יותר:
  • גריידל זוכה לתמיכה טובה יותר, ועדכונים תכופים יותר. למשל: כבר שנה וחצי כשיש בעיה בהרצה של JUnit 5 ב Failsafe של מייבן בצורה מקבילית – אך בגריידל הכל עובד כשורה מהגרסה הראשונה של Junit 5.
  • לגריידל יש כלים תומכים משמעותיים וטובים יותר:
    • Build Scan עוזר לנתח ולהבין בדיוק מה קרה ב Build. האם הגדרה מסוימת שרצינו אכן פעלה? לא צריך לנחש מתוך הלוגים – אפשר לבדוק בצורה ישירה.
      • גריידל מספקת Build Scan רזה יותר גם למייבן – בעיקר בכדי לתמוך במעבר ולוודא שה Build בגריידל מכסה את כל מה שקרה במייבן.
    • Continuous Build – גרדייל יכול להאזין למערכת הקבצים ולקמפל את הקוד תוך כדי שאתם עובדים.
    • Gradle Profiler – שעוזר למצוא bottlenecks בבילד ולשפר אותם. ניתן לגשת לתוצאות שלו בתוך ה Build Scan.
    • Gradle Build Debugger – בגלל שה build Script הוא שפת תכנות – אפשר ממש לדבג את תהליך הבילד כדי לנתח תקלות קשות-להבנה.
    • Gradle Enterprise – הם סט של יכולות נוספות שחברת Gradle מספקת בתשלום – כמו Central build Cache (שימוש משותף בתוצרים: מפתח אחד קימפל מודול, כל השאר יכולים להשתמש בתוצר), או כלי Diagnostic ו Analytics משופרים לבילד ולהרצת הבדיקות.
  • נטפליקס, היא תורמת גדולה של Gradle Plugins. רבים מהם היו הבסיס לפיצ'רים שהיום הם סטנדרטיים בגריידל.
  • Gradle Wrapper (שמירת תאימות מדויקת של גרסת גריידל בין מפתחים) הוא כלי פופולארי בגריידל, אך שווה לציין שהוא קיים גם במייבן (אם כי שם הוא פחות נפוץ בשימוש).
התייחסות מהירה לבאזל
ב 2015 שחררה גוגל, את כלי ה Build שלה כפרויקט קוד פתוח, ולאחרונה הוא הפך ל GA.
  • Bazel ו Gradle דומות זו לזו, יותר משהן דומות ל Maven או Ant.
  • גריידל מספקת גמישות רבה יותר, באזל היא יותר מובנה.
  • באזל בנויה יותר מסביב לניהול מרכזי (למשל: ניהול גרסאות של תלויות) בעוד גריידל לא לוקחת הנחות כאלו, ותומכת טוב יותר במציאות מבוזרת של שליטה.
  • לגרדייל יש קהילה גדולה יותר, ובגרות גדולה יותר – במיוחד בעולם ה JVM. סיכוי טוב שקהילות כמו Go ו ++C יזכו לתמיכה טובה יותר בבאזל.
  • היכולת להשתמש ב Cache מרכזי ומשותף היא יכולת ליבה של Bazel (שעליה מתבססים במידה הביצועים הטובים של הספריה) בעוד ב Gradle זו יכולת פרמיום בתשלום (Gradle Enterprise).
  • בסה"כ באופן דיי עקבי (מסקירת מאמרים ומבחנים) Gradle מהירה יותר מ Bazel – גם כאשר משתמשים ב Build Cache מרכזי.
    • מבחני ביצועים לא מעטים משווים את שתי התצורות בגרסה החינמית שלהן – ואז ל Bazel יש יתרון משמעותי על יכולת שב Gradle היא בתשלום (Central Build Cache).
  • הנה מאמר מ 2015 של יוצרי גריידל שמנתחים את באזל.
אין ספק שבאזל מכניסה תחרות וגורמת לגריידל לעבוד קשה יותר. בעוד ההשוואה בין גריידל למייבן היא מאוד מחמיאה, ההשוואה של גריידל מול באזל נראית תחרותית הרבה יותר.
הפוסט שלי נכתב על גריידל, שנראה שתמשיך להיות בטווח הנראה לעין הסביבה המתקדמת והמקובלת בעולם ה JVM. בעולמות הללו לגריידל יש עדיין יתרון משמעותי – וקהילות לא מתחלפות כ"כ מהר. חשוב לציין שבאזל היא רענון חשוב בעולם כלי ה Build, ובוודאי היא מעניינת יותר בעולמות ה Go וה ++C כבר עכשיו.

תכל'ס

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

Maven

Gradle

Pom.xml,
Where:

  Properties section
  Modules section     

  DependencyManagement section
  Most other sections

build.gradle.kts
Mapping to:
  gradle.properties file
  settings.gradle.kts file
  dependencies.kts file (a convention)

  java-conventions.gradle.kts (file per language being compiled)

pom.xml file (what happens when we build)

build script (that comprised of multiple files)

Plugin

Task

–  (there is one fixed build flow)

Plugin (describes build flow)

Goal

Lifecycle Task

Module

(sub) project

Dependency scope

Dependency configuration

Profile

Custom Task or Custom Properties.gradle file (depends what the profile does).

“Install” (to maven repo)

“publication” (to whatever repo)

"package”

“assemble”

בואו נראה מבנה של קובץ בילד מינימלי (שם הקובץ הוא: build.gradle.kts. הסיומת kts מציינת שזה kotlin script, ולא Groovy – שמגיע ללא סיומת):
  1. אנו מייבאים את הפלאגין של Java.
    1. בגריידל, Plugin מביא איתו שורה של Tasks ו Lifecycle Tasks (ועוד הגדרות רבות) – ובעצם קובע את השלבים השונים ב build והסדר ביניהם. במייבן ניסו לבנות תהליך אחד גנרי לכל שפות-התכנות, למרות שלשפות שונות (JavaScript, ++C) – יש צרכים שונים. בגריידל לרוב יהיה לנו בפרויקט Plugin אחד שיגדיר את ליבת התהליך ויספק גם את ה Tasks הנדרשים (Compile, Test, Jar, וכו' – המתאימים לשפה / תהליך)
      בנוסף אולי נרצה להשתמש ב Plugins נוספים כמו: למשל War, PMD, או Reports – ולהרחיב את התהליך.
  2. ה default scope שלנו ברמה הגבוהה ביותר היא אובייקט הפרויקט, וכך אנו קובעים properties מסוימים על הפרויקט.
  3. כאן אנו קובעים באלו Repositories לחפש את ה dependencies שלנו. כמו במייבן, אפשר ומומלץ לקבוע כמה repositories בכדי שיהיה גיבוי (לא עשינו את זה בדוגמה) – והחיפוש יעשה ע"פ הסדר בו נרשמו.
    1. במקום לספק את ה URL ל jCenter, אנו יכולים להשתמש בפונקציה שמסופקת ע"י גריידל ומחזירה אובייקט Repository עם ה URL הנכון (קיימים כאלו ל repositories נפוצים). פחות מקום לטעויות.
  4. כאן אנו נכנסים ל scope של ה Java Plugin (שהגדרנו בתחילת הקובץ) – ומוסיפים לו הגדרות.
    1. כברירת מחדל, גריידל תשתמש ב build בגרסת הג'אווה שמריצה אותה. זה יכול ליצור חוסר עקביות אם במחשבים שונים מריצים את הבילד בגרסאות ג'אווה שונות.
    2. בכדי "ליישר" את תהליך הבילד בצורה מדויקת יותר, אנו יכולים להגדיר לגריידל באזו גרסת ג'אווה להשתמש בתהליך הבילד. ה toolchain עוזר בקלות רבה לקנפג ולטפל בהגדרה של הגרסה שבחרנו. אם לא מצליחים למצוא מקומית את הגרסה המתאימה – ה toolchain יוריד לצורך ה build עותק מקומי של גרסת הגא'ווה שנבחרה.
    3. גם אם אתם מתכנתים בג'אווה 8 (ממגבלות מוכרות), הרצה של תהליך הבילד בגרסה מתקדמת יותר – יכולה להאיץ אותו מעט.
  5. כאן אנו נכנסים ל scope של ה tasks בפרויקט. אמנם אנו רוצים להתייחס ל Tasks שהגיעו מה Java Plugin, אך מרגע שהוספנו את ה Java Plugin והוא רשם את ה Tasks שלו – הם כבר לא משויכים אליו, אלא פשוט רשמים בפרויקט.
    1. ספציפית יש כאן כתיבה מקוצרת בשורה אחת להכנס גם ל scope של ה test plugin.
    2. ה Test Plugin תומך גם ב Junit וגם ב TestNG – ועלינו לציין לו במי להשתמש. הכל עטוף בפונקציות פשוטות ובטוחות לשימוש.
    3. בניגוד ל Maven בו יש שני Plugins: גם Surefire (לבדיקות יחידה) ו Failsafe (לבדיקות אינטגרציה) – בגריידל ה Java Test Tasks מספק את שני הצרכים, וניתן פשוט לקבוע בהגדרות אם לעצור בכשלון ראשון – או לא. אם אחנו רוצים להגדיר שלב של "בדיקות יחידה איטיות" – קל לעשות זאת ע"י שימוש חוזר ב Test Task.
  6. אנו צריכים להגדיר את התלויות לגרסה הספציפית של Junit שבה אנו רוצים להשתמש. לא פינקו אותנו ב wrapper לקונפיגורציה הזו. כמו במייבן – הגדרה של dependency בקובץ הראשי תשפיע על כל תתי-הפרויקטים (במייבן: מודולים) שלו.
    1. ההגדרה הזו מקבילה במייבן לתחביר הבא:

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

    2. מהן הפונקציות הללו,()testImplenentation ו ()testRuntimeOnly?
      נסביר עליהן בהמשך.

The Java Plugin

כפי שציינתי, מרכיב חשוב בגריידל הוא ה Plugin הליבה בו אנחנו משתמשים. הוא מגדיר הרבה דברים חשובים:
  • Tasks – בהם נוכל להשתמש.
  • LifeCycle Tasks (במייבן: "Goals") – אליהם Plugins שונים יוכלו להתחבר, ולחבר Tasks שונים.
  • מבנה התיקיות של קוד המקור (במקרה זה ה Java Plugin שומר על הקונבנציה המורכת לנו ממייבן)
  • Dependency Management (כללים)  – כללים כיצד תלויות מנוהלות. מכיוון שלסביבות פיתוח שונות (Go, ++C, JVM) יש צרכים שונים – נתנו בגריידל ל Plugin להגדיר את הכללים המדויקים לניהול התלויות.
    גריידל מספקת תשתית/כלים עליהם כל Plugin יכול לבנות את המודל שהוא זקוק לו.
כשאנחנו לומדים לעבוד עם Gradle, חלק מהכללים נובעים מה Plugin הליבה בו אנחנו משתמשים – ולכן חשוב ללמוד ולהבין אותו היטב. במקרה שלנו – זה ה Java Plugin, וחשוב ללמוד אותו.

ל Java Plugin יש כמה הרחבות סטנדרטיות:

  • Java Library Plugin – עבור build script של ספריה המופצת לקהל רחב כספריה.
  • Java Application Plugin – עבור build script של אפליקציה. מוסיף Tasks כגון start או install. הוא מתאים הן לכתיבת שרת או אפליקציה שולחנית.
  • Java Platform Plugin – לכתיבה של סט ספריות קשורות (למשל: Junit 5 מורכב מכמה תתי-פרויקטים, הקשורים זה לזה)
  • גרסאות ספציפיות ל Groovy ו Scala – המרחיבות
ייתכן והיה נכון יותר לקרוא לו "JVM Plugin".
אתם יכולים לעבוד עם ה Java Plugin ישירות, או כל אחת מההרחבות שלו שיכולות להקל על המקרה הספציפי. בכל מקרה, רוב ההתנהגויות בכל הוריאציות – נובעות מה Java Plugin עצמו.
התיעוד הוא כלי חשוב, ובוודאי תמצאו את עצמכם לא פעם ניגשים לתיעוד של ה Java Plugin ישירות. במסגרת הפוסט, אני אתמקד בהסבר של שני רעיונות שמעט שונים ממייבן: Lifecycle Tree, וניהול תלויות.

מחזור החיים של בילד ב Java Plugin בגריידל

נתחיל עם מקור ההשראה: מייבן.
 
בניגוד ל make, ant, וכלים אחרים שהיו מקובלים קודם לכן והיו חסרי-מבנה סטנדרטי – מייבן הגדירה מבנה סטנדרטי – שעזר להשריש best practices בתהליכי build, ולהפוך אותם לסטנדרטיים ומאורגנים יותר.
 
מייבן הגדירה שלושה Lifecycles (נקרא להם build, clean, site), שבכל Lifecycle יש שורה של צעדים קבועים (להלן: "Goals"). כל Plugin (המספקים יכולות) מגדיר באלו צעדים הוא יכול להשתתף (למשל: Fail-Safe יכול להתחבר ל verify או integration-test) ואז המפתח ראשי לחבר אותו לאחד או יותר מהצעדים הנתמכים.

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

 

המבנה בגריידל הוא דומה, אך גמיש יותר. במקום שרשרת טורית של צעדים – ישנו עץ. העץ מורכב ממשימות (Tasks) כאשר כל משימה תגרום להרצה של המשימות האחרות בהן היא תלויה.
למשל, כאשר אנו מריצים את המשימה classes (המקבילה ל compile במייבן), המשימה תגרום להרצה של המשימות compileJava ו processsResources – ורק אז תרוץ בעצמה.
כאשר מריצים פרויקט המורכב מכמה תתי-פרויקטים בגרדייל, גריידל יכול להריץ במקביל משימות שונות בפרויקטים שונים על מנת להתקדם מהר יותר. למשל: פרויקט X תלוי בפרויקטים a ו b. גריידל יריץ את שניהם במקביל. אם פרויקט a הוא קטן יותר, גריידל עשוי להתקדם בפרויקט a לשלב הבדיקות על אף שפרויקט b עדיין מתקמפל.
כפי שאתם יכולים לראות בתרשים, משימות בגריידל מסווגות למשימות רגילות (עושות משהו ; בכתום) ומשימות Lifecycle (באדום), הנועדו לשמש כעוגן / מבנה לתהליך ה build.
למשל, המשימות assemble, clean, build ו check (באדום מודגש) הן משימות Lifecycle של ה base plugin בגריידל. כלומר: הן צפויות להיות נוכחות בכל מחזור חיים כלשהו. כמפתח Task אני יכול להסתמך על כך שהן יהיו נוכחות – ולבקש שהן ירשמו ל Task שלי כתלות.
לדוגמה, ה CheckStyle Plugin (כלי לבדיקות קוד סטטיות בג'אווה) רושם למשימה Check תלות בכל ה Tasks של ה Plugin – כך שהרצה של המשימה Check תפעיל אותו. אם לפני הרישום המשימות Check ו Test עשו אותו הדבר, לאחר הרישום – יש ביניהן הבדל.
משימות ה Lifecycle מייצרות סטנדרטיזציה ומבנה כדי שה Builds יהיו פשוטים ומובנים יותר.
כאשר מדובר ב Multi-project build (המקבילה של modules במייבן), כל הרצה של משימה על הפרויקט הראשי – תריץ את אותה המשימה על כל ה sub-projects (כמו במייבן).

Dependency Configurations

זוכרים את התלויות שהגדרנו ל JUnit5 ב build script שהצגנו למעלה?
השתמשנו בפונקציות ()testImplenentation ו ()testRuntimeOnly – על מנת להגדיר תלויות.
אנו נוטים לחשוב על גרף התלויות בפרויקט כמבנה קבוע, אך בעצם גרף התלויות הוא תלוי-הקשר:
אנו רוצים למשל שקבצים וספריות מסוימות (למשל: ספריות Mocking) יהיו זמינות רק כאשר מריצים בדיקות, בעוד אנו לא מעוניינים שהקוד בפרודקשיין יהיה תלוי בהן, או יארוז אותן.
היכולת להפריד בין קוד "לבדיקות" לקוד "פרודקטיבי" מתבטאת בגריידל בשני גרפי תלויות:
גרף התלויות של הבדיקות ("testImplementation") יורש את כל התלויות שהוגדרו בגרף של הקוד הפרודקטיבי ("implementation") – ויכול להוסיף עליהן תלויות נוספות.
הפונקציות ()testImplenentation ו ()testRuntimeOnly – פשוט רושמות תלות בגרף תלויות. עדיין לא הצגנו את גרף התלויות של testRuntimeOnly.
בגריידל התלות ברירת-המחדל / המומלצת היא "implementation". יש תלות בסיסי נוספת לקוד פרודקטיבי הנקראת "api" – והיא מדמה את ההתנהגות ברירת-המחדל של מייבן (מה שנקרא במייבן "compile scope"):
נניח שאני כותב ספריה (להלן "My Library") שאפליקציה מסויימת משתמשת בה.
כאשר אני יוצר תלות מסוג "api" בספרייה Jackson, משמע שאני הופך אותה לחלק מה API שלי, ובעצם מעביר (propogate) לאפליקציה את התלות כאילו היא שלה. מעכשיו קוד באפליקציה יכולה להשתמש ב Jackson כאילו ייבאה אותה בעצמה.
כאשר אני יוצר תלות מסוג "implementation" ספריה Gson, משמע שאני משתמש בתלות לצורך המימוש שלי, אבל התלות לא מועברת הלאה. אם האפליקציה רוצה להשתמש ב Gson יהיה עליה להוסיף תלות בעצמה או להסתמך על פונציונליות שהספריה שלי מספקת, שמתשמשת מאחורי הקלעים ב Gson.
לשימוש ב api יש תופעה שלילית שנקראת Dependency Pollution:
  • קוד האפליקציה יכול להשתמש בקוד מספריות שלא הייתה כוונה להשתמש בהן.
    • המקרה הזה בעייתי במיוחד בקוד פנימי שלנו, שאנו רוצים במפורש להימנע מהשימוש בו – אך מפתחים עשויים לא לשים לב (כי ה package name תואמים).
  • בשל האופי הטרנזיטיבי של התלויות, מרחב הקוד שאני שחשוף לשימוש ע"י קוד האפליקציה עשוי להיות גדול מאוד: האפליקציה תלויה ישירות ב 3 ספריות, אך ב 50 ספריות בצורה טרנזיטיבית – הוא תסריט נפוץ.
  • יש עבודה מיותרת לתהליך ה build – בהבאת כל הספריות, טרנזיטיבית, ל classpath בזמן קומפילציה.
  • יש עבודה מיותרת לתהליך ה build – בכך שכל שינוי קוד בספריה שהובאה טרנזיבית, מעלה ספק אולי צריך לקמפל את הספריות שתלויות בו (טרנזיטיבית) – וכך קשה לבסס build cache יעיל.
בקיצור: ההעדפה בתלות מסוג "implementation" חשובה הן מבחינת הנדסת-תוכנה, והן משיקולי ביצועים של ה build.
אם אתם רגילים לעבוד במייבן "הסלחנית" במובן של תלויות – תתחילו להתרגל.
בטווח הבינוני – ארוך, בהחלט משתלם להתרגל להגדרות המדויקות יותר של התלויות כ "implementation". בבאזל, למשל, הגדרת התלויות דורשות דיוק רב אף יותר.
בגריידל יש מנגנון נוסף, המתבטא ב Dependency Configuration בשם "runtimeOnly" ו "compileOnly" הקבילים ל scopes במייבן בשם runtime ו provided – בהתאמה.
התלויות הללו מאפשרות לנו להיות תלויים בזמן הקומפילציה ב API בלבד (לא לבלבל עם ה "api dependency configuration"), ובזמן ריצה להביא מימוש ספציפי של אותו ה API – תוך כדי שאנו מבטיחים שאנו לא יוצרים תלות במימוש הספציפי.
למשל: ספריית SLF4J, מספקת API לקומפילציה בלבד וגם מימושים הכוללים את ה API + יישום ספציפי (למשל LogBack או log4J12).
בכדי להבטיח חוסר תלות ביישום ספציפי – אנו רוצים לא לכלול את המימוש הנוכחי שבחרנו בזמן הקומפיציה – אלא רק בזמן ריצה. אם כך עלינו להוסיף את ה SLF4J API ב "compileOnly" ואת המימוש העכשוי – ב "runtimeOnly".
נראה שאנחנו יכולים להסביר עכשיו את התלויות שראינו בדוגמה למעלה:
ספריית JUnit5 הפרידה את ה API לספריה אחת (להלן "testImplementation", אנו רוצים אותו זמין רק לבדיקות) ואת מנוע הבדיקות – שיהיה זמין רק בעת הרצת הבדיקות ("testRuntimeOnly").
כלומר: המקרה של JUnit 5 מעט שונה מזה של SLF4J – אבל קיימות גם וריאציות נוספות.
יצרתי סיכום של ה Dependencies Configurations הזמינים לנו בגריידל + Java Plugin:

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

סיכום

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

שיהיה בהצלחה!
—–
קישורים רלוונטיים:

ג'אווה 11 – 9: שאלות ותשובות – שכדאי להכיר

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

החברה שפיתחה את ג'אווה במקור (Sun Microsystems) כשלה עסקית ונרכשה ע"י Oracle בשנת 2010 – שקיבלה את הנכס הנדיר שנקרא ג'אווה, אך נכס שגם דורש השקעה רבה ומחולק בחינם בעולם.

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

כשאנו מדברים על שינוים אנחנו לא מדברים רק על שינוים בשפת ג'אווה עצמה – אלא בעיקר על שינויים בפלטפורמת ה JVM, פלטפורמה שמשרתת את Scala, Closure, קוטלין, Groovy, JRuby, ועוד. חשוב לזכור שה JVM הוא גדול יותר מג'אווה.

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

  • Module System – מודולריזציה בתוך ה JDK, ולראשונה הסרה של קוד ישן (בכמויות לא-מבוטלות). המהלך מציע כמה יתרונות טכניים חשובים – אך משמעותו שלראשונה ה JDK הופך ל non-backward-compatible, מה שעשוי לפגוע לאורך שנים בקצב האימוץ של הגרסאות החדשות. הסרת קוד ישן מה JDK מתוכננת להמשיך בגרסאות עתידיות של ג'אווה גם כן.
  • New release cycle – הכולל releases "קטנים" כל חצי שנה, וגרסאות LTS.
  • גביית דמי-שימוש עבור עדכוני באגים ועדכוני-אבטחה – אורקל עושה מהלך משמעותי על מנת לסבסד את תחזוקת ג'אווה ע"י משתמשיה, ואולי אף יוביל לרווחים משמעותיים לאורקל עצמה.
    • יישור קו בין OracleJDK ל OpenJDK – צעד משנה למהלך הנ"ל.

גרסה 9 הציגה לראשונה התחלה של כל השינויים הנ"ל, אך הוכרז מראש שזו גרסה שתתמך לחצי-שנה בלבד, וגרסת היעד לתמיכה ארוכה (LTS, כלומר Long-term support) תהיה גרסה 11.
מאז יצאה גם ג'אווה 10 – גם היא גרסה מינורית ולא כ"כ נוספת, שסיפקה עוד כמה התקדמויות בנושאים הנ"ל (ומספר תוספות קטנות לשפה) – אבל חשיבות הגרסה הייתה בהיותה הכנה גנרלית לשחרור גרסה 11.

גרסה 11 היא זו שתעמת את קהילת ה JVM בפעם הראשונה באמת עם מציאות חדשה, והדילמות שנובעות ממנה. זו גרסת LTS שתתמך עכשיו עד 8 שנים קדימה, גרסת ג'אווה המשמעותית שתהיה זמינה בשנים הקרובות.

  • האם קהילת הג'אווה שהייתה רגילה לג'אווה חינמית תתחיל לשלם ל Oracle עבור עדכונים ותמיכה?
  • האם החלק הארי של הקהילה יישאר על גרסה 8, או שנראה אימוץ משמעותי גם של גרסאות 11 והלאה, כבר בשנים הקרובות?

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

בואו נתחיל לסגור פינות…

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

מה ההבדל בעצם בין OpenJDK ו OracleJDK? ומה השתנה?

החל מגרסה 7 החלה להיות משוחררת ההפצה של OpenJDK, שהיא הפצה תחת רישיון GNU GPL v2 עם החרגה ל linking – רישיון Open Source חופשי, המאפשר שימוש בג'אווה בכדי לכתוב תוכנה מסחרית ולמכור אותה.

היוזמה הייתה עוד של חברת Sun, והגנה על האופי הפתוח של ג'אווה.

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

החל מגרסה 9, הפצות ה OpenJDK וה OracleJDK מתאחדות. זה לא יקרה ע"י הסרה של תוכן מתוך ה OracleJDK אלא בעיקר ע"י פתיחה של התוספות שאורקל סיפקה להיות OpenSource. כמה תכונות עם רישיון מסחרי (כמו ה Java Web Start) אכן יוסרו מה JDK, אבל בעיקר בגלל שהאימוץ שלהן נמוך למדי. התהליך מסתיים עכשיו בשחרור גרסה 11, בו תוכן ההפצות הוא זהה – וההבדל ביניהן הוא רק הרישיון.

למה לי לשלם לאורקל כסף על מה שזמין בחינם וזוכה לתמיכה של הקהילה (כלומר: OpenJDK)?

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

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

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

ג'אווה 9 הציגה מחזור חדש של שחרור גרסאות שנראה כך:

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

החל מגרסה 9, אורקל מתחייבת שחרר גרסה פעמיים בשנה, מה שיאפשר לשחרר תוספות לשפה וה JVM בתדירות גבוהה יותר.
הבחירה במספרים שלמים עבור הגרסאות הללו לא מצביע על כמות התוכן שיתווסף, אלא על אופנה של השנים האחרונות ״לרוץ״ עם מספרי גרסאות (למשל: ראקט הייתה בגרסה 0.14 לפני שלוש שנים – אך כיום היא כבר בגרסה 16.5. אנגולר הייתה בגרסה 2.0 ב 2014, אך עוד מעט משתחררת גרסה 7.0).

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

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

  • לעדכן את גרסת ה JVM/ג'אווה שלנו כל 6 חודשים כמו שעון. כל יום שאנו לא על הגרסה האחרונה הוא יום שבוא יכול להשתחרר עדכון אבטחה קריטי שלא נקבל.
  • להשתמש ב OracleJDK בתשלום ולעבוד על גרסאות LTS על מנת לקבל עדכוני אבטחה שוטפים. התשלום הוא כ $2.5 דולר לחודש ל Desktop/Laptop ו $25 לחודש לכל Core פיסי של שרת [1].
  • לקנות תמיכה מספק צד-שלישי, כגון Azul או RedHat כאשר הם אלו שיספקו תיקונים לבאגים ובעיות אבטחה. כל חברה – עם המדיניות שלה.
    • למשל, ל Azul יש תוכנית בשם Medium Term Support (בקיצור MTS), שבה היא תתמוך לאורך שנתיים וחצי בכל גרסה שניה של ג'אווה שאיננה LTS. יתרון גדול בגישה הזו היא חפיפה בתמיכה בין גרסאות ה MTS – מה שלא קיים בתמיכה ה"חינמית" של אורקל .
    • חשוב לספקי צד-שלישי יספקו גם תמיכה לג'אווה גרסה 8 לעוד כמה שנים – היכן שבכל מקרה רוב התעשייה צפויה להיות בזמן הזה. הנה למשל הצהרה של יבמ.
עוד פרט שכדאי לציין הוא שהרישיון של OracleJDK (לפחות לגרסה 11) מאפשר שימוש ללא הגבלה לצורך פיתוח. התשלום הוא רק עבור שימוש production (שאותו ישלם לקוח המריץ את הקוד On-Premises, או ספק SaaS – עבור המכונות שהוא מריץ).

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

באמת היא כנראה איפשהו באמצע. אורקל מצהירה שכל גרסה שתשוחרר תעבור Quality cycle מפרך כמו של גרסה מ'אגורית עד היום. כמו כן יש הצהרה שהיא תשחרר לפחות שני עדכונים (תיקוני באגים ואבטחה) מינוריים – לכל גרסה שתצא. גרסאות שאינן LTS  הן לא גרסאות "בטא" – ע"פ ההצהרה.
סביר להניח ששחרור גרסאות תכוף יאפשר לג'אווה להתפתח מהר יותר – אך במידה. מי שצפוי לעבוד באמת עם גרסאות שאינן LTS הוא רק פלח צר של Early adopters (למשל: סטארטאפ בתחילת דרכו), במיוחד בשנים הראשונות.

האם מה שאורקל עושה הוא "בסדר"? האם זה לא "מחטף מרושע"?

אין לי תשובה ברורה. בד"כ כשארגון רוצה לקבל הכנסות מפרויקט Open Source – הוא מציע ערך נוסף על הקיים בכדי לקבל עליו תשלום: Premium support או יכולות נוספות (למשל: Redis modules).
אורקל בעצם ממשיכה לגבות תשלום על תמיכה, תוך כדי שהיא מפסיקה בפועל את התמיכה-בחינם שהיא סיפקה לאורך שנים רבות. אם היה מדובר בפרויקט קטן – זה אולי לא היה מתקבל בשמחה, אך זה לא היה עושה הרבה רעש. ג'אווה היא אחת מהתשתיות (ביחד עם לינוקס, MySQL ועוד כמה) המשמעותיות ביותר המסופקות בחינם. ההשפעה – היא משמעותית!

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

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

כאב ראש לאנשים רבים בעולם – ככל הנראה ייגרם בכל זאת.

"איך אתה מתכנן להגיב לשינוי בתהליך שחרור הגרסאות של ג'אווה" מתוך סקר של ה JVM Ecosystem

דיברנו על איזו בעיה של תאימות לאחור, במה בדיוק מדובר?

ג'אווה 9 הציגה לראשונה את ה Module System (הידוע לשעבר כ Project Jigsaw או super packages) – כלי מודולריזציה בשפה.

packages, כלי ארגון הקוד עד עכשיו, הוא כלי נחמד – אבל עם מגבלה עיקרית: הוא עוזר לארגן את תוך ה package (למשל: ניתן להגדיר מחלקות כ private ל package) אך הוא לא מתמודד עם ניהול תלויות בין packages.

  • אין לי יכולת אמיתית לחשוף מחלקות מסוימות בצורה סלקטיבית ל packages אחרים – הרזולוציה היא רק private או public.
  • אין ניהול רציני של התלויות בין ה packages.
    • כל מחלקה יכולה להוסיף כל תלות שהיא רוצה ל package אחר. כלומר: התלויות בין ה packages מגודרות שוב ושוב, ומפוזרות בין ה classes השונים של המחלקה – שזה לא DRY, וקשה מאוד לבקרה.
    • כל מחלקה יכולה להוסיף את עצמה לכל package – כך שאין שליטה ממה מורכב בדיוק ה package. זו בעיה משנית.
ה Module System (בקיצור: MDS) של ג'אווה מאפשר להגדיר קובץ בשם module-info.java המגדיר מודול ואת התלויות שלו. הנה דוגמה לכזו הגדרה:
module monitor.rest {
    requires spark.core;
    requires monitor.statistics;
    exports monitor.rest;
}
הקומפיילר יתייחס להגדרות ויאכוף אותן (יופי!).
חשוב לציין שמילים שמורות חדשות, כמו module ו requires הן בעלות משמעות רק בקובץ ה module-info, ולא ישפיעו על שאר קוד הג'אווה.

קישורים: Java 9 modules cheat sheet, מדריך, ועוד כמה מקורות טובים

ג'אווה השתמשה ב MDS בעצמה עבור ה JDK. במידה מסוימת זה היה סוג של "eat your own dog food", אבל מצד שני – זה היה צורך מרכזי של ה JDK עצמו, שעם השנים התלויות הרבות והמיותרות בו הפכו אותו לקשה מאוד לתחזוקה. באורקל טוענים שזו אחת הסיבות לשחרור הכל-כך איטי של שינויים בג'אווה.

במעבר מ Java 8 ל Java 11, אורקל חילקה את ה JDK לכ 80 מודולים שונים, חלקים יצאו מה JDK ויהיו זמינים כספרייה נפרדת, או שנפרד מהם לגמרי. הנה רשימת היכולות שהוסרו מה JDK:

  • corba – לזקנים ביננו, שזכו להכיר את המפלצת.
  • java.transaction (מדובר בטרנזקציות אפליקטיביות, לא של JDBC. כאלו כמו שהיו ב EJB גרסה 1).
  • java.xml.ws וספריות נוספות של טיפול ב XML (למשל JAXB). בתחילת שנות ה 2000 התייחסו ל XML כ "פורמט שהולך לתאר את כל המידע בעולם". תזכרו את זה כשאתם מתחילים להתלהב מטכנולוגיה חדשה.
  • JavaFX – טכנולוגיית Desktop UI של ג'אווה שלא ממש הצליחה. לג'אווה יש היסטוריה מרשימה ביותר של טכנולוגיות UI כושלות (אפשר לדבר בהזדמנות גם למה…)
    • JavaFX תמשיך להיות זמינה כמודול עצמאי שלא קשור ל JDK.
  • Applets
  • Java WebStart (טכנולוגיה להתקנה ועדכון אוטומטי של אפליקציות ג'אווה)
  • Nashorn שהחליף את Rhino כמפרשן JavaScript שרץ על גבי ג'אווה ומסופק כחלק מה JDK.
    • נשהורן הוא חדש, אך שפת ג'אווהסקריפט מתפתחת בקצב כ"כ מהיר, שהייתה מספיקה גרסה אחת בכדי להבין שלא ישים לתחזק עוד מפרשן JavaScript כחלק מה JDK.

בנוסף, בג'אווה 9 עד 11 הסירו כמה פונקציות שהיו deprecated לאורך זמן רב. זו הפעם הראשונה בהיסטוריה של ג'אווה שקוד באמת מוסר מה JDK.
אם אתם משתמשים באחת מהטכנולוגיות או ה APIs שהוסרו מה JDK – יהיה עליכם ליבא אותן בצורה מפורשת (למשל: באמצעות מייבן) או למצוא חלופה (למשל ל Applets או Nashron – שלא ייתמכו יותר).

המודולריזציה של ה JDK תסייע גם להלחם בצריכת הזיכרון הגבוהה של ג'אווה, ובגודל ה distributables. ג'אווה עובדת עם Dynamic linking (קישור של קוד בזמן ריצה, ולא בזמן קומפילציה). יש בכך כמה יתרונות (קל מאוד לייצר Plugin architecture, ניתן לבצע אופטימזציות מסוימות) אבל המחיר הוא קבצי jars גדולים מהנדרש (אנו כוללים את כל הקוד בספריה, אפילו אם אנו זקוקים רק ל 5% ממנה) וגם צריכת זיכרון גבוהה יותר.

בעולם ה micro-services וה FaaS (למשל: AWS lambda) – התכונות הללו הן מגבלה רצינית של ג'אווה. למבדה קטנה בג'אווה יכולה בקלות לדרוש jar file של עשרות MB ולצרוך מאות MBs של זיכרון. למבדה דומה ב ++C או Go תדרוש שבריר מהמשאבים של ג'אווה. כנ"ל לגבי ג'אווהסקריפט או פייטון – אבל מסיבות קצת שונות.

ג'אווה 9 הוסיפה כלי בשם jlink המאפשר לבנות אפליקציית ג'אווה עם static linking. השימוש העיקרי הוא FaaS או הפצה של ג'אווה למכשירים עם מגבלות במשאבים. החיסרון של jlink הוא שהתוצר לא ניתן לעדכון ללא החלפה מלאה של התוצר הבינרי. היום ניתן לעדכן גרסאת JRE ולקבל עדכוני אבטחה / באגים קריטיים מבלי לעדכן את קוד האפליקציה. אפליקציה שנבנתה עם jlink תדרוש בנייה מחדש ו deploy מחדש – על מנת להחיל את אותם העדכונים.

הסכנה שבשינוי

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

המעבר בין ג'אווה 8 לג'אווה 11, או בין JDK 8 ל JDK 11 (למי שמשתמש בשפת JVM שאיננה ג'אווה) – עומד להיות עניין גדול יותר.

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

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

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

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

בואו ניזכר שנייה מה קרה בפייטון:

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

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

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

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

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

פייטון 3 שוחררה ב 2008, אבל לקח בערך עשור עד שרוב הקהילה הצליחה לעשות את המעבר.

—————–

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

האם המעבר לג'אווה יהיה מהיר יחסית, או שיגרר לאורך שנים?

נחכה ונראה.

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

קישורים: מדריך לעדכון פרויקט מייבן לג'אווה 11.

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

—-

[1] החישוב הוא באמת יותר מורכב, ופחות קל להבנה. לארכיטקטורות שונות של חומרה, יש הגדרה שונה למהו "Processor". רישיונות הוא בד"כ נושא סבוך בחברות Enterprise – אז אל תניחו שהסיבוכיות הזו היא "תרגיל" של אורקל.

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

הפעם אני רוצה לדבר על Interoperability בין קוטלין וג'אווה.

 

באופן כללי, ה interoperability בין השתיים הוא מצוין – וייתכן ותוכלו לעבוד זמן ארוך מבלי להיתקל בבעיות.

מתישהו… במקרי הקצה – זה יגיע.
משהו בג'אווה לא יאהב משהו בקוטלין (או אולי ההיפך – אבל זה פחות נפוץ).

כיצד null יכול לצוץ לו בקוטלין ללא הודעה מוקדמת?

 

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

בוודאי שאפשר! אחרת לא הייתי אומר ש interoperability ביניהן כ"כ מוצלח.

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

ערבוב של קוד קוטלין וג'אווה לצורך רצף הקריאות. במציאות כמובן שהקוד ישב בקבצים נפרדים.
  1. יצרנו מחלקה מופשטת A בשפת ג'אווה.
  2. הרחבנו את המחלקה בג'אווה A – בעזרת מחלקה בקוטלין B.
    1. מכיוון שברירת המחדל בקוטלין היא מחלקה final – עלינו להגדיר אותה כ open ע"מ שקוד הג'אווה יוכל לרשת את המחלקה C.
  3. ואכן הרחבנו את המחלקה בקוטלין B בג'אווה, ללא בעיה. כל שפה שומרת על הקונבנציות שלה (במידת האפשר)
  4. הממ… ה IDE מעיר לי פה משהו: 
Not annotated method overrides method annotated with @NotNull 

מה זה?
אני לא רואה Annotation בשם NotNull@ בקוד.

מה? java.lang.NullPointerException? – אבל אני כותב בקוטלין!?

 

בכדי להבין מה קורה, נחזור שלב אחר אחורה – למחלקה KotlinB.

במחלקה הזו דרסנו את המתודה ()getHelloMessage שהוגדרה בג'אווה.
ערך ההחזרה של המתודה שהוגדרה בג'אווה הוא String, אבל מה זה אומר עבור קוטלין: String או ?String, אולי?

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

והנה השימוש שלה בקוטלין:

ה IDE מסמן לי שערך ההחזרה של המתודה הזו הוא !String.

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

מה שבאמת ה IDE מנסה לומר לנו הוא שהוא לא יודע אם ה String הוא null או לא. אני חושב שתחביר כמו [?]String היה יכול להיות יותר אינטואיטיבי.

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

other?.java?.object?.always?.might?.be?.null()
 

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

זה נוח, אבל גם יכול לגרום לשגיאות בלתי צפויות.

הנה דוגמה מהחיים:

jdbi הוא פריימוק רזה (וחביב) הכתוב בג'אווה, ומאפשר גישה לבסיס הנתונים.
אופן השימוש בו הוא להגדיר interface או abstract class עם מתודות ומוסיף להן annotation עם השאילתה שיש לממש.
jdbi, בעזרת reflection, מג'נרט (בזמן ריצה) אובייקט DAO שמממש את הממשק שהגדרתי. התוצאה היא אובייקט שמבצע את השאילות שהגדרתי ב annotations. קוד עובד.

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

…עד הרגע שאני מפעיל את השאילתה עם job_id שלא קיים – וחוזר לי null.
מתישהו אני "חוטף" NullPointerException.

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

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

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

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

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

JebBrains (החברה מאוחרי קוטלין ו IntelliJ) סיפקה annotations לג'אווה שיכולים להנחות את ה IDE, מתי צפוי null ומתי לא ייתכן null. הנה דוגמה:

השימוש ב annotation מסיר מה IDE את הספק:

ואז הוא יכול להגן עלי.

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

באופן דומה, אגב:

(Mutable)List

הוא סימן שה IDE מספק שמשמעו: רשימה שייתכן שהיא mutable, וייתכן immutable. הקומפיילר לא מסוגל להגיע למסקנה בעצמו.

הנה דוגמה לביטוי מורכב:

  • הרשימה ו/או האיברים בה עלולים להכיל ערך null.
  • הרשימה עשויה להיות mutable או לא.
קוד הג'אווה מאחורי המתודה ()getStrings הוא זה:
 
 
מה שמוביל אותנו לעניין נוסף שכדאי להכיר:
כאשר מתודה בג'אווה נקראת ב naming של JavaBeans, כלומר: ()getXxxx או ()setXxxx – קוטלין מתייחסת אליהם כתכונה בשם xxxx.
 
הנה הקוד בקוטלין שקורא לקוד הג'אווה הנ"ל:
 
 
אתם רואים שהשלפנים (getters) שכתובים בג'אווה נראים בקוטלין כמו תכונות לכל דבר.
מכיוון ש true היא מילה שמורה בקוטלין, יש לעטוף (escaping) אותה בגרש מוטה.
 
באופן סימטרי, תכונות (properties) שהוגדרו בקוטלין כ yyyy יופיעו בקוד הג'אווה כמתודות ()getYyyy ו/או ()setYyyy.
 
 
כדרך אגב, יכולת ה escaping של שמות בקוטלין – מאפשר לתת שמות קריאים יותר לפונקציות של בדיקות:
 
 

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

 

חשיפה מתוכננת

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

אם יש לי תכונה בשם yyyy בקוטלין (מה שטבעי בקוטלין), הגישה אליה תהיה בעזרת getYyyy ו setYyyy – מה שטבעי בג'אווה.

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

אאוץ. אאוץ!!

הנה רשימת בעיות:

  • כאשר אני קורא לתכונה now מג'אווה – שם הפונקציה מופיע כ ()getNow, ומסיבה כזו או אחרת אני רוצה להשתמש בשם now כ field.
  • המילה transient היא מילה שמורה בג'אווה – אך לא בקוטלין. אי אפשר לקרוא לפונקציה הזו מתוך ג'אווה, ואין escaping בג'אווה המאפשר להשתמש בשמות שאינם תקינים בשפה.
  • אני לא יכול ליהנות מהערך ברירת המחדל של המתודה repeat. אין קונספט של default value בג'אווה – ולכן אני נדרש לשלוח את שני הפרמטרים בכל קריאה. בריבוי קריאות – זה יכול להיות מעצבן!
  • יצרתי companion object על מנת "לחקות" מתודות סטטיות בג'אווה – אבל הדרך לקרוא ל foo היא באופן: ()KotlinProducer.Companion.foo. מסורבל!

מה עושים?

הנה הפתרון, מבוסס annotations – אך עובד:

 

JvmOverloads היא הנחיה להשתמש ב default values על מנת לג'נרט מופעים שונים של פונקציות בקומבינציות השונות, מה שנקרא בג'אווה Method Overloading. אני מניח ששאר ה annotations הן self-explanatory.

הוספתי גם דוגמה לשימוש ב extension function. איך משתמשים ב extension functions מתוך ג'אווה?!

הנה קוד הג'אווה שמשתמש בקוד הקוטלין, "בהנאה":

 

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

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

 

סיכום

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

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

ה interoperability בין ג'אווה וקוטלין פשוט עובד!

יצא לראות לא מעט קוד קוטלין (צד-שרת) שעובד:

  • בצורה אינטנסיבית עם ספריות של ג'אווה.
  • ספריות ותיקות, שנכתבו לג'אווה – עוד לפני שקוטלין הייתה מעניינת.
  • ספריות שמבצעות reflection והורשה לקוד הקוטלין שנכתב (למשל: JDBI, Guice, או Jackson שמקודד עשרות רבות של מחלקות ל json ובחזרה לקוטלין)
  • והעבודה הייתה בסה"כ חלקה מאוד!
    • במקרים מעטים היה צורך / או היה יפה יותר להשתמש בכלים שסיפקתי בפוסט הזה.
    • במקרים מעטים נאלצנו לכתוב קוד "java-like" בקוטלין, על מנת שדברים יעבדו. עם הזמן צצו wrappers לקוטלין שהקלו על הדברים, ואפשרו להשתמש בסגנון "קוטליני" בחופשיות.

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

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

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

– אבל גם שפה שהגיל ניכר בה:

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

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

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

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

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

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

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

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

לעשות שינוי

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

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

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

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

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

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

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

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

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

חבלי מעבר

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

לא? … נסו שוב.

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

המעבר היה מתבקש, מכיוון שהיה כמו תכונות "ישנות" של פייטון שכבר היו "בלתי-נסבלות":

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

יש פתרון!

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

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

במקרה שלנו: נספק שני סוגי סיומות קבצים:

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

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

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

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

תוכנית עבודה

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

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

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

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

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

אף אחד מקהילת הג'אווה הרשמית לא הולך לומר לכם ששפת קוטלין היא בעצם Java.V3.

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

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

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

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

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

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

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

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

בקיצור:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

אשמח לתגובות 🙂

להבין Dependency Injection [מעודכן]

עדכון: ספטמבר 2014 – הפוסט חודש כליל. הפוסט המקורי פורסם בספטמבר 2012.

לשימוש ב Dependency Injection (בקיצור: DI) יש שני מניעים עיקריים:

  • הקפדה על ה Single Responsibility Principle[א] (בקיצור: SRP).
  • ביצוע בדיקות יחידה / בדיקות אינטגרציה.
לעתים, השימוש ב DI הופך מוטמע כ\"כ עמוק בפרויקט / בתהליכים (״התשתית שלנו היא Spring״) – עד שניתן לשכוח מהיכן הכל התחיל. כאשר מקפידים מאוד על שימוש ב DI, אך מחלקות בפרויקט עושות מטלות שונות – זה נראה סוג של \"פספוס\": זה כמו להאכיל את הילדים בקינואה וברוקולי בצהריים, אבל צ\'יפס וקרמבו בשאר הארוחות. מותר… אבל לא מומלץ.
DI הוא גם פתרון ממשי ומעשי לבעיות שצצות בביצוע בדיקות יחידה, ואפילו יותר: בביצוע בדיקות אינטגרציה. אני עבדתי עם קבוצות פיתוח שהוסיפו DI בכדי לבצע בדיקות יחידה, וללא קשר ל SRP (ל SRP היה לנו כלי אחר).
כיום, נראה שביצוע בדיקות יחידה/אינטגרציה הוא יותר נפוץ מהקפדה מלאה על SRP. רוב הארגונים עושים היום בדיקות יחידה, אבל חלק קטן מהם (מניסיוני האישי) באמת מקפיד באמת על SRP. ויש עוד אחוז מסוים של ארגונים שמשתמשים בכלי DI – \"כדי שיעשה סדר\", אך ללא יכולת להסביר מהו בדיוק הסדר הזה שהם מצפים לו וכיצד הוא יקרה. לצפות ל\"פחות באגים\" בעקבות שימוש בכלי DI זה נחמד, אבל ניתן באותה מידה לצפות לכזו תוצאה משימוש ב Log4J… ללא שימוש נכון ויעיל – הציפיה היא תקוות שווא.

בפוסט זה אני מנסה גישה חדשה: פרסמתי את דוגמאות הקוד (העובדות) מהפוסט בגיטהאב: https://github.com/baronlior/di-post
בואו נראה איך זה עובד…

DI בהיבט של SRP

בואו נתבונן על הקוד הבא:

הממשק

ספק השירות

צרכן השירות

הנה השקענו, בהגדרת Interface (ע\"פ עקרון ה Program to an interface), בהפרדה בין נותן שירות לצרכן השירות. הפרדה זו ניתן לקטלג כ\"ניהול תלויות\" / \"Low Coupling\" – ערכים בסיסיים בהנדסת תוכנה.

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

אמנם המשתנה המקומי artist הוא מסוג הממשק Artist – אך השימוש ב new מחייב שיהיה import ותלות למחלקה הקונקרטית. זרעי הפרת-התלויות – נזרעו.

מדוע הפסימיות? המתכנת ישתמש בממשק מבלי קשר ל imports הזמינים…

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

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

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

מלבד החשש לטשטוש הגבולות שהוגדרו, יש פה עניין של \"קוד מיותר\".
יצירת הקשרים בין המלקות הוא לא חלק מה \"Single Responsibility\" של המחלקה – ופשוט עדיף שלא יהיה שם. עדיף ליצור מחלקה מיוחדת (למשל: Factory) שזהו ייעודה בחיים.

איך אפשר לעשות זאת?
– פשוט \"נעיף\" את ה import של המחלקה הקונקרטית מצרכן השירות ונוסיף מחלקה שהכרת המחלקות הקונקרטיות הוא ייעודה. Repository בו מחלקות יכולות להירשם, ולמצוא אחת את השנייה ע\"פ \"סימנים\" מסוימים (שם, שם התפקיד, ממשק, וכו\'). נקרא לו Service Locator:

דוגמא לשימוש ב Service Locator

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

עם השנים התפתחו ספריות של Service Locator שמקלות על המפתח, ועם הזמן החליטו להחליף את סדר האחריויות: במקום שמחלקה תגדיר למה היא זקוקה (\"the artist\"), מחלקת ה Service Locator תתפוס פיקוד והיא \"תזריק\" לצרכן השירות את מה שהוא צריך. מימושים אלו נקראו בהתחלה \"Inverse of Control\" (בקיצור: IoC), עד שמרטין פאוולר טבע את המונח \"Dependency Injection\" (בקיצור: DI) אותו אנו מכירים היום.

הנה דוגמה לשימוש ב JSR-330 (הסטנדרט של ג\'אווה ל DI):

ה Annotation של Inject@ מסמנת שלבנאי של Michelangelo \"יוזרק\" המימוש הקונקרטי של המחלקה מסוג ה Painter לה הוא זקוק – מה שמותיר את הקוד של Michelangelo נקי למדי מכל עניין של ניהול הקשרים בין המחלקות.

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

מחלקה זו אינה מוגבלת בתלויות שלה – התפקיד שלה הוא \"לשאת בנטל\" התלויות של האפליקציה.

הקישור בין Inject@ והפונקציה ()bind נעשית בספריית ה DI בשם Google Guice – ה Reference Implementation הרשמי של JSR-330.

אי אפשר לא להזכיר את Spring – הספרייה שבעצם הפכה את DI לכ\"כ נפוץ, והופיעה זמן מה לפני JSR-330 או Guice. ספרינג היא עדיין כנראה ספריית ה DI הנפוצה ביותר בעולם הג\'אווה.

ההיסטוריה של ה DI/IoC. מקור

הערה: בעוד המינוח \"Dependency Injection\" היה ברור יותר מ \"IoC\" המעורפל-משהו, בד בדד הוא כנראה הותיר גם לאנשים מספיק מרחב לדמיין מהי \"הזרקה\". הזרקה, למשל, יכולה להיות גם השמה ב Reflection של Private member של מחלקה, לא? למשל כך:

זהו תחביר מקוצר לדוגמה האחרונה. במקום לקבל את הערך כפרמטר לבנאי המחלקה, ושם להציב אותו כ private member – אנו פשוט מציינים את ה Inject@ ישירות על ה private member של המחלקה.

מצד אחד חסכנו שורת קוד משעממת, מצד שני \"עקפנו\" את ה encapsulation של ג\'אווה והתרנו לגשת ל private member של המחלקה.

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

  1. טשטוש הגבולות של מושג ה private בג\'אווה.
  2. בעוד בנאי עם הרבה (נאמר 10) פרמטרים יציק לנו, וידרבן אותנו לנהל תלויות בצורה קפדנית / לחלק את המחלקה לכמה מחלקות קטנות יותר ועם התמחות מוגדרת יותר – (מניסיון) לא כ\"כ מפריע לנו שיש מחלקה עם 10 פרמטרים עליהם יש Inject@.
  3. שימוש מוגבר ב class members ולא משתנים עם scope מצומצם יותר – כי כ\"כ קל להגדיר בדרך זו משתנים.

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

ריבוי צורות (Polymorphism)

אם שמתם לב, הדרך להגדיר קשר בין הממשק למחלקה הקונקרטית (פקודת ()bind) היא על בסיס הטיפוס (class) בג\'אווה. בתחילת הדרך נהוג היה דווקא לרשום מחלקה תחת שם (מחרוזת מסוימת) ואז לבקש אותה בחזרה ב Service Lookup / בהזרקה. עם הזמן הגיעו ל 2 תובנות:

  • שם המחלקה הוא כנראה התיאור הברור והצפוי ביותר שלה.
  • השימוש ב class object בשילוב עם Generics מאפשר ליצור ספריית DI / Service Lookup ללא הצורך המרגיז בביצוע downcasting.
מה קורה כאשר רוצים להשתמש בריבוי צורות?

ברור לכם שברגע שהמיפוי הוא לא חד-חד ערכי, ה framework לא יצליח לבצע resolving נכון ב runtime. יש frameworks שמתירים מצבים שהם לא חד-חד ערכיים, בהנחה שהם יצליחו לנחש נכון, ויש כאלו שפשוט זורקים שגיאה בזמן ריצה, למשל: \"binding xxx was already configured\".

מה עושים? יש לכך כמה פתרונות (ל Spring Framework, למשל, יש כמה דרכים משלה), אך הפתרון המקובל ב JSR-330 הוא להוסיף Qualifiers (מעין תגיות) על ה binding ועל הבקשה (ועל צרכן השירות – בהתאמה), וכך לבצע את ההתאמה: התאמה ע\"פ תיאור הכוונה.

רישום של 2 מימושים לממשק Painter, תחת שמות שונים

הנה המימוש המחלקה Michelangelo שמקבלת שני מימושים שונים של Painter:

הדוגמה היא רק בכדי להמחיש את נושא ה DI

DI בהיבט של בדיקות יחידה/אינטגרציה

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

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

ראינו את גישת ה Qualifiers של DI, שהתאימה לנו עבור מקרים של Polymorphism אך לא תתאים למקרים של בדיקות. צרכן השירות לא יכול לבקש משהו אחד בזמן הרצה רגילה, ומשהו אחר בזמן בדיקות. הוא לא אמור להיות מודע לסוג ההרצה שהוא עובד (אבוי אם כן…).

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

אזור ירוק – הכנסת הסביבה הבדיקה; אזור הכתום – הרצת הבדיקה.

  1. אנו בודקים את מחלקת ה Artist ולכן צריכים מופע שלה. כדי שה DI Framework יוכל לעבוד, אנו צריכים ליצור את Michelangelo בעצמו בעזרת ה Framework.
    יש פה הזרקה ל member. מה שאני לא מרשה לעצמי לעשות בקוד production, אני מרשה לעצמי לעשות בקוד של בדיקות – כדי לכתוב קוד קצר יותר.
  2. אני יוצר mock ל Painter. לא רציתי להתעסק עם ספריית mocking (כמו Mockito או PowerMock) ולכן יצרתי mock כ Anonymous Class.
  3. פונקציה זו היא בעצם inline של הקונפיגורציה. בעוד שבקוד production אנו רוצים להפריד את ה wiring מהקוד, דווקא בבדיקה ה wiring הוא חשוב להבנת הבדיקה, ולכן אני מעדיף שיהיה באותו הקובץ. שוב השתמשתי ב Anonymous Class.
  4. זו הבדיקה עצמה, הפעלה של המתודה ()createArt מול התוצאה הצפויה (הבדיקה עוברת).

סיכום

בפוסט זה סקרנו את נושא ה Dependency Injection ושורשיו, וניסינו להפוך \"שימוש אוטומטי\" בכלי DI, לשימוש מושכל יותר.

מה השתנה בשנתיים האחרונות שגרם לי לחדש את הפוסט?
אני חושב שבאופן אישי יצא לי להיחשף בעבר ל DI בעיקר ככלי לביצוע בדיקות יחידה – את ניהול התלויות העיקרי עשינו במערכת (הישנה) על גבי JNDI של JEE (סוג של Service Locator). את תמונת עולם זו שיקפתי בפוסט בצורה דיי נחרצת – כפי שהגיב והעיר ויטלי.

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

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

  • אין צורך ב Framework בכדי לבצע DI. בסה\"כ, DI הוא רעיון, וה Frameworks השונים רק יוצרים אוטומציה של התהליך.
  • באוטומציה (או Framework) יש יתרון שהוא גם בעיה: היא חוסכת מאיתנו את הצורך בהבנה עמוקה. ללא הבנה עמוקה ניתן לשכוח מדוע אנו עושים מה שאנו עושים (במקרה שלנו – DI) – ולפתע למצוא את עצמנו עושים משהו אחר מהכוונה המקורית.
    אני נוטה בשנים האחרונות להטמיע רעיונות ראשית ללא Framework, בכדי להבין לעומק את הנושא – ורק בסיבוב השני להוסיף את ה Framework שמקל על העבודה. זה כמו ללמוד \"חשבון\" (מתמטיקה בסיסית) עם מחשבון ביד מהרגע הראשון: האם לא עדיף ללמוד כמה חודשים ללא מחשבון – ורק אז להוסיף אותו?
  • DI הוא לרוב רק חלק קטן במערך המודולריזציה של המערכת. שימוש ב DI לא מבטיח איכות או מודולריות, בפני עצמו. הוא רק כלי (נוסף) להשיג מטרות אלו.
  • אני לא שולל את הרעיון של אי-שימוש ב DI. למשל: להפסיק ליצור interface לכל מחלקה (מלבד מקרים של ריבוי צורות), להסתמך על public vs. private להגדרת הממשק, ולצמצם את השימוש ב DI למינימום האפשרי (למשל: עבור בדיקות אינטגרציה, או ניהול תלויות ברמת השירותים בלבד). בסופו של דבר אנו משלמים לא מעט עבור ההגנה שמספקת גישת ה Program to Interface (בצורה הדקדקנית שלה) וצוותים מסוימים / פרוייקטים מסוימים – יכולים להסתדר מספיק טוב עם פחות הגנות, ועדיף לתת להם להתקדם מהר יותר. 
את דוגמאות הקוד כתבתי ב Guice ולא בספריה הנפוצה יותר Spring Framework בעיקר בגלל ש Guice מצומצם ופשוט יותר. ספרינג מספקת הרבה מאוד יכולות – שיכולות מאוד שימושיות למערכת גדולה ומסובכת, אך לא היו תורמות דבר להבנת הרעיונות הבסיסיים.
שיהיה בהצלחה!

—-

[א] עקרון זה מוכר גם כ Separation of Concerns או Segregation of Duties. הכוונה היא שכל יחידת קוד (פונקציה, מחלקה, מודול) יעשו דבר אחד, ודבר אחד בלבד. צד שני של אותו המטבע הוא שכל אחריות תהיה בידיים של יחידת קוד אחת גם כן. כלל זה עוזר ליצור סדר במערכת, ולוודא שמי שכותב את הקוד יראה לנגד עיניו את התמונה המלאה ולא חלקים ממנה (כי כל האחריות נמצאת באותו המקום).

קישורים:

DI קליל ברובי
הפסיכולוגיה של בדיקות תוכנה – מישקו הארווי (היוצר של AngularJS ו jsTestDriver).