להבין Maven (הצצה ראשונית)

מייבן (Maven, מלשון \”מבין\” ביידיש) היא ספריית build ו dependency management לסביבת ה JVM, וג\’אווה בפרט.

מייבן איננה חדשה, והיא איננה הטרנד החם בתחום – אך היא עדיין נפוצה מאוד. ע\”פ סקר מ 2013 – כ 70% ממפתחי הג\’אווה משתמשים בה.

אמנם ל Gradle (ספרייה מתחרה, שהיא הטרנד החם בתחום) יש כמה יתרונות מובנים על פני מייבן (קלות תחזוקה: DSL מול XML + מהירות ריצה) – אבל ייקח עוד זמן עד שהיא תוכל להחליף את מייבן: ל Gradle אין עדיין repositories משלה, וכמות ה plugins הזמינים עבורה – נמוכה בהרבה. היא גם מתבססת על Groovy – שפה עם קהילה לא כ\”כ גדולה [א].

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

בואו נתבונן לרגע על מרחב הכלים הקיים.

יש כמה וכמה כלי Build נפוצים:

  • Make של C או ++C ו Unix (שנת 1977)
  • Ant של ג\’אווה (שנת 2000)
  • מייבן של ג\’אווה (שנת 2002)
  • Rake של רובי (לא יודע ממתי בדיוק)
  • SBT – קיצור של Simple Build Tool, של סקאלה (שנת 2008)
  • Gradle לג\’אווה / גרובי (שנת 2012)
  • Grunt ו Gulp לג\’ווהסקריפט (חדשות למדי).
ויש גם כמה כלים לניהול תלויות:
  • מייבן – הכוללת גם כלי ניהול תלויות. אולי הראשון מסוגו.
  • Ivy (שנת 2007) – קיימת כתת פרויקט של Ant, אך יש לה זכות קיום עצמאית. משמשת את SBT ואולי גם כלים אחרים.
  • Gradle – התחילה כתלויה ב Ivy, אך פיתחה עם הזמן מערכת ניהול תלויות עצמאית.
מערכות לניהול תלויות הן מערכות דומות מאוד ל package managers, כמו אלו של לינוקס, npm של node.js, או bower של ספריות ג\’אווהסקריפט. ההבדל הוא שהן מנהלות source code ולא code ל production (כך שאין צורך לבצע התקנה), ושיש להן אינטגרציה לכלי build: כאשר ב build מסומנת תלות בספרייה שלא קיימת – כלי ה build יוריד אותה בצורה אוטומטית, כך שה build יוכל להסתיים בהצלחה.

מה מייבן מספקת, ובמה היא טובה מ Ant?

קרן Apache מנהלת גם את Ant וגם את Maven. מדוע לנהל 2 ספריות מתחרות? מה מייבן (המאוחרת יותר) מנסה לעשות ש Ant לא עשתה?

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

לסקריפט (קובץ build.xml, במקרה של Ant) יש כמה יעדים (Targets / Goals) שנקראים לרוב משהו כמו: build, clean, jar ו test – כ\”א הוא תיאור של סדר הפעולות האטומיות (העתקת קבצים, קומפילציה וכו\’) הנדרש להשגת מטרה זו. בין ה targets השונים ניתן להגדיר תלות, כך שהפעלת אחד (למשל: אריזה ב jar) – תפעיל גם את התהליך בו היא תלויה (למשל: compile). מנגנון התלויות בין ה targets מאפשר לנו לעשות שימוש חוזר בסקריפט ה compile גם ב targets כמו jar או install.

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

דוגמה ל Ant Task פשוטה

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

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

  • יש לקמפל את קבצי ה java לקבצי class ולשים אותם בתיקיה זמנית.
  • מקמפלים ומריצים את הקוד של בדיקות היחידה.
  • במידה והבדיקות עברו בהצלחה – בונים jar או war.
  • מנקים את הקבצים הזמניים שנוצרו.
מדוע לכתוב את ה Script הזה כל פעם מחדש? האם האנושות לא יכולה לחסוך לעצמה את \”המצאת גלגל ה build\” – בכל פעם מחדש?

מייבן מספקת Archetypes (מעין templates) של פרויקטים נפוצים: פרויקט jar, פרויקט ווב, פרויקט ווב של backbone וכו\’. שימוש ב Archetypes חוסכת הן עבודת קידוד והן עבודת תכנון – כיצד להרכיב את פרויקט ה build, באיזה מבנות תיקיות להשתמש וכו\’.

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

כל פעולות ה build עצמן (יצירת תיקיות, העתקת קבצים, הפעלת קומפיילר וכו\’) מגיעות כ Plugins – וניתנים להחלפה. מייבן מספקת את המודל, קובץ הקונפיגורציה של מייבן מתאר את ה Strategy (ה design pattern), וה plugins עושים את העבודה.

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

מרגע שמבנה הפרויקט ידוע וקבוע – ניתן לספק הוראות הרבה יותר high level מאשר ב Ant: \”בצע קומפילציה\” במקום \”צור תיקיה זמנית x\”, \”הפעל את javac עם target של x\”, \”העבר קבצים מתיקיה x לתיקיה y\”, וכו\’.

פרויקט פשוט במייבן

יצירת פרויקט של מייבן מתוך Archetype (להזכיר: סוג של template של פרוייקט) יכולה להיעשות בעזרת command line, אבל בפועל כיום יש אינטגרציה לכל IDE נפוץ – כך שאין כ\”כ צורך להכיר את ה command line. הבה נבחן את מבנה התיקיות של הפרויקט הגנרי של מייבן:

כמה הסברים:

  • תיקיית src נועדה לקוד, ותיקיית target לתוצרי קומפילציה (קבצי class., למשל). 
  • תיקיית ה src/main נועדה לקוד שמיועד לדילוור, ותחתיה יש תיקיה לכל שפת תכנות. יכולה להיות, למשל, תיקיה ל java, ל scala ול javaScript. ספריית ה src/test נועדה לקוד של בדיקות יחידה / אינטגרציה. בפנים יש תיקיות של ה java packages – כמו בכל פרויקט java רגיל.
  • תיקיית src/main/resources/ נועדה לקבצים אחרים של הפרויקט שאינם קוד, למשל קבצי הקונפיגורציה של Spring Framework.
  • את תיקיית ה target מחלקים לתוצרים של קוד שהולך לדילוור (classes) ותיקיית הקוד שלא הולך לדלוור (test-classes).
  • pom.xml הוא קובץ הקונפיגורציה של מייבן, המקבילה של build.xml של Ant.

קובץ ה pom.xml (קיצור של Project Object Model) הוא קובץ XML, דקלרטיבי, שמגדיר את ה Strategy של תהליך ה build שלנו.

הנה דוגמה ל pom.xml מינימלי ביותר:

הסברים:

  1. כל פרויקט מייבן מוגדר באופן ייחודי ע\”י שלושה פרמטרים:
    1. groupId – סוג של namespace שאמור להיות globally unique. בד\”כ כתובת אתר האינטרנט של החברה שלכם בסדר הפוך של tokens.
    2. artifactId – שם ייחודי, פנימי אצלכם, לפרויקט.
    3. מספר גרסה, בפורמט: ..-, כאשר ה qualifier הוא טקסט [ב].
      ל qualifier בשם \”SNAPSHOT\” (אותיות גדולות) – יש משמעות מיוחדת, והיא משמשת לציין גרסה שעדיין בפיתוח שמשתנה כל הזמן. במקום לבדוק אם הגרסה התעדכנה בעזרת מספר הגרסה, הוא יבדוק את התאריך, כך שהמפתח לא נדרש לשנות כל רגע מספר גרסה בכדי שחבריו יקבלו גרסה עדכנית בכל build.
  2. הגדרה של תלות בספריה JUnit. כ default קיבלנו את גרסה 3.8.1 – אך אנו יכולים להחליט שאנו רוצים לעבוד עם JUnit 4. שימו לב שגם ספרייה זו מזוהה בעזרת השילוש הקדוש: קבוצה, artifact וגרסה. זהו הזיהוי הייחודי ב repositories של מייבן.
  3. זוהי תצוגת ה IDE למייבן של IntelliJ – בכל IDE יהיה משהו שנראה אחרת. אנו רואים את השלבים המרכזיים בתהליך הבילד כפי שהוגדר.
שנייה! לא הגדרנו כמעט כלום בקובץ ה pom.xml שלנו. מאיפה מגיעות ההגדרות הללו?
אם אנסה להריץ אחד השלבים, למשל test, מייבן יוריד לי ערמה של maven plugins הנדרשים להרצה – ויריץ קומפילציה ובדיקות בהצלחה. כיצד הוא יודע מה לעשות?
פרויקטים של מייבן מוגדרים בדלתאות (deltas): ההגדרה הבסיסית נמצאת ב super pom.xml – קובץ שנמצא בתוך אחד ה jars של מייבן עצמו (ואפשר, אך לא כדאי, לשנות אותו). הגדרות נוספות מגיעות מה settings של מייבן או מחושבות בזמן ריצה (כמו \”תיקיה נוכחית\”), אח\”כ מ pom.xml אבות של ה pom.xml המדובר (בפרויקט גדול – מרכיבים קבצי pom.xml בהיררכיה), ואז לבסוף, מה pom.xml הנוכחי – שבמקרה שלנו הוא כמעט ריק.
במקרה שלנו אין pom.xml אב, והקובץ שלנו כמעט ריק – אז ההתנהגות בה אנו חוזים מגיע בעיקר מה super pom.xml ומה settings. הרכבה של כל השכבות המוגדרות בקובצי ה pom.xml הרלוונטיים השונים נקראת effective pom.xml, ניתן לראות  אותה ב eclipse בתוך ה IDE. ב Intellij אני לא מכיר דרך לראות אותה, ולכן אני מריץ את ה command line הבא:

mvn help:effective-pom -Doutput=effective-pom.xml

דוגמה ל Effective-pom.xml מינימליסטי

הנה ה effective-pom.xml שנוצר לנו. בואו נבחן אותו. שימו לב שכמה חלקים הם collapsed:

  1. ה dependencies היחידים הם אלו שהגיעו מה pom.xml שלנו – תלות ב Junit 3.8.1
  2. ה repositories וה pluginRepositories כרגע הם ה central repository של מייבן, קרי http://repo.maven.apache.org/maven2.
    ה repositories מכילים הרבה (מאוד) פרויקטי open source ו/או maven plugins – אותם מייבן ידע להוריד אלינו למחשב ע\”פ הצורך. כאשר עובדים ב IDE וזקוקים לאיזו ספרייה – פשוט מוסיפים אותה כתלות ב pom.xml ומייבן יביא אותה לבד ב build הבא. אם הספרייה שציינתם תלויה בספריות אחרות – מייבן תביא גם אותן. כמו כן – אותה הורדה תתרחש גם אצל מפתחים אחרים בצוות. זה היופי של ניהול התלויות של מייבן.
  3. כאן ניתן לראות את מבנה הספריות של הפרויקט, כפי שתיארנו אותו קודם (כ full paths). מייבן משתמש ב super pom.xml במשתני-סביבה בכדי להגדיר את הנתיבים, וה effective-pom.xml כבר מכניס בהם את הערכים.
  4. כפי שציינו, plugins הם אלו שעושים את כל עבודת ה build בפועל. ניתן לראות בדוגמה למעלה שני core plugins שמתארים את ההתנהגות של שלבי ה clean וה install של מייבן.
    Plugins אחרים שלא נכנסו לצילום המסך הם:
    1. maven-resources-plugin
    2. maven-surefire-plugin – הפלאג-אין של מייבן להרצת בדיקות-יחידה. אין לי מושג למה הוא קיבל \”שם מגניב\”, ורבים אחרים – לא.
    3. maven-compiler-plugin
    4. maven-jar-plugin – כפי שהשם מצביע, הוא פשוט אורז קובץ jar.
    5. maven-deploy-plugin
    6. maven-site-plugin – הפלאג-אין של מייבן ליצירת תיעוד לפרויקט

בהמשך, נרחיב עוד על Plugins והקונפיגורציה שלהם.

Build Lifecycles

המודל של מייבן מגדיר 3 פעולות שמייבן יודע לעשות:
  • לבנות תוכנה (ואולי גם להתקין אותה) – מה שנקרא ה default lifecycle
  • לנקות שיירים (קבצים זמניים וכו\’) מבנייה קודמת (בעקרון: ספריית ה target) – מה שנקרא clean lifecycle
  • בניית תיעוד לתוכנה (יצירת מערכת דפי html) – מה שנקרא site lifecycle, כלומר אתר אינטרנט (סטטי) הכולל את התיעוד של התוכנה.
כל אחד ממחזורים אלו בנוי מרשימה מוגדרת-מראש של שלבים (phases).
המשתמש יכול לבחור לבצע רק חלק מהמחזור שהוגדר ע\”י מייבן. למשל: להפעיל את ה default lifecycle רק עד שלב הקומפילציה. שלבים מוקדמים יותר במחזור, כגון validation של פרויקט המייבן או יצירת ה resources הנדרשים – יופעלו, אבל השלבים המאוחרים יותר (כמו בדיקות או התקנה) – לא יופעלו.

שלבי ה Lifecycle השונים של מייבן, כאשר השלבים החשובים / היותר שימושיים – מוגדשים

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

mvn clean

מה שיגרום למייבן לזהות שזהו שלב (phase) של ה clean lifecycle ולהפעיל את ה livecycle הזה עד שלב ה clean. שלב ה pre-clean יתבצע, אך שלב ה post-clean – לא יתבצע. כמובן ששלבים רבים במחזורי הפעילות של מייבן הם שלבים ריקים – אלא אם נגדיר בהם משהו.

בכדי להפעיל את כל ה clean lifecycle יש להקליד mvn post-clean. בד\”כ אנו מסתפקים ב clean, הקצר יותר להקלדה.

האם איי פעם תהיתם מדוע אתם מקלידים (הפקודה הכי נפוצה אולי) \”mvn clean install\”,
אבל לא \”maven clean compile install\”, או משהו דומה?

התשובה עכשיו ברורה: clean נדרש מכיוון שהוא שלב ב lifecycle שונה מה default lifecycle. כאשר מפעילים את mvn install – הוא יבצע את כל 20 ומשהו השלבים מ validate ועוד install. הוא רק לא יעשה deploy.

הנה כמה מלים על מה שמייבן עושה בשלבים השונים של ה default lifecycle:

  • validate – מוודא שפרויקט המייבן תקין, למשל: ולידציה של ה pom.xml, שכל המשתנים שבשימוש – מוגדרים, וכו\’.
  • generate sources / resources – שלבים שהוגדרו בכדי לשמש לשלבי pre-proccesing להתרחש (במידה ואתם משתמשים בכלים שמג\’נרטים קוד או resources).
  • compile – קומפילציה של קוד תחת ספריית main (לא כולל קוד של בדיקות)
  • process-classes – שלבי post processing על קבצי ה class. שקומפלו, למשל \”אריגה\” של AspectJ על קבצים שכבר קומפלו (יש אפשרות כזו).
  • test-compile – מקמפל רק את קבצי הבדיקות. אם אתם לא מתכוונים להריץ בדיקות – חבל על הזמן לקמפל את קוד הבדיקות, לא?
  • package – אריזת הקוד ל jar, war, ear וכו\’.
  • integration-tests – שלב מיוחד של הרצת בדיקות על מערכת \”חיה\”. מתקין את ה deployable שכרגע נארז על מערכת בדיקות, ומריץ מולה בדיקות אינטגרציה / מערכת (איך שאתם קוראים להן). ה plugin של מייבן שמריץ בדיקות אינטגרציה נקרא \”failsafe\”.
  • verify – שלב בו ניתן לבצע בדיקות נוספות על ה package הארוז – לוודא שהוא תקין.
  • install – השם של השלב הזה הוא מעט מבלבל: ההתקנה היא של ה deployable ל local maven repository – ולא לשרת ה production כמו שאולי זה נשמע. ה local repository הוא צד של מייבן שעדיין לא נגענו בו בפוסט זה. נאמר כרגע שזו איזה תיקייה של מייבן בה הוא שומר deployables, pugins ו ספריות שנדרשות בגלל התלויות.
  • deploy – עושה את מה שאפשר לחשוב: מתקין את ה deployable על שרתי ה production.

סיכום

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

  • מהם בדיוק ה repositories?
  • כיצד ה plugins משתמשים בשלבים השונים שמייבן מגדיר? מהם ה goals?
  • ואולי הכי פרקטי: כיצד משנים את הגדרות ה pom.xml ורותמים את מייבן לצרכים הייחודיים של הפרויקט שלנו ?
אני מקווה לכסות לפחות כמה מנושאים אלו בפוסט המשך.

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

[א] Groovy היא (מלבד ה nested classes) בעצם superset של ג\’אווה. לכאורה, מאוד קל למתכת ג\’אווה לעבור אליה: לשנות את סיומות הקבצים ל groovy. ולהימנע משימוש ב nested classes. בפועל, כמעט כל דוגמאות הקוד של gradle משתמשים בתחביר מתקדם של שפת Groovy – שיהיה זר ומוזר למתכנת ג\’אווה שלא ישקיע זמן ללמוד אותו ואת הדקויות שלו.

[ב] בגלל שמייבן מתייחס ל qualifier כטקסט, יש פה pitfall מסוים:
גירסה:

0.9.9-CR10 

תחשב כמוקדמת יותר מגירסה:

0.9.9-CR2

(בגלל שהתו \”1\” נמוך בערך ה ASCII שלו מ \”2\”).

מפרשן מול מהדר – מה ההבדל?

למי שלא עשה קורס ב”קומפילציה” (ואולי גם למי שכן), עולה מדי פעם השאלה: “מה ההבדל בין מפרשן (interpreter) למהדר (compiler)?”
– שניהם הרי גורמים לקוד של תוכנה לרוץ על מכונה כלשהי.התשובה הפשוטה היא כזו:
מהדר (Compiler) – מתרגם תוכנה שנכתבה בשפה “קריאה לאדם”, כיחידה שלמה, לקוד בשפת מכונה (אפסים ואחדים) שמבוצע ישירות על המעבד.
מפרשן (Interpreter) – מריץ את התוכנה, שורה אחר שורה ו מפרשן כל שורה בנפרד. כל שורה עוברת תרגום “משפת אדם” לשפת מכונה – ואז מורצת, וחוזר חלילה.

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

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

מפרשן יכול לספק debugger בהשקעה אפסית כמעט – עוד יתרון. הקלות בכתיבת מפרשן יכולה להיות מתורגמת בקלות לכתיבת שפה cross-platform – יתרון נוסף.

כשעבדתי ב NICE, השקעתי כמה חודשים טובים בכתיבת מפרשן לשפה מוזרה ללא שם. זה היה אתגר מרתק ומעשיר, עד לשלב בו נתבקשתי לממש מצביעים (pointers). זה היה מספיק על מנת שאלחץ שוב, עד שהצלחתי לשכנע את המנהל שלי להשקיע כ 200$ ברכישת רישיון למפרשן JavaScript – שפה שמילאה בקלות אחר כל הצרכים והיה אלגנטית בהרבה (תארו לעצמכם).לכתוב מפרשן, זה בסה”כ דיי פשוט. קוראים שורה מקובץ (להלן ה”קוד”), מפרסרים מה כתוב שם (הכי פשוט שהפקודה היא המילה הראשונה) ואז מבצעים בשפה שלכם (#C למשל) את מה שרציתם לבצע. הדברים נהיים קצת יותר מורכבים כאשר יש משתנים, פונקציות, Scope ויותר. על המשמעות של הכנסת מצביעים אני לא יודע לספר.

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

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

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

נוסטלגיה. כנראה שרוב קורסי ה”קומפילציה” בארץ נעשו על בסיס ספר זה מ 1986. מקור: amazon.com

המצב בפועל
בואו נבחן כמה שפות מוכרות: האם הן שפות-מהדר או שפות-מפרשן?

שפת אסמבלי
בואו נתחיל מאסמבלי: טעות נפוצה היא לקרוא לשפת אסמבלי (assembly) בשם “אסמבלר”. אסמבלר הוא השם של המהדר שלה. האם האסמבלר “מתרגם תוכנה שנכתבה בשפה ‘קריאה לאדם’, כיחידה שלמה, לקוד בשפת מכונה (אפסים ואחדים) שמבוצע ישירות על המעבד”? כן. 
שפת אסמבלי היא בעצם ייצוג טקסטואלי, בעזרת מילים “בשפת אדם” כגון JMP ו MOV, לפקודות מכונה. האסמבלר פשוט עושה מין “Search and Replace” של “JMP” ל 00010010101101, למשל.

יש לי הסתייגות קלה מהגדרת שפת אסמבלי כ”קריאה לאדם”. אני חושב שכדאי מאוד לעשות את ההבחנה בין “Human Readable” לבין “Human Parsable”. ברוב הפעמים שמדברים על Human Readable (לדוגמה קובץ XML) רוצים לומר שזה טקסט שאדם יכול “לפענח”, לאו דווקא “לקרוא”.

שפת ++C
הנה דוגמה טובה למהדר “קלאסי”. בניגוד לאסמבלי, התרגום משפת ++C לשפת-מכונה הוא מורכב וכולל טרנספורמציות ואופטימיזציות רבות. הרבה דברים קורים מאחורי הקלעים. ניתן אפילו לומר ש ++C “שותל” מפרשנים קטנים בתוך הקוד (למשל לטיפול ב virtual functions) כך שמבחינה מסוימת יש במהדר של ++C אלמנטים של מפרשן!

שפת JavaScript
ג’אווהסקריפט הייתה במשך שנים דוגמה קלסית לשפת מפרשן. הסיבה העיקרית הייתה כנראה הרצון להריץ קוד שלה על סביבות שונות: כתוב את הקוד פעם אחת והוא ירוץ על ספארי (מק), FF (לינוקס) או IE (חלונות). אם הקוד קומפל לשפת מכונה אחת – הוא לא יתאים למכונה אחרת ולכן האפשרות היחידה היא לשלוח את קוד המקור למחשב עליו רץ הדפדפן – ושם לפרשן אותו.
בשנים האחרונות, הפכו נפוצים מהדרי JIT [ב] לשפת ג’אווהסקריפט. דוגמה בולטת היא מהדר ה JIT של V8 – מנוע  הגא’ווהסקריפט של גוגל. התוצאה: קוד המקור נשלח לדפדפן לפני ההרצה, אך הדפדפן מחליט להדר אותו ואז להריץ אותו בשפת מכונה – כך שבפועל כיום, JavaScript רצה מהודרת. הנה דוגמה ליתרון שבהידור. דמיינו את הקוד הנפוץ הבא:

var a.b.c.d.e.f.g.h.x = 100;
var a.b.c.d.e.f.g.h.y = 200;

כאשר a.b.c.d.e.f.g.h הם שמות namespaces או אובייקטים בג’אווהסקריפט. כאשר יש מפרשן, עליו לקחת את אובייקט a ואז לחפש בו property בשם b, ואז על b לחפש property בשם c וחוזר חלילה. ללא שיפורי ביצועים מיוחדים יכול להיות שמדובר בעשרות פעולות בזיכרון.
מהדר לעומת זאת קורא את 2 השורות ומבין שיש פה אופציה לייעל את הקוד. הוא יכול להפוך את הקוד ל:

var _p = a.b.c.d.e.f.g.h;
var p.x = 100;
var p.y = 200;

שיהיה יעיל בהרבה. מכיוון שג’אווהסקריפט היא שפה דינמית, עליו לוודא ש a עד h לא השתנו בעקבות ההשמה של x. כאן שוב יש מין “מיקרו מפרשן” שהמהדר מייצר ופועל בזמן ריצה.



שפת Java
Java היא שפת מהדר – נכון?
כן. בג’אווה יש מהדר שהופך קוד ג’אווה לקוד bytecode, אבל קוד ה bytecode עובר פרשון (מכיוון שהוא אמור להיות cross-platform). כבר מזמן יש JIT Compilers ל bytecode, כך שבפועל ג’אווה עוברת הידור ראשון בסביבת הפיתוח והידור שני על פלטפורמת ההרצה – מה שמתגלה כנוסחה יעילה למדי!
גם כאשר מהדרים קוד ++C למעבד מסויים (למשל מעבדי אינטל 64-ביט) יש לדגמים שונים יכולות מעט שונות – יכולות שלא ניתן לנצל בקוד המהודר מכיוון שעל המהדר לפנות למכנה המשותף הנמוך. הידור דו-שלבי כגון זה של ג’אווה מסוגל לבצע אופטימיזציות ייעודיות לחומרה הקיימת – ולנצל את כל היכולות / התכונות של החומרה.

שפת CoffeeScript
לקופיסקריפט יש מהדר, אך הוא עושה דבר מוזר: הוא מתרגם קופיסקריפט לג’אווהסקריפט. בשל הבא קוד הג’אווהסקריפט  (המהודר) נשלח למכונת משתמש-הקצה ועובר הידור ב JIT Compiler – כך שיש לו הידור כפול, אך קצת שונה משל ג’אווה מכיוון ששפת הביניים (intermediate language) היא ג’אווהסקריפט – שפה גבוהה בהרבה מ bytecode והיכולת לבצע אופטימיזציה בקוד – פחותה.
בפועל, אתם מבינים, יכולה להיות שרשרת דיי מורכבת של מהדרים ומפרשנים בדרכו של קוד תוכנה להיות מורץ. הדרך לתאר את כל האפשרויות הקיימות היא מורכבת, ויתרה מכך – כנראה לא מעניינת.


(DSL (Domain Specific Languages
סוג חדש של שפות שהופיע לאחרונה[ג] הן שפות מבוססות דומיין, כלומר שפות מתאימות לפתור בעיות מסוג מאוד מסוים: ויזואליזציה של נתונים בתחום מסוים, ביצוע שאילתות על נתונים, קביעת תצורה של Firewall וכו’.
לרוב לא מדובר בשפה חדשה לגמרי, אלא שפה ש”מורכבת” על שפה קיימת. אלו יכולים להיות פונקציות או אובייקטים שמתארים עולם מונחים שמקל על פתרון הבעיה ובעצם מהווה “שפה חדשה”.

לדוגמה: ניתן לחשוב על “שפה לחישוב שכר”. מי שמגדיר את החוזה (להלן ה”קוד”) יכול בעצם לכתוב בשפה גבוהה כמו שפת Scripting או קובץ קונפיגורציה, ולהשתמש במונחים שנכונים לעולם זה. הקוד יהיה קריא למדי למישהו מתחום השכר, גם אם איננו יודע תכנות.

האם שפת השכר שלנו, “P” נקרא לה, היא שפת-מהדר או שפת-מפרשן?
אם היא ממומשת מעל Groovy, למשל, אזי אפשר להתייחס אליה כסוג של שפה-מפורשנת (לגרובי).
כלומר: שפה-מפורשנת (ע”י גרובי), שעוברת קומפלציה (לבייטקוד) ואז מקומפלת (ב JVM JIT Compiler) לשפת מכונה שכוללת מיני-מפרשנים (שגם הם קומפלו בדרך לשפת מכונה). באאאאההה.

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

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

עדכון: הנה נתקלתי בבסיס נתונים שמהדר SQL ל ++C ואז לשפת מכונה.

—-

[א] זה כמובן איננו טיעון שפוסל שפות שירוצו על מפרשן. שפות מסוימות (בעיקר שפות gluing) לא זקוקות לפרשון מהיר. קחו לדוגמה שפות כמו Bash או Windows PowerShell – הן מפעילות תוכנות יעילות, אך אם לכל שורה שלהן לוקח עוד כמה מילי-שניות – אין לכך כל חשיבות.

[ב] (JIT (Just In Time הוא מונח של “ייצור רזה” שאומר “עשה את הפעולה רק מתי שצריך – לא לפני כן”. הרעיון הוא שאם עושים פעולה (כגון הזמנה של מלאי חדש למחסן) לפני הרגע האחרון – היעילות היא איננה מרבית. JIT Compiler הוא כזה שמקמפל את הקוד ממש לפני ההרצה – ולא לפני כן. יש בכך יתרונות וחסרונות – מצד אחד יודעים בדיוק את הסביבה בה הקוד עתיד לרוץ וניתן לבצע אופטימיזציות לפלטפורמה הספציפית (לדוגמה דגם המעבד), מצד שני כנראה שהמשתמש לא ימתין לאופטימיזיות קוד שאורכות זמן רב.

[ג] בעצם הוא קיים המון זמן, אבל המודעות לקיים – היא זו שהתפתחה לאחרונה + קיבלה fancy name.

טרנדים חמים בתחום ה Web וה Mobile – חלק 2

בהמשך לפוסט הקודם בסדרה, טרנדים חמים בתחום ה Web וה Mobile – חלק 1, הנה עוד טרנדים חמים בתחום הווב והמובייל:

Unit Testing ל JavaScript
אין פה משהו חדש לגמרי: מי שבאמת רצה, יכול היה לכתוב unit tests לקוד ה JavaScript עוד מזמן. עד לא מזמן Unit Tests היו נפוצים בעיקר בצד השרת ומעט מאוד בצד הלקוח. מה שהשתנה הוא הפיתוח הרב שנעשה למובייל, שעבורו פתרונות כמו GWT, פלאש או IceFaces – כבר אינם טובים מספיק.

העיקרון דומה למדי ל Unit Tests בצד השרת, מלבד העובדה שהשונות בין סביבות הריצה היא רעש לא קטן. בעוד שלג\’אווה יש JVMים רבים, יכולתי בתור מפתח לבדוק את הקוד שלי על JVM של Windows (רץ ב IDE וב CI) ולהניח שהוא ירוץ בצורה זהה על שאר הפלטרפורמות הנפוצות. את רוב ה JVMים הקיימים מייצרת סאן אורקל והם דומים למדי. במצב נדיר בו הקוד שלכם אמור לרוץ, נאמר, על Z/OS (מערכת הפעלה של יבמ למיינפריים) אזי עליכם להתמודד עם JVM של יצרן אחר (יבמ) וייתכנו שונויות. אני נתקלתי פעם אחת בחיי בקוד שמתנהג אחרת (סביב SoftReferences) בין JVM של סאן ל JVM של יבמ.

אם ניקח את Rhino (מנוע ה JavaScript שמגיע עם ג\’אווה) ונוסיף את Env.js, ספרייה טוב של ג\’ון ריזלינג (היוצר של jQuery) שמדמה את ה DOM של הדפדפן, נוכל לבצע Unit Testing שיבדקו בצורה דיי טובה את הריצה של הקוד שלנו ע\”פ התקנים הרלוונטים, ובצורה בינונית למדי את הריצה של הקוד על דפדפנים אמיתיים, בגרסאותיהם השונות.

ישנם 4 מנועי ג\’אווה סקריפט שמשולבים בדפדפנים נפוצים: V8 של גוגל, Spider Monkey של מוזילה, Chakra של מייקרוסופט ו SquirrelFish שרץ בספארי.
כל אלה שונים במידה מורגשת מ Rhino, ושונים גם אחד מהשני. Charka, הוא מנוע חדש שנכתב עבור IE9 והוא שונה מהמנוע של גרסאות קודמות (JScript Engine) שעדיין זמין על IE9 ב Compatibility Mode [א].

מה הפתרון?
ניתן להריץ כלי אוטומציה כמו Selenium על דפדפנים שונים כחלק מתהליך ה CI. רוצים להתקין מספר דפדפנים (בעיקר IE) על מחשב אחד? Spoon.Net הוא שירות מצויין שיעשה עבורכם את העבודה עבור תשלום של 60$ בשנה.
כלי אוטומציה פשוט ומגניב יותר שעושה את אותו הדבר הוא JSTestDriver. הוא \”משתלט\” על ה process של הדפדפן ולכן אמור להיות מהיר ואמין יותר מ Selenium שמתבסס על ה UI.
עבור מובייל, יש כאלו שבודקים רק על WebkitJSCore שכנראה מספיק דומה לV8 ברוב המקרים, וניתן להריץ אותו ללא הרצה של דפדפן. כלומר, הרבה יותר מהר. חפשו בגוגל \”Webkit headless\” כדי למצוא מגוון של פתרונות.

הנה עוד כמה ספריות שיעשו את פיתוח ה Unit Tests שלכם לקלים ומהנים יותר:
Jasmine – סביבת unit testing ו BDD, כנראה הנפוצה מכולן.
QUnit היא אלטרנטיבה מבית היוצר של ג\’ון ריזלינג. יש כאלו שאומרים שהיא קצת יותר טובה, אך הקהילה שלה קטנה יותר והשוני בקוד של הבדיקות הוא דיי זניח, לדעתי.
Mocha (מבטאים כמו \”מוקה\”) היא ספריית בדיקות נחשבת, עם הרבה פלאג-אינים ותמיכה ב Node (פרטים בהמשך).

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

בדיקות תוכנה: \”אני לא סומך על עצמי, אבל אני סומך על פראנק\”. מקור: youtube

Hybrid Web Container או בקיצור: PhoneGap
דילמה קשה ובסיסית של פיתוח למובייל הוא הבחירה בין פיתוח אפליקציית נייטיב (native) לבין אפליקצת ווב (כלומר HTML5 ו CSS).
לאפליקציית נייטיב יש יתרון ביצועים משמעותי, היא יכולה לגשת למצלמה ולרשימת אנשי הקשר ובעזרתה ניתן לייצר חווית משתמש \”יותר חלקה\”.
אפליקציית ווב מאפשרת לכתוב קוד פעם אחת ולהשתמש בו, עם התאמות קלות, במכשירים שונים. גם ההתאמה למסך בגודל שונה (טאבלט, Note) היא קלה יותר. אין API שיישבר בין גרסאות של מערכות הפעלה.

ישנה אופציה שלישית דיי פופולרית – ה Hybrid Web Container.

איך לצרוך PhoneGap – ה HWC הנפוץ. מקור: האתר של phoneGap

הרעיון הוא דיי פשוט: כתבו אפליקציית HTML רגילה. ה Hybrid Web Container, או בקיצור HWC, יספק לכם אפליקציית נייטיב שכל מה שהיא עושה היא לפתוח מעין iFrame ענק על על המסך שמפנה לכתובת האפליקציה שלכם.
מה היתרון בכזה תרגיל?

  • אתם יכולים להפיץ את האפליקציה שלכם בעזרת ה AppStore או Google Play – היכן שמשתמשים בד\”כ מחפשים אחר אפליקציות.
  • ה HWC יחשוף יכולות native של המכשיר שלא זמינים ב HTML5 כ JavaScript API: מצלמה, אנשי קשר, גישה לקבצים מקומיים, notification (לדוגמה הפעלת רטט של המכשיר, או push messages) וכו\’. הערה: כותבי HTML5 עובדים כדי לאפשר להשתמש בשירותים אלו מאפליקציות ווב – אך הפער בין המצוי לרצוי הוא עוד גדול.
  • ה HWC יאפשר לכם לכתוב custom native code (נניח שדורש חישוב מאוד יעיל) ולחשוף אותו ב JavaScript עבור החלק ה HTML-י של האפליקציה שלכם.
האם HWC הוא הטוב משני העולמות? הפתרון המושלם שייתר את שתי הגישות האחרות (נייטיב וווב)? החומר השיווקי של מפתחי ה HWC יטען שכן, אך נראה שזו תשובה לא מדויקת.
HWC מביא איתו גם חסרונות מ-2 העולמות הקיימים, למשל:
  • חווית המשתמש לא תהיה חלקה ונקיה כמו באפליקציית נייטיב (עדיין יש דפדפן).
  • התאימות לכל המכשירים לא תהיה טובה כמו אפליקציית HTML.
  • אם תעשו שינוי בnative קוד תצטרכו לשחרר גרסה חדשה דרך ה AppStore, דבר שלוקח זמן אצל אפל אפילו אם מדובר ב\”תיקון אבטחה קריטי\”. צד ה HTML – מצד שני, יצטרך לדעת להתמודד עם הגרסאות השונות של ה Container מכיוון שאין לכם שום דרך להכריח את המשתמשים לשדרג את ה Container.
בקיצור, HWC היא אופציה שלישית ואטרקטיבית – אך אינה בהכרח אופציה נעלה. היא פשוט עוד אופציה אפשרית שכדאי לשקול.

MVC Frameworks for JavaScript
מכירים Frameworks מבוססי תבנית העיצוב MVC – Model View Controller? אני מדבר על Struts, JSF או ASP.NET MVC? אני מניח שכן.
ובכן – תשכחו מכל מה שידעתם. אנחנו נדבר על MVC ל JavaScript.
לא, לא בגלל שMVC התגלה לפתע כלא-מוצלח. להיפך. בגלל שמה שנקרא כיום \”MVC Framework for JavaScript\” – הוא פשוט לא MVC במובן שאתם מכירים. העקרונות דומים, אך הצורך העיקרי איננו עוד בשליטה בניווט בין דפים. לדקדקנים: מדובר יותר ב MVP או MVVC – תבניות עיצוב שמתארות בצורה מדוייקת יותר את מה ש Frameworks אלו מספקים. בקיצור ניתן לומר שמדובר ב *MV, שיש בהם אלמנטים דומים ל MVC.
MVC קלאסי מבוסס על 2 עקרונות מרכזיים:
  • הראשי: הפרדה בין UI ל Business Logic, או מה שנקרא View ל Model. זו הפרדה חשובה מאז ומעולם. הקשר בין ה View ל Model יהיה לרוב ע\”י Value Objects בלבד, מה שנקרא בג\’אווה Beans.
  • המשני: העברת פעולות ה navigation בין הדפים למקום מרכזי (ה controller). במקום שכל דף יכיר את הדפים אליהם אפשר לנווט ממנו – הוא מכיר רק פעולות, וגורם שלישי מרכז את הפענוח וההפניה ליעד האמיתי. לעתים קרובות בעזרת קומפיגורציה שאפשר לשנות ללא כתיבת קוד או תלוית הקשר, משתמש למשל.
ה Framework הנפוץ ביותר ה Backbone.js. זהו \”*MV של JavaScript\” ללא Controller שמבצע ניווט בין דפי ווב. גם ההפרדה בין UI ל Business Logic היא given – אך היא לא העיקר. היכולת העיקרית שלו היא לסייע בכתיבת אפליקציות \”דף אחד\” עם אלמנטים דינמיים מתחלפים.
חשבו לדוגמה על Gmail: מעבר בין קריאת הודעה אחת לשנייה – מביאה את המידע מהשרת באופן אסינכרוני (Ajax) והדף מתעדכן ללא ריצוד. יצירת הודעה חדשה או שינוי הגדרות הם שינויים  חזותיים יותר משמעותיים – אך מתנהלים באופן דומה.
אם פיתחתם אפליקציות ווב שכאלה בעבר, קרוב לוודאי שפיתחתם תשתית משלכם לבצע את המטלות. אם הקפדתם – אז ייתכן שיצרתם Renderers שונים שמקבלים מבנה נתונים (שהגיע מהשרת) ויודעים לצייר אותו במקום הנכון.
מסתבר שחווית השימוש באפליקציית מובייל דומה הרבה יותר ל\”אפליקציית דף אחד\” מאשר לאתר אינטרנט בו מנווטים בין דפים – ולכן הצורך והפופולריות של Frameworks מסוג זה.

Backbone.js, למרות השם ה\”כבד\”, הוא ספרייה רזה בת כ 700 שורות קוד (אם לא סופרים הערות). אפשר ללמוד אותו על בוריו בשעה-שעתיים והוא מתאים למכשירי מובייל. אם דיברנו בפוסט הקודם על Micro JS Frameworks- הרי גרסת ה minified שלו היא 16K בלבד. 

ישנם Frameworks רבים, אך ללא ספק הנפוץ שבהם הוא Backbone.js. בעולם ה .NET נפוץ יותר Knockout.js שמקדם את תבנית MVVP, שלמיטב ידיעתי, מוכרת בעיקר מפירסומים של מייקוסופט. 
עוד 2 ספריות נפוצות הן Spine.js ו Ember.js.

Node.js
חשבתם פעם בכמה שפות וטכנולוגיות מפתחי Web צריכים להתמחות? מצד אחד HTML CSS ו JavaScript, מצד שני לרוב האפליקציות זקוקים לשרת ואז צריך לכתוב ב Java, Ruby או #C. פעמים רבות, מפתחי הווב חזקים יותר בצד הלקוח ו\”קצת מחפפים\” בצד השרת. \”לו רק ניתן היה לפתח רק ב JavaScript…\”.
מפתחי צד השרת (כמוני) נוטים לתעב ג\’אווהסקריפט ומחפשים כל מיני דרכים להימנע ממנו (פלאש, GWT ועוד).

האם אפשר להריץ Groovy (וריאנט של ג\’אווה שאני אוהב) בדפדפן? – בחלומות הלילה.
האם אפשר להריץ JavaScript בצד השרת? כן. קיימים מנועי ג\’אווהסקריפט שרצים בצד השרת כמו Rhino או V8. הרשו לי להציג את קפיצת המדרגה בצד זה: Node.js.

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

אבל בכל זאת – זה קצת גימיק, לא? JavaScript עם Interpreter הוא קצת איטי. ובכלל להשוות שרת מורכב כמו Apache Geronimo לאיזו ספריית JavaScript? זה נשמע קצת לא רציני.
אז מתי אני ארצה להשתמש בפיתוח ג\’אווהסקריפט לצד השרת? עבור בדיקות? עבור Prototyping?

האמת המפתיעה הוא ש Node.js הוא מהיר. מאוד מהיר.
בואו נתחיל בלהסביר ממה Node.js מורכב:
Node.js הוא (הגדרה שלי) שרת ווב שכתוב בג\’אווהסקריפט. הוא רץ על V8 – מנוע הג\’אווהסקריפט המהיר של גוגל (שבעצם מקפמל JIT את הג\’אווהסקריפט לקוד מכונה). בנוסף יש ספריות שונות המשלימות יכולות צד שרת שחסרות  בג\’אווהסקריפט (גישה לקבצים, תקשורת וכו\’) וספריית ניהול חבילות בשם npm (המקבילה ל Ruby Gems או Python Eggs). הכתיבה ב Node היא דיי low level, ויש להרכיב לבד את הודעות ה HTTP, אך אין מניעה להשתמש בספריות שיאפשרו רמת אבסטרקציה גבוהה יותר.

Node מול אפאצ\’י – השרת המהיר של הווב. מקור: http://blog.raducojocaru.com

בעוד שרתי ווב מלפני עשור (כמו תקן ה Servlets של ג\’אווה) תוכננו בראיה של הגשת דפי HTML גדולים, Node נכתב בראייה של מענה להודעות קטנות (קריאות Ajax או WebSockets), כאשר שרת HTTP מצומד מגיש את קבצי ה JavaScript או CSS ובניית ה markup נעשית בצד הלקוח. התוצאה היא טיפול בעיקר בפעולות IO (בסיס נתונים, קבצים, רשת) קצרות. כל פעולות ה I/O ב Node הן אסינכרוניות ומבוססות callbacks ברגע שפעולת ה IO הסתיימה.

למטרה זו Node מהיר בצורה ניכרת מרוב השרתים שאנחנו מכירים היום (IIS, Tomcat וכו\’) כאשר הוא צורך קצת יותר זיכרון ו CPU (יש Overhead לכל המקביליות הזו) אך הוא יכול לטפל במספר רב יותר של קריאות. בדיקות מסוימות מראות טיפול בפי 10 בקשות במקביל משרת Apache + PHP. המספרים המדויקים כמובן תלויים בתסריט המדויק – אך יש הרבה פוטנציאל. במצבים של גישות ארוכות שחסומות ב CPU – צפו לתוצאות משמעותית פחות מרשימות. כמו כן שימו לב שכדאי להימנע מקוד סינכרוני ולהתאים את גודל ה Connection Pool על מנת להגיע לתוצאות הרצויות על גבי Node.

את אותה ארכיטקטורה אסינכרונית ניתן כמובן לשחזר בשפות שונות. תוכלו למצוא אותה בשרת Webbit שכתוב בג\’אווה, למשל.

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

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

[א] המצב ב JavaScript הוא עוד טוב. אם אתם מתעסקים בDOM ותומכים ב IE, בקרוב תצטרכו לבדוק את הקוד שלכם על 72 גרסאות שונות.
על Android תצטרכו להתמודד עם Chrome (החל מ Android 4 ועדיין לא דפדפן ברירת המחדל) או Android Browser – דפדפן מעפאן לגמרי שזמין בכל הגרסאות המוקדמות יותר של אנדרואיד. בנוסף יש לא מעט דיווחים שחומרה שונה של אנדרואיד גורמת לדפדפן ה Android Browser להתנהג קצת אחרת – דבר שלא אימתתי בעצמי.

הורשה היא הפרה בוטה של עקרונות ה Object-Oriented!

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

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

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

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

  • הורשה (inheritence)
  • ריבוי-צורות (polymorphism)
  • הכמסה (encapsulation) – או כימוס, כפי שהעירו. תודה.

זהו בדיוק סדר החשיבות ההפוך של עקרונות ה OO!

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

  • הכמסה (private)
  • ריבוי-צורות (implements / instanceof / interface / upcasting / downcasting)
  • הורשה (extends / protected / super / @Override)

(בסוגריים ציינתי את הכלים בג’אווה המשרתים כל עקרון. #C הוא דומה למדי).

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

זוהי קפיצת המדרגה העיקרית מול שפות פרוצדורליות שניהלו את המידע בצורה גלובלית (נאמר C).

ומה עם ריבוי צורות והורשה?
עקרונות אלו הם כלים המאפשרים לכתוב קוד מתוחכם יותר. במשך שנים לא הייתה בניהם הבחנה ברורה. אולי בגלל שפת ++C שאפשרה ריבוי צורות כמקרה פרטי של הורשה, ולא להיפך.

הסבר קצר: שפת ++C הכילה יכולת מקבילה ל abstract בג’אווה / #C שנקראה virtual. מפתחים יכלו להגדיר מחלקות שכל המתודות שלהן virtual (מה שנקרא pure virtual class) וכך לקבל באופן עקיף משהו מקביל ל interface בג’אווה / #C. לא היה לממשק, כפי שאנו מכירים אותו בשפת ג’אווה, שום ייצוג רשמי בשפה – זה היה [1] Pattern.

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

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

מה הבעיה עם הורשה?
התובנה על המגבלות/בעיות בהורשה לא נתגלו עם ג’אווה. עוד בשנת 1992 הציג סקוט מאיירס, בספרו האלמותי “++Effective C” מספר בעיות עם הורשה והזהיר: “use inheritance judiciously”. שנתיים אחר-כך, בספרם המפורסם של GoF, הדבר נאמר יותר במפורש: “Favor object composition over inheritance” – נדבר על כלל זה בהמשך.

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

רוב הסיכויים שמפתחים ותיקים ומנוסים ירצו עדיין להשתמש בהורשה [2] ועבור מפתחים צעירים (הייתי אומר 5 שנים או פחות) – מומלץ לדסבלה (disable it).

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

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

public class InstrumentedHashSet extends HashSet {
  privateint addCount = 0;
  …
  @Override publicboolean add(E e) {
    addCount++;
    returnsuper.add(e);
  }
 
  @Override publicboolean addAll(Collection c) {
    addCount += c.size();
    returnsuper.addAll(c);
  }
 
  publicint getAddCount() {
    returnaddCount;
  }
}
InstrumentedHashMap היא הורשה שנוצרה על מנת לספור את מספר הפעולות על הרשימה.הריצו לרגע את הקוד בראש: אם אוסיף אוסף של עשרה איברים, מה תהיה התוצאה של getAddCount?
התשובה היא: It Depends.
אם אני רץ ב JDK 5 או ישן יותר התוצאה תהיה 20, אולם החל מ JDK 6 התוצאה תהיה 10. באופן רשמי ה API של HashSet לא השתנה כלל בין גרסאות ה JDK, אולם כאשר אני משתמש בהורשה אני נחשף ל API לא מפורש ולא מתועד: הקשר בין המתודות. במקרה שלנו זוהי הנקודה האם addAll קורא למתודת add או לא. כאשר אני יורש, אני יורש גם את הקשר הזה בין המתודות. בJDK 6 החליטו לוותר על הקריאה ל add מתוך addAll (עבור שיפור ביצועים) וכך שינו את החוזה הלא מפורש – אך שמשפיע בהחלט על המימוש שלי ל InstrumentedHashSet.
זהו בדיוק ה”שינוי הפנימי” שלא צריך להפריע לאף אחד אך במקרה שלנו, בעקבות ההורשה, הוא שובר פונקציונליות.הורשה פוגעת בהכמסה: מתודות כמו equals, compareTo, toString ו hashCode, שמומשו באב, מאבדות את נכונותן בעקבות הורשה. יש לכתוב אותן מחדש ולתת גם למחלקת האב (שעשויה להשתנות) לומר את דברה. לא פשוט.

הורשה פוגעת בהכמסה: אם מחלקת האב הכריזה על Serilazible, Clonable, Entity וכו’ – המחלקה היורשת יכולה לשבור הגדרה זו בכל רגע בלי יכולת להכריז על ההיפך. יותר גרוע: ירשתי ממחלקה שלא הייתה Serializable ולאחר שנה מחלקת האב הפכה לכזו – הקומפיילר לא יאמר לי דבר. הבעיה תשאר בעינה.

נו… אני מקווה שהבהרתי את הנקודה.

מה הפתרון? הפתרון הוא להשתמש בקשר הרכבה בין אובייקטים, מה שנקרא Composition. הוא כ”כ נפוץ ושימושי כך שהוא קיבל סימון משלו ב UML. אני אמנע מלכתוב על נושא שכבר תועד רבות. למי שמתעצל לחפש, הנה מקור קצת ישן, אך שנראה טוב:
http://www.artima.com/designtechniques/compoinh.html

סיכום
המסקנה ברורה: אם העיקרון החשוב (הכמסה) סובל מעקרון משני (הורשה) – זו סיבה מספיקה להחרים את העיקרון הסורר והפחות חשוב.
לצערי, עם כל זרם השפות החדשות שנוחת עלינו בשנים אלו (לינק) אף אחד לא הכחיד את ההורשה. הייתי שמח לראות וריאנט של ג’אווה או #C שהופך את extends, ברמת השפה, לפעולת composition במקום הורשה. זה פשוט ואפשרי.
עד אז – השמרו לכם מהורשה. ואם אתם שומעים את את עצמכם, או אנשים אחרים לידכם, אומרים “זה ממש וודו. התנהגות תוכנה זה לפעמים משהו מיסטי שלא ניתן להסביר” – בדקו שוב. אולי השתמשתם בכלי בעל flaws, כמו הורשה – ופשוט לא שמתם לב / אתם לא מודעים לפרטים.
—–[1] אם רוצים לדייק יש לקרוא לזה Idiom, שזה Pattern שנכון לשפת תכנות ספציפית. Patterns הם נכונים בכלל ולא רק לשפה ספציפית.

[2] הנה בשפת סקאלה הם החזירו לחיים את ההורשה המרובה. אבוי!

כיוונים חדשים-ישנים במתודולוגיות פיתוח תוכנה (Data-Oriented Programming)

תשאלו אנשים הכותבים מערכות ב #C או Java מהי מתודולוגית הפיתוח הנפוצה ביותר כיום וקרוב לוודאי שתשמעו \”תכנות מונחה-עצמים, ברור!\”.

הייתי מקשה ושואל: \”איך עובד לכם תכנות מונחה-עצמים, טוב? עומד בציפיות?\” והתשובה היא לרוב \”האמת, ביננו? קצת חטאנו ויש מה לשפר – אבל בגרסה הבאה קצת נשפר / במערכת הבאה נכתוב הכל \’כמו שצריך\’ והכל יהיה טוב…\”

ובכן, לצערי רוב המערכות שראיתי (וראיתי) היו לא-מוצלחות בהיבטי ה Object-Oriented. כינויים נפוצים למערכות אלו הן:

  • Anemic Object Model – מצב שבו האובייקטים הם דלילים ולרוב מחזיקים רק נתונים או רק פונקציות.
  • או Big Ball Of Mud – \”גוש גדול של בוץ\” (שהאמת, מתייחס למגוון רחב יותר של בעיות).

רבים מאיתנו רוצים ליצור תוכנת Object-Oriented הבנויות לתפארת עם Domain Model עשיר, אך אנו נכשלים לעשות זאת שוב-ושוב. האם רוב המתכנתים בעולם גרועים? או שאולי מתודולוגית ה Object-Oriented אינה טובה? (השם ירחם – דברי כפירה)

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

אז אם לא תכנות מונחה-עצמים, אז מהי האלטרנטיבה? תכנות פרוצדורלי? תכנות פונציונאלי? או אולי לכתוב את כל המערכת בפונקציה אחת גדולה? (הכי קל – בהתחלה)

פיתוח מונחה-נתונים (Data Oriented Programming)
בתכנות מונחה-עצמים, האובייקטים הם במרכז ובתכנות פרוצדורלי – הפונקציה היא במרכז. בתכנות מונחה-נתונים (DOP מותר לבטא כ Dope) תשומת הלב נעה מן הקוד לכיוון הנתונים: מהו הקלט, איזה טרנספורמציות (עיבוד) המידע עובר וכיצד הוא נראה בסוף.

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

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

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

על האנומליה של הזכרון
בוודאי למדתם באוניברסיטה קורס \”מבני נתונים\” – קורס חשוב המכסה חומר לא טריוויאלי.
למדתם שיש רשימה משורשרת עם הכנסה של (O(1 ו\”טיול\” (traversal) של (O(n, ויש וקטור (נקרא ArrayList ב #C וג\’אווה) עם מחיר הכנסה של (O(1 או (O(n (עבור הכנסה באמצע או מילוי המחסנית שהוקצה) ו\”טיול\” (traversal) של (O(n.

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

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

פיזור מקובל של תפוסת-זכרון של רשימה משורשרת (באדום). מקור: תוכנת ה disk defrag שלי 😉

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

  • אנו יודעים שמערכת ההפעלה עובדת עם Virtual Memory. אם בלוקים של זכרון בהם שמור אלמנט אחד לפחות מהרשימה שלנו הם paged out (כלומר נשמרו לדיסק על מנת לשחרר זכרון פיסי), מערכת ההפעלה תקבל Page Fault שיגרור Context Switch וטעינת הדף / כתיבת דפים אחרים לדיסק – פעולה יקרה!
  • זכרון המטמון (בעיקר L2 ו L3) במעבד נוטים לעשות prefetch ואחזקה של בלוקים של זכרון – לא תאי זכרון בודדים. כאשר אנו משתמשים בזכרון רציף גישה זו תהיה מועילה, אך עבור רשימה משורשרת היא יכולה אפילו להזיק ולבצע prefetch לזכרון לא רלוונטי [1].
\”אבל זכרון הוא נורא מהיר!\” – אתם עלולים לטעון. \”אנו יודעים שעבור ביצועים-גבוהים יש לבצע הכל בזכרון\”. ובכן יחסית לפעולות IO זה נכון – אבל יש גם הבדל בין שימוש בזכרון כאשר זכרון המטמון יעיל או כאשר הוא לא יעיל.
במשך 30 השנים האחרונות – המעבדים הלכו והפכו מהירים עוד ועוד , משמעותית מהר יותר מהקצב בו התפתח הזכרון. אם ב 1980 המעבד המתין Cycle אחד לקריאה מהזכרון, היום הוא ממתין בערך 400 Cycles. הבעיה מחמירה כאשר הזכרון הזמין גדל (המעבר לעבדי 64 ביט פרץ את גבולות 4GB זכרון) ואנו רוצים להשתמש בזכרון על מנת לעבד כמות רבה יותר של נתונים – בעיה הידועה כ\”צוואר הבקבוק של פון-ניומן\”. מעבדים מודרניים יודעים לעבוד עם Bus רחב בהרבה לזכרון, כלומר קריאה של יותר ביטים במקביל שמושגת ע\”י קריאה (וכתיבה) במקביל מ 2 עד 4 יחידות זכרון (עקרון שדומה מאוד ל RAID 0 בכוננים קשיחים).
שיפורים בביצועי המעבד מול הזכרון ב30 בשנים האחרונות. מקור: Computer architecture: a quantitative approach

כיצד מתכנתים ב Data Oriented Programming
אלו שפות הן שפות \”Data Oriented\”? ובכן, השפה היחידה שאני יכול לחשוב עליה ככזו היא SQL: אנו מודעים לטבלאות וחושבים בפעולות על הנתונים. לטבלה B יש Foreign Key המצביע על טבלה A? אנו יכולים לבצע Select על טבלה B ולמצוא את כל ההצבעות – אין צורך (או משמעות גדולה) בהצבעה כפולה (כמו באובייקטים). ב #C יש את LINQ שהיא אמנם שפה לטיפול בנתונים, אך טבעית מאוד גם לנתונים במודל אובייקטלי – לכן אני לא בטוח שהיא דוגמא טובה.

העקרון של תכנות מונחה נתונים Per-Se הוא להחזיק נתונים (כאשר יש רבים מהם) בזכרון בצמידות. ממש כמו שמירה של טבלאות של בסיס נתונים רלציוני. כך יהיו לנו הרבה Collections גלובאליים של \”אובייקטים\”, כאשר האובייקטים הם רזים (יותר דומים ל struct של נתונים ופחות אובייקטים קטנים ועשירים). מצד שני יהיה ניתן להפעיל כל פעם פונקציה אחרת על אותו struct – מעבר שהוא \”זול\” מבחינת עלות זכרון (מכיוון שיש יחסית מעט פונקציות נפוצות שיכולות להשמר ב Cache).
בעוד תכנות מונחה-עצמים יוצר מבנה דומה ל LinkedList – אובייקטים הפזורים לכל עבר בזכרון, כאשר קריאות getX.GetY.GeyZ המפורסמות של ג\’אווה מדלגות בזכרון לא-רציף, תכנון מונחה-נתונים הוא דומה יותר לוקטור (ArrayList) רציף בזכרון המאפשר להשתמש ב Cache בצורה יעילה ופונציות שונות שפועלות בצורה ממוקדת על הנתונים ללא \”קפיצה\” תכופה לאובייקטים אחרים.

השימוש בData-Oriented Programming כמתודולוגיה מקובלת חזר בשניים האחרונות בעקבות אפליקציות ה Mobile (כלומר Android, iPad, iPhone) – אפליקציות קטנות הנכתבות עבור מכשירים בעלי כח עיבוד חלש.
מתכנתים מדווחים על ביצועים גבוהים פי 2 עד 4 בהמרה של אפליקציות Object Oriented להיות אפליקציות Data Oriented ובקלות פיתוח גבוהה יותר.

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

כמו שאנשים חששו להשתמש ב\”אובייקט פשוט של ג\’אווה\” (והשתמשו ב EJB) עד שניתן לו השם המכובד POJO, כך גם אין לחשוש בתכנות מונחה נתונים ולהתייסר שאנו לא כותבים OOP, הנה עכשיו יש לו שם מכובד: DOP.
בסופו של דבר כל אלו הם כלים, עם יתרונות וחסרונות, ומקצוען אמיתי ידע לבחור בניהם בחוכמה ולא ייקלע למלכודת של \”חייבים לעבוד ב EJB או OOP או – כי כולם עובדים כך\”.
דוגמא קרובה נוספת היא ההצלחה של REST – מודל פשוט וממוקד נתונים / פרוטוקול רשת, שהצליח יותר בשטח ממודלים מונחי אובייקטים או שירותים, ארכיטקטורות נבונות מגובות בהמון תאוריה.

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

היתרונות של DOP על OOP
אני מניח שהיתרונות של OOP (הכמסה, מידול, ריבוי-צורות ועוד) מוכרים לכולם, בואו נסקור כמה יתרונות של ה DOP:

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

Unit Testing
מי שעובד עם unit tests יודע שהכי קל לכתוב בדיקות לפונקציות המרה פשוטות של נתונים, כמו פעולות parsing, למשל. שימוש ב DOP יהפוך גם את ה unit tests לפשוט וטבעי יותר מכיוון שיהיו הרבה יותר פונקציות ש\”רק מעבדות נתונים\” ויפחית משמעותית את הצורך ב mocks, stubs וחברים. אימוץ Unit Tests הוא לא דבר קל, ושימוש ב DOP יכול להיות סיוע משמעותי.

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

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

מאגר אדיר של בסיסי נתונים (רלציוניים) התואמים למודל כמו כפפה.
מודל ה OOP לא רק הקשה על החשיבה הלוגית שלנו ועל ארגון התוכנה לאובייקטים, הוא גם הקשה מאוד על שימוש בבסיסי הנתונים הרלציוניים, אשר בטבעם הם מונחי-נתונים.
במשך שנים ניסו לבנות כלי (ORM (Object-Relational Mapping עם הצלחה חלקית בלבד. אפילו Hibernate/NHibernate – ה framework הפופולארי ביותר, הוא נחמד לשמירת קונפיגורציה אך כושל כאשר אנו זקוקים לביצועים טובים על הרבה נתונים. גם אני האמנתי למצגות שמספרות שברוב המקרים Hibernate יבנה סכמה יעילה יותר מהמתכנת ויספק Cache שישפר את הביצועים אפילו יותר. הנסיון שלי הוא שכאשר יש דרישה לביצועים טובים, ישנו מאבק ארוך עם Hibernate שבסופו Hibernate מוצא את עצמו מחוץ למשחק.
אמנם אם היינו יוצרים באופן ידני את אותה הסכמה ש Hibernate מייצר – הוא יכול היה להיות מהיר יותר, אך ההבנה שלנו בנתונים מובילה אותנו לסכמות שונות לחלוטין ממה ש Hibernate ייצר.
אחד המניעים של תנועת ה NoSQL היא השתחררות ממיפוי אובייקטים למודל רלציוני ועבודה ישירות מקוד OO למודל שמירת נתונים OO (כגון בסיסי נתונים KV). על אותו מטבע אם הקוד שלנו הוא מונחה-נתונים, כך גם בסיסי נתונים רלציונים ישרתו אותנו היטב ובקלות – ויש המון כאלה.

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

היתרונות של חלוקה של מערכת למודולים עם אחריויות ברורות, הכמסה נוקשה (encapsulation) של כל מודול ותיאור המערכת כשיקוף של העולם האמיתי / הבעיה העסקית (מה שנקרא גם DDD – Domain Driven Design) – הם יתרונות ברורים שעובדים היטב בשטח.

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

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

בהצלחה.

[1] בשקפים 31 עד 101 של מצגת זו – ניתן למצוא הסבר מפורט של התופעה.

[2] יש לזכור שמערך בג\’אווה הוא רשימה רציפה של מצביעים. האובייקטים עצמם (אליהם אנו מצביעים מה ArrayList) עדיין מפוזרים באופן שרירותי בחלק הזכרון הנקרא Heap. ב #C המצב נוח יותר – יש structs שיכולים להיות מוגדר באופן רציף. בכל מקרה אני רוצה להדגיש ולחזור שאופטימזציית performance היא לא עצם העניין, עצם העניין הוא מודל שקל יותר לשימוש המפתח – אופטימזציות הביצועים בדיון זה היא משנית.