על פרדיגמות תכנות, ומה ניתן ללמוד מהן בשנת 2019? – חלק ב׳

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

התכנות הפרוצדורלי – מה הוא נתן לנו?

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

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

Functional Decomposition

הרעיון החזק ביותר שהציג התכנות הפרוצדורלי [א] נקרא Functional Decomposition. הוא הציל את עולם התוכנה מכבלי הקדמוניות – אבל מאז הפך גם ל Anti-Pattern מסוכן ונפוץ.

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

הרעיון, הולך בערך כך:

איך מתמודדים עם בעיה גדולה ומורכבת? – נכון: Divide & Conquer. מחלקים לבעיות קטנות יותר, שקל יותר לפתור אותן אחת אחת.

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

החלקים הם:

  • תיאור state ראשוני – נתון.
  • תיאור state סופי – רצוי.
  • סדרה של טרנספורמציות שיהפכו את ה state הראשוני ל state הסופי.
את הטרנספורמציות אתאר כפרוצדורות (~= וריאציה מוקדמת יותר של הפונקציות), ואז למרות שאני לא ידוע בדיוק איך לממש כל פונקציה – שברתי את הבעיה הגדולה לבעיות קטנות. עכשיו יהיה עלי להתמודד עם פונקציה בודדת בכל פעם – ולפתור בעיות קטנות יותר, ולכן קלות יותר.
יתרה מכך – הפרוצדורות / פונקציות הללו – הן בסיס טוב ל code reuse. טרנספורמציה על מחרוזת כגון ()trim או ()replaceAllCaptialsWithNonCapitalLetters – יכולה לשמש אותי במקרים רבים נוספים!

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

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

מה הבעיות של Functional Decomposition שהפכו אותו ל Anti-Pattern בעולם ה OO?

  • אי התייחסות / מחסור ב Information Hiding ו Encapsulation:
    • האם ה State הראשוני חשוף לכלל המערכת? האם הוא גלובאלי, למשל? – כך היה בהתחלה.
    • האם כל פונקציה שאפעיל רשאית לעשות כל שינוי ב state?
      אם ה state הוא רשימה של מחרוזות – אז אין בעיה. ככל שה state גדול ומורכב יותר (קיצונות אחרת – לב הנתונים של המערכת) – קל יהיה יותר לאבד שליטה ובקרה: איזה פונקציה עושה מה – על הנתונים הללו.
  • סיכויים גוברים לארגון נחות של המערכת:
    • כאשר הפונקציות מפוזרות במערכת ללא סדר וללא היגיון – קל להגיע לבלאגן.
    • כאשר אין סדר במערכת, סביר יותר שלא אמצא פונקציה קיימת ומספקת – ואכתוב אותה מחדש = קוד כפול.
  • סיכויים גדולים למידול נחות של המערכת:
    • חשיבה על המערכת בצורה של ״נתונים״ ו״טרנספורמציות״ אינו טובות למערכת גדולה ומורכבת. במערכת מורכבת, חשוב מאוד למצוא הפשטות (abstractions) טובות לפרטי המערכת, כך שיתאפשר לנו לחשוב ברמה גבוהה יותר של הפשטה. ה Functional Decomposition לא מוביל אותנו לשם, ואולי אפילו מפריע.
    • ביטוי של הסיכון הזה עשוי להגיע בדמות מודל אנמי – דפוס עיצוב שלילי שכתבתי עליו בהרחבה.
למרות ש Functional Decomposition נשמע כמו נאיביות של העבר, השימוש בו צץ שוב ושוב גם היום –  גם בשנת 2019. בעשור האחרון נתקלתי בעשרות מקרים של ״נסיגה״ מ OO ל Functional Decomposition שלא הועילו למערכת. שווה ללמוד את ה Anti-Pattern הזה ולהבין אם אתם מיישימים אותו במערכת. אולי ההבנה הזו – תעזור לכם לפשט את המערכת, ולהפוך אותה לקלה יותר לשינויים ותחזוקה.

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

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

Modules
רעיון החלוקה למודולים, הוא הרעיון שאנו נחלק בחלוקה יזומה של קוד המערכת שלנו לאזורים – ע”פ קריטריונים מסוימים, מה שיאפשר לנהל טוב יותר את בסיס הקוד.
איזה סדר בדיוק? – אין לכך הנחיה ברורה אחת. חשוב שהסדר במערכת יהיה ברור לכולם, ועקבי – אחרת הוא חסר משמעות.
יש דרכים שונות לארגן קוד לאזורים שונים במערכת. בשפות תכנות שונות, יש וריאציות מעט שונות או לפחות שמות שונים לאותם “מודולים”: Packages, Namespaces או Modules – הם שלוש צורות / שמות – לאותו הרעיון:

  • כל פיסת קוד שנכתבת תהיה ״מקוטלגת״ או ״מיוחסת״ לאזור מסוים בקוד – להלן ״מודול״.
  • הקרבה הזו היא קרבה רעיונית, ולא בהכרח פיסות קוד שקוראות אחת לשנייה.
  • אין ברירה, קוד ממודול אחד – יקראו לקוד במודול שני. אבל, אנחנו רוצים לחלק את הקוד למודולים כך, שנצמצם את הקריאות בין המודולים, ונמקסם את הקריאות / ההיכרות בתוך אותו המודול.
    • הרעיון הזה נקרא גם: ״High Cohesion / Low Coupling״. בעברית: אחידות גבוה (בתוך המודל) / תלות נמוכה (בין המודולים).
למרות שרעיון החלוקה למודלים הוא לא מדויק (לא מדויק בהגדרה, ולא מדויק במימוש) – הוא רעיון רב עוצמה שיש עליו קונצנזוס גדול: רבים ממובילי דעת הקהל בעולם התוכנה מאמינים שאם נחלק את המערכת לחלקים ע״פ סדר משמעותי – יהיה יותר קל להשתלט עליה ולנהל אותה. גם אם אין לכך הוכחה מתמטית ברורה.
בהמשך הדרך, האזורים של הקוד, “המודולים” – צמחו גם להיות יחידות נפרדות של קומפילציה, או linking/loading ואולי גם deployment. הנה, למשל, רעיון ה Micro-Services – על קצה המזלג.
כל אלו הם רק שיפורים – על הרעיון הבסיסי.

הפרדיגמה מונחית-העצמים (OO)

מי שחי בתקופה בה צמחה הפרדיגמה מונחית העצמים, בעיקר בשנות ה-80 וה-90 – חווה דיון נוקב בהבדלים בין תכנות מונחה-עצמים (OOP), ותכנון מונחה-עצמים, Object-Oriented Design, או בקיצור: OOD.
אפילו לא פעם נשמעה הטענה ש”OOD הצליח – אך OOP נכשל”. טענה שנויה במחלוקת, הייתי מוסיף.

אני לא רוצה להשקיע זמן על שחזור הוויכוח, ועל הדקויות הנלוות – ולכן אתייחס בעיקר ל OO כ OOD+OOP.

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

על פייטון ניתן לומר שהיא יותר שפת OOD מאשר שפת OOP…

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

Everything is an Object

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

לרעיון יש שני פנים:

  • לארוז קוד ביחידות קטנות (יותר) של מודולריזציה – להלן מחלקות.
  • למדל את המערכת ליחידות המתארות את העולם האמיתי. זה עיקרון ליבה של ה OOD, שלעתים לא מודגש מספיק: זה לא מספיק לחלק את הקוד ל״מחלקות״. חשוב מאוד שהחלוקה הזו תתאר את עולם הבעיה (העסקית) – בצורה נאמנה והגיונית. המיפוי / מידול הזה – מקל על הבנת המערכת, ועל התקשורת לגביה – מה שמקל על לקבוצה גדולה של אנשים להיות שותפים אליה ולעבוד בצורה קוהרנטית.
    • אם בעולם שאותו המערכת משרתת יש מונחים שלא מתבטאים במערכת כאובייקטים – אז כנראה עשינו מידול לא מוצלח.
    • אם הקשר בין האובייקטים לא תואם להיגיון מקובל – אזי כנראה שהמידול שלנו לא מוצלח.
    • אחד מכלי המחשבה למידול לאובייקטים הוא המחשבה על ״תחום אחריות״ (להלן: SRP). אם האובייקט היה בן-אדם, מה הוא היה דורש שרק הוא יעשה? שיהיה רק באחריותו?
      • טכניקה מעניינת של מידול שצמחה, למשל, היא CRC cards.
האובייקטים תוארו בעזרת המטאפורה של כ״תא ביולוגי עצמאי״. לתא יש מעטפת קשיחה המגנה עליו מהעולם (על כך – בהמשך) והוא מכיל בפנים את מה שהוא זקוק לו לפעולה עצמאית. כלומר: אנו מצמדים את הנתונים (state) לפעולות (functions). זה עוזר לנו גם בכדי להגן על ה state – אבל גם בכדי ליצור סדר הגיוני וצפוי במערכת.
שפות תכנות מודרניות, המתוארות כ OO, לרוב מספקות מבנה בשם מחלקה (Class) – שמקל עלינו למדל את המערכת כאובייקטים. האחריות לתוכן, והמידול עצמו – היא כמובן בידנו. שום שפה לא תעשה את זה במקומנו.
אם המערכת שלנו בנויה מ”מחלקות של נתונים”, ומנגד, מ”מחלקות של פעולות” – אז פספסנו את הרעיון המרכזי הזה של Everything is an Object. זה לא משנה בכלל אם אנחנו עובדים ב Enterprise Java או Shell Script. יותר מהכל, OO הוא לא כלי – אלא רעיון.
אל תבטלו את העניין הזה סתם: אנחנו עדיין כותבים הרבה קוד שלא מציית לרעיון של Everything is an object. הרעיון הזה קל אולי להבנה – אבל לא תמיד קל ליישום.

Information Hiding

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

OO חידשה בכך ששמה את ה Information Hiding כעקרון ליבה מחייב – ולא עוד כהמלצה ״לפעמים כדאי לצמצם את הידיעה של חלק אחד בקוד על חלקים אחרים״. ב OO מדגישים: ״בכל מקום במערכת אנחנו נסתיר כל מה שלא נחוץ – לחלקים האחרים״.

  • Encapsulation הוא השילוב של הרעיון הזה, בשילוב הרעיון של Everything in an Object. ההכמסה היא ״קרום התא הביולוגי״ המגן כל פנים התא הרגיש (ה state הפנימי) – מהשפעות חיצוניות.
    בשפות OO לרוב יש לנו directives הנותנים לנו לשלוט על הנראות: private, package, protected – וכו’.

    • אובייקטים הרוצה משהו מאובייקט אחר, שולח לו ״הודעות״. לא עוד אלך ואפשפש לי (ואולי גם אשנה) State של חלק אחר במערכת. יש מעתה Gate Keeper, שלו אני שולח רק את המידע הנדרש (“ההודעה”) – ואקבל בחזרה – תשובה.
      • אם אתם יוצרים הודעות בין אובייקטים ושולחים את כל ה State של האובייקט, אז אתם מממשים משהו שקרוב יותר לתכנות פרוצדורלי – מאשר ל OO.
  • יש צורות נוספות של Information Hiding מלבד הכמסה של אובייקטים:
    • הפרדה של בסיס הנתונים לסכמות, תוך הגבלת הגישה לסכמות השונות – הוא information hiding
    • תכנון המערכת כך שיהיה צורך של פחות אובייקטים להכיר אחד את השני – הוא information hiding מעבר להכמסה של האובייקט הבודד.
    • העברה ל Front-End רק את פיסות המידע הנדרשות (לפעמים נדרש עיבוד נוסף לצורך כך) – הוא information hiding.
  • המוטיבציה העיקרית ל Information Hiding היא היכולת לבצע שינויים בחלקים מסוימים במערכת, בלי שהדבר יגרור צורך לשינויים באזורים נוספים. להגביל את גודל השינוי.
    • אם אני לא יודע על חלקים אחרים – שינוי בהם לא אמור לדרוש ממני להתעדכן.
    • זה לא תמיד נכון. לפעמים יש השפעות עקיפות – שכן ידרשו ממני להתעדכן. למשל: סדר שונה של פעולות שמרחש במערכת. לא נחשף לי מידע חדש / שונה, אבל עדיין אני צריך להתאים את עצמי.
לרעיון של Information Hiding (או הכמסה: אנו נוטים היום להשתמש במונחים לחליפין) – יש מחיר: הסתרה של נתונים דורשת עבודה נוספת – עוד כתיבת קוד, וחשיבה ותשומת לב – שבעקבותיה עוד כתיבת / שינוי קוד.
ברור שנקודת האופטימום איננה למקסם את ה Information Hiding עד אינסוף. אין כלל ברור כיצד למצוא את נקודת האופטימום, ורוב אנחנו מוצאים אותה (פחות או יותר) תוך כדי גישוש.
לאורך חיי ראיתי מערכות רבות שחסר בהן Information Hiding, וראיתי גם מערכות שיש בהם עודף של Information Hiding – והתקורה לכל שינוי, הייתה רבה מדי.

הורשה

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

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

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

שלא תגידו שלא ידעתם.

non-Objects

מהה?? אם “Everything is an Object” – אז איך יש דברים שאינם אובייקטים?

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

כש OO אומץ בצורה יותר ויותר רחבה, עלו עוד ועוד קולות שרצו להבחין בין מחלקות (classes) לבין מבני-נתונים (structs). בניגוד למחלקה, מבני-נתונים רק “מחזיקים” כמה נתונים ביחד – ואין עליהן פעולות (כך טענו בתחילה). יותר מאוחר הסכימו שיכולות להיות גם על מבני-נתונים פעולות (פונקציות) – אבל רק כאלו שעוזרות לגשת לנתונים במבנה, ובוודאי לא Business Logic. ההבחנה הזו לא השתנה – ולא נראה שיש קולות לשנות אותה.

ג׳אווה, שפה מאוד פופולארית ומשפיעה – דילגה על ההבחנה בין מחלקות למבני-נתונים. אם אנחנו רוצים להגדיר בה מבנה-נתונים – אנחנו משתמשים ב classes. בעצם כך, שפת ג’אווה “מנעה” במידת מה, מההבחנה בין מחלקות למבני-נתונים לחלחל בתעשייה.

  • האם Map או List הם מחלקות? לא – אלו הם מבני-נתונים. כל מטרתם היא להחזיק ולחשוף נתונים. הכמסה? זה לא הקטע שלהן [ב].
  • האם DTOs או Enum הם מחלקות? לא – אלו מבני-נתונים. בשפת ג׳אווה נממש DTOs כ Class, אך מבחינה רעיונית הן לא מחלקות: אין להן הכמסה [ב], ואין להם אחריות לתאר פעולות בעולם.
    • בג’אווה הרבה פעמים כאשר ממשמשים DTO (לרוב מצוין ע”פ Suffix בשם) – מאפשרים לכל השדות של ה DTO להיות public – ומדלגים על השימוש ב getter/setters. הגיוני.

חוסר ההבחנה בין מחלקות ומבני-נתונים לא מאפיין את כל השפות: ב #C קיימים structs, בקוטלין יש Data Classes, ובפייטון היו named tuples כסוג של ייצוג, ולאחרונה הוסיפו גם Data classes.

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

יש כמה סימנים שיעזרו לנו לזהות מבני-נתונים:

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

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

סיכום

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

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

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

—–

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

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

[ב] זה כמובן, חצי מדויק: לפעמים מבנה הנתונים הוא מורכב (למשל HashMap) ואנו מכמיסים את מורכבות המבנה הפנימי בכדי לחשוף למשתמש מבנה-הנתונים הפשטה פשוטה יותר.

שיעור מס’ 8 בהורשה

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

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

לצורך הדיון אני אציג רק ממשקים, ולא אתעכב על חלוקה ל Interfaces ו Concrete classes (שהוא באמת משני לדיון).

בואו נתחיל.

הבעיה

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

מיקום הצורה (x, y), לצורך הדיון – הוא לא חלק מההגדרה.

משהו לא מריח כאן טוב בהגדרה הזו…
יש משהו מאוד פרוצדורלי, ולא כ”כ OO בהגדרה הזו: ה Path נראה כמו מרכיב זיהוי מרכזי בהתייחסות לצורה – אבל הוא מיוצג כתכונה כשאר התכונות.

למשל: אנו יכולים לייצר Line עם fillColor – מצב לא תקין. הכוונה להשתמש ב fillColor היא רק עבור פוליגון.

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

כיצד ניתן למדל את האובייקט בצורה יותר נכונה?

יש לכם איזה רעיון?!

פתרון ראשון

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

בואו נבחן את המודל שלנו עכשיו:

  • DRY (חוסר חזרה על הקוד) – מצב מצוין. אין באמת הרבה מה לקצר את הקוד.
  • הכמסה (encapsulation) – מצב בינוני. על הפשטת העל יש כמעט את כל התכונות, מה שאומר שכל מי ש”אוחז” ברפרנס ל BaseShape יקבל גישה לכל התכונות הללו. האם יש לזה צורך? או האם “שיתפנו” יותר מדי?
    הנחת היסוד היא שהשימוש האחיד באובייקטים הוא בעזרת הפונקציה ()draw. נניח: רוצים לרוץ על מערך ולצייר את כולם, אך בכל מקרה, כל גישה מעמיקה יותר דורשת את בדיקת הטיפוס ו downcasting.

    • אין נזק מיידי באי-הכמסה (“אז שלא ישתמשו בזה! מה הבעיה?!”) – אבל זה זרז לשימוש לא מבוקר בתכונות הללו. מפתח שמקבל הצעה לשימוש בתכונה ב autocomplete של ה IDE – לעתים רחוקות ישאל את עצמו אם נכון היה שהאובייקט יחשוף תכונה זו, או האם בכלל נכון להשתמש בה.
    • כל מפתח יכול להיכנס לאובייקט ולשנות את כל שדות האובייקט ל Public – אבל זה סייג המעורר הערכה מחודשת על נכונות הפעולה (ברוב, התקין, של המקרים).
  •  אמינות המודל למציאות / פשטות הבנה (presentation) – הרבה יותר טוב, אך עדיין ישנם עיוותים.
    • הסרנו את ה Path כתכונה, ופתרנו את הבעיה בה fillColor זמין להיכן שאינו רלוונטי – שזה מצוין.
    • שמות המשתנים אינם טובים. הם “פשרה” עבור “שימוש חוזר בקוד” / או סתם מכח האינרציה – פשרה לא טובה.
כשאנחנו ממדלים אובייקטים במערכת אנו משרתים כמה צרכים:

  • אנו מספקים את הצורך הבסיסי של המערכת ביישות לאחסן בה נתונים / להגדיר פונקציות. לחלופין זה היה יכול להיות מערך גלובאלי של נתונים. זו חובה כדי שהקוד יעבוד – אך זה קו האפס מבחינת הנדסת תוכנה.
  • אנו מתארים כוונה / רעיון – שאנו רוצים להנחיל לשותפנו לבסיס-הקוד (להלן Presentation), ובתקווה שקבענו את הכוונה / רעיון בצורה טובה ואמינה לעולם העסקי הרלוונטי.
  • אנו מגבילים את חשיפת הידע במערכת, בעזרת הכמסה – אם אנחנו עובדים לפי מודל Object Oriented.
  • כן, פרקטיקה טובה של קוד, היא לא לשכפל קוד (DRY = Don’t Repeat Yourself) – אבל זו לא ממש מטרה בהגדרת אובייקט.
בפועל, הפרקטיקה של DRY (מסיבות כאלו ואחרות) מוטמעת היטב בקרב אנשי-תוכנה, הרבה יותר חזק מפרקטיקות לא-פחות חשובות כמו Encapsulation או פרקיטקות מידול. התוצאה: מודלים קצרים יותר בשורות קוד – אך פחות עמידים לאורך זמן, ולאורך כמות שינויים שעומדים לעבור עליהם.
כדי לחדד מדוע האובייקטים בדוגמה שלנו אינם ממודלים בצורה טובה, נשטח שנייה את האובייקטים מהיררכיית שבה הם בנויים, ונציג אותם כפי שיופיעו ב run-time, או למשל – ב IDE כאשר אנו מקבלים המלצות שימוש ב autocomplete:

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

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

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

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

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

פתרון שני

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

עבור Point, הרבה יותר מדויק להסביר שתכונה מסוימת היא קוטר (או לחלופין זה יכול היה להיות רדיוס) – ולא “line width”, שמשאיר מקום לפרשנות. בוודאי שיותר טבעי לי לחשוב על color ולא “line color” – כי אין קו בנקודה.
ב Polygon יותר מדויק לדבר על Border ולא Line, עניין של דייקנות – שיכולה להשתלם מאוד לאורך חיי-מערכת.

שיתפנו בין האובייקטים רק את מה שהכרחי: הפונקציה ()draw (לצורך ריבוי-צורות).

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

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

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

ממשקים הוא כלי שתכליתו היא PolyMorphism (ריבוי-צורות): התייחסות אחידה לאובייקטים שונים ע”פ מכנה משותף משמעותי. אל תתלו עליהם משימות של Code-Reuse, כי זה לרוב ייגמר בפשרה לא-משתלמת..

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

סיכום

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

DRY היא פרקטיקה חשובה מאוד – אבל יותר באזור של תוכן הפונקציות: יש כמה שורות קוד שחוזרות על עצמן? הקפידו להוציא אותן לפונקציה משותפת.
יש משתנים כפולים (או state כפול כלשהו)? אבוי – היפטרו מהעותקים מיד! (“data duplication is the mother of all Evil”).

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

ייתכן ואתם שואלים את עצמכם “אם זה שיעור מס’ 8 בהורשה – מהו שיעור 7? “. טוב, נו – זו לא הייתה כותרת מדויקת. ניסיתי לבטא שזה שיעור חשוב ב Object-Oriented (ולאו דווקא הורשה), אבל לא השיעור הראשון והחשוב ביותר. וגם לא הבסיסי ביותר. אני מדמיין אותו … כאיפשהוא כשיעור מספר 8, מבלי מחויבות גבוהה לדיוק.

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

תכנות מונחה-עצמים בשפת רובי

שפת רובי היא שפת Object-Oriented (בקיצור: OO).

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

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

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

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

נפתח בהתמצאות בסיסית:

המבנים הבסיסיים של ה OO ברובי הם:

  • מחלקה (class)
  • אובייקט (object)
  • מודול (module) – אפשר לחשוב עליו כ\”מודול הרחבה\” שניתן \”לחבר\” אותו למחלקה בכדי להרחיב את היכולות שלה.
  • מחלקה יחידנית (singleton class) – לפעמים נקראת גם metaclass, משמשת להשגת התנהגויות מסויימות שנדבר עליהן בהמשך.
  • מבנה (struct) – סוג של \”תחליף זול\” ליצירת מחלקות פשוטות בקלות.

ברובי אין interfaces, ואין abstract classes.

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

  • ניתן לייצר מופעים (instances) ממחלקות וממבנים בלבד.
  • מודול ומחלקה יחידנית הם דומים מאוד למחלקה – אבל יש עליהם כמה מגבלות (הברורה: אין מתודה new ליצירת מופעים).
  • כל המבנים הנ\”ל הם בעצם גם אובייקטים לכל דבר: הם instances של מחלקה כלשהי, יש להם self, וכו\’ – לא שונה כ\”כ מג\’אווה, למען האמת.

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

נראות במחלקה

מה העיקרון החשוב ביותר ב OO? – הכמסה!
בואו נראה איך הכמסה עובדת ברובי.

כברירת מחדל ברובי:

  • כל משתני המחלקה (@@) או משתני המופע (@) – הם private
  • כל המתודות – הן public
  • מקרה מיוחד היא המתודה initialize (הבנאי) שהוא private: המתודה new של המחלקה BasicClass (ממנה כל מחלקות הרובי יורשות) היא public והיא קוראת לבנאי של המחלקה שלנו.
יש שוני בין ההגדרות של private ו protected בין ג\’אווה (או ++C) לבין רובי.

נתחיל בהגדרת ה private ברובי (במילה: מחמיר מעט יותר מג\’אווה):

  • ברובי private הוא פרטי של המופע – כלומר מופעים אחרים של המחלקה לא יכולים לגשת אליו (כמו שהם יכולים בג\’אווה – אם אתם לא מכירים. בג\’אווה כל האובייקטים מאותו המחלקה הם חברים, friends)
  • ההגדרה ברובי ל private: לא ניתן לקרוא למשתנה / מתודה – אם צריך לציין מי המקבל של ההודעה.
  • להבהיר: הורים וילדים של המחלקה – יוכלו לגשת למתודה, אולם באופן עקיף – ע\”י הקריאה למתודה כאילו היא שלהם או ע\”י super. בן לא יוכל לקרוא למתודה של מופע אחר מאותה המחלקה, למשל.

למשל:

class MyClass

@other_object = MyClass.new

def foo
say_it
# specified no-one - okay!
self.say_it # specified \'self\' - no go.
@other_object.say_it # specified \'other_object\' - no go.
end

private

def say_it
puts
\'yay!\'
end

end

x = MyClass.new
x.say_it
# NoMethodError
x.foo # yay!, then NoMethodErrors
המילה השמורה private בגוף המחלקה מגדירה שכל מתודה שהוגדרה מקטע זה ואילך תהיה private (דומה ל ++C). ניתן בהמשך להגדיר מקטע protected ואז public או private בחזרה, וכו\’

למתכנת ג\’אווה מנוסה, הקוד הבא אמור להעלות שאלה:
\”ניתן לראות בקלות ש x.say_it היא קריאה למתודה פרטית. למה ה IDE (נניח RubyMine) צועק על בעיות אחרות – אך לא על זו ?!\”
התשובה היא בגלל שניתן לשנות את נראות המתודות בזמן ריצה – ולכן לא ניתן להחליט בבבירור שזוהי שגיאה.

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

מה המשמעות אם כן של protected?

protected ברובי היא דומה יותר ל private בג\’אווה. הההגדרה: המתודה היא נגישה כל עוד self מתייחס למופע של אותה המחלקה.
הקריאה יכולה להתבצע מהאובייקט עצמו, מופע אחר של אותה המחלקה, או מופע של מחלקה שיורשת / נורשת מאותה המחלקה. הייעוד של protected ברובי הוא לשתף מידע בין מופעים הקשורים זה-לזה.

מחלקות ומופעים – כיצד הם מיוצגים?

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

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

  • מופעים מכילים state – משתנים
  • מחלקות מכילות מתודות וקבועים
  • מכיוון שמחלקה היא גם מופע – אזי היא גם מכילה משתנים, אם כי ב\”רמה\” אחרת.
מתודות המופע (instance methods)
מתודות המשותפות לכל המחלקות המופעים של המחלקה MyClass. אלו בעצם המתודות הרגילות של המחלקה, כמו המתודה foo בדוגמת הקוד למעלה.
מתודות המחלקה (class methods)
הן מתודות שזמינות להפעלה מתוך reference למחלקה עצמה. הן דומות למתודות סטטיות (static method) בג\’אווה.
שימו לב שבתיעוד מקובל לסמן מתודות מופע ב# ומתודות מחלקה ב::
class MyClass
def self.my_class_method
puts
\'first!\'
end

def MyClass.second_class_method
puts
\'second!\'
end

class << self
def MyClass.third_class_method
puts
\'third!\'
end
end
end

x = MyClass.new

MyClass.my_class_method # first!
MyClass.second_class_method # second!
MyClass.third_class_method # third!

x.my_class_method # error !@#$!
x.class.my_class_method # first!

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

מתוך המופע x אינני יכול לקרוא למתודות המחלקה של MyClass (למרות שאני מופע של MyClass), אלא רק בעזרת התייחסות (reference) למחלקה עצמה (למשל: x.class).

משתני מחלקה וקבועים
את משתני המחלקה אנו מכירים מהפוסט הראשון – הם מתחילים ב @@ והם דומים ל static fields בג\’אווה. ניתן לגשת אליהם מתוך מתודות של המופע.

נזכיר שגם משתני מופע (@) וגם משתני מחלקה (@@) הם private members. ניתן לגשת אליהם מתוך מתודות של המחלקה / מופע – אך לא מתוך קריאה ל x.@some_variable (מדוע? מכיוון שציינו את מקבל ההודעה, כמובן!)

class MyClass
@@y = 4
Z = @@y

def foo
puts
@@y
@@y += 1
end
end

MyClass.new.foo # first instance -> 4
MyClass.new.foo # second instance -> 5
# MyClass.new.@@y = syntax error

x = MyClass.new
puts
MyClass::Z # 4
puts x::Z # error!

אופיים של קבועים (Z – במקרה שלנו) נקבע ע\”י כך ששמם שמתחיל באות גדולה. שימו לב שמחלקות ומודולים – הם (אובייקטים) קבועים בשפת רובי.

מתודות ישירות / יחידניות (singleton method)
בשונה מג\’אווה ניתן להגדיר מתודות על מופע בודד – ולא על המחלקה.

class MyClass
def foo
puts
\'yay!\'
end
end

y = MyClass.new

def y.goo
puts
\'woo\'
end

x = MyClass.new

y.goo
# woo
puts y.singleton_class.method_defined? :goo # true

puts x.singleton_class.method_defined? :goo # false
x.goo # NoMethodError

כלומר: המתודה goo קיימת רק על המופע y – ולא על שאר המופעים במחלקה.
אם אתם זוכרים את הכללים שלמעלה – מופע (אובייקט) מכיל רק state ולא מתודות. מתודות יושבות על מחלקות או מודולים. כיצד זה מסתדר עם מה הקוד שזה עתה ראינו?

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

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

המחלקה היחידנית של MyClass היא לא חלק מהיררכיית ההורשה של המופע x (היררכית הורשה היא עניין אישי ברובי) – ולכן אם מתודת המחלקה נקראת ישירות מתוך המופע (x.some_class_method) – המופע לא ימצא אותה.

הנה דרך נוספת להגדיר singleton method על המחלקה x

x = MyClass.new
y
= MyClass.new

class << x
def poo
puts
\'ok\'
end
end

puts x.poo # ok
puts y.poo # NoMethodError

הביטוי class << x גורם לנו להכנס ל scope של מהחלקה היחידנית. פשוט.
בכדי שלא יהיה משמעם, קיים הבדל בין שתי דרכי ההגדרה של מתודה על מחלקה יחידנית – הבדל שקשור לנראות של קבועים על המחלקה היחידנית. בצורת ההגדרה הקודמת  הפונקציה לא \”תראה\” את הקבועים שעל המחלקה היחידנית, ואילו בצורת ההגדרה הנוכחית – הם יהיו זמינים מחלקה. למה? מדוע? – לא חפרתי מספיק בכדי להבין…

הערה אחרונה בנושא: הגדרת נראות private למתודת מחלקה (class method) נעשית בצורה מעט שונה: לא ע\”י שימוש ב private שאנו משתמשים עבור מתודות מופע, אלא ע\”י שימוש ב private_class_method. שימוש מקובל ב directive הזה הוא להחביא את המתודה new:: בכדי להגדיר דפוס עיצוב של Singleton (של GOF).
ה directives של הנראות (private, public, protected) הן בעצם פונקציות של המחלקה Module המשפיעות על המטא-מודל של האובייקטים ברובי. מכיוון שמתודות מחלקה לא נמצאות באמת על המחלקה (אלא על המחלקה היחידנית של המחלקה) – זקוקים למנגנון מעט שונה בכדי לשנות את הנראות שלהם מבלי לסבך. מבלי לסבך את המפשן של רובי, התכוונתי.

זוכרים שהזכרנו בתחילת הקטע את המטאפורה של המחלקה כ\”שק של תכונות\”?
בואו נראה התנהגות זו בפעולה:

class MyClass
def foo
puts
\'aleph\'
end

def foo
puts
\'beth\'
end
end

x = MyClass.new
x.foo
# beth

הגדרנו מתודה בשם foo, ואז הגדרנו אותה שוב.
התוצאה? דריסה של רישום המתודה הראשון ברישום השנה – וזה מה שנשאר.

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

הנה דוגמה נוספת:

class MyClass
def foo
puts
\'aleph\'
end
end

x = MyClass.new

class MyClass
def goo
puts
\'beth\'
end
end

y = MyClass.new
x.foo
# aleph
x.goo # beth
y.foo # aleph
y.goo # beth

הגדרנו את המחלקה MyClass ואז הגדרנו אותה שוב?
הפעולה הזו נקראת ברובי \”re-opening a class\”. הגדרה נוספת של מחלקה לא \”דורסת\” את רישום המחלקה הקיים, אלא גורמת לרישום חדש או נוסף של כל ה members לאותו \”שק\” של מתודות שנקרא MyClass.

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

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

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

האם יש יכולת דומה בשפות אחרות?
בג\’אווהסקריפט ניתן לעשות אותו הדבר – שינוי של ה prototype של האובייקט (= בערך מחלקה ברובי) מכל מקום במערכת, וזה נחשבת פרקטיקה לא טובה.
ב #C יש Partial Classes שזה סוג של פיצול מחלקה לכמה קבצים שונים – אבל השימוש הנפוץ הוא שחלק אחד נוצר מ code generation והשני – מתוחזק בצורה ידנית, והם יושבים במבנה הפרוייקט זה לצד זה.

בקיצור:ע\”פ כל קריטריטיון שאני מכיר, כדאי להמנע מלהשתמש ביכולת ה\”פתיחה מחדש של המחלקה\” – ולשמור על יכולת ההתמצאות (orientation) וההבנה הקלה של המערכת. שימוש אחד שיכול להיות סביר להרחבת מופעים והוספת singleton_methods היא unit-testing ו mocking. עדיף קוד שלא דורש זאת – אך יש כמה מצבים שבהם אני מעריך שהייתי שמח להשתמש ביכולות הללו בבדיקות.

מודולים

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

module NicePrinter
def putz(msg)
puts
\'[\' + msg + \']\'
end
end

class
MyClass
include NicePrinter

def fooz
putz
\'yay!\'
end
end

MyClass.new.fooz # [yay!]

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

דרך נוספת לשלב מודול במחלקה הוא בעזרת extends

module NicePrinter
A = 1
def putz(msg)
puts
\'[\' + msg + \']\'
end

module NiceSpacer
B = 2
def add_spaces(str)
str.scan(
/./).join(\' \')
end
end
end

class
MyClass
include NicePrinter
extend NicePrinter::NiceSpacer

def fooz
putz
MyClass.add_spaces(\'yay!\')
end
end

x = MyClass.new

x.fooz
# [y a y !]
puts MyClass::A # 1
puts MyClass.singleton_class::B # 2
puts MyClass::NiceSpacer::B # 2

בדוגמה הזו עשינו include כמו בדוגמה הקודמת, וגם עשינו extend ל מודול המקונן NiceSpacer (ניתן לעשות extends לכל מודול – פשוט רציתי להראות קינון של מודולים \”על הדרך\”).

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

extend, בניגוד ל include, מוסיף את המתודות שעל המודול להיות class methods. להזכיר: include מוסיף את המתודות שעל המודול להיות instance methods. כנ\”ל לגבי קבועים.

שימו לב שהקבוע B נוסף לנו פעמיים:

  • בפעם הראשונה (כרונולוגית) – כחלק מה include: כאשר עשינו include נוספף המודול NicePrinter וכל המודולים המקוננים שלו (במקרה שלנו: NiceSpacer).
  • בפעם השניה פעם על המחלקה היחידנית של MyClass – כאשר השתמשנו ב extends

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

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

ניתן להגדיר צורות שונות של תלויות בין מודולים כך שחיבור של אחד למחלקה (ע\”י include או exclude) בעצם יחבר גם מודולים אחרים, או סתם \”יפתח\” (re-open) את המחלקה שמוסיפה את המודול ויבצע בה שינויים.

יכולות אלו שימושיות מאוד לבניית DSL – אבל כדאי מאוד להיזהר בהן בכתיבת \”תוכנה בסיסית\”. לכו תבינו שמתודה שלכם מתנהגת אחרת כי מודול שנוסף, גרר מודול אחר – שעושה שינוי ב members של המחלקה…
התחכמות (cleverness) ב Metraprogramming של רובי היא סגולה ללילות לבנים ללא שינה. ראו הוזהרתם!

מודולים משמשים גם כ namespace (כמו ב ++C או #C), ניתן לאגד בתוכם מחלקות, פונקציות, וקבועים – תחת שם שלא יתנגש עם מחלקות, פונקציות, וקבועים אחרים:

module MyModule
class MyOtherClass
def goo
puts
\'wow!\'
end
end
end

class
MyClass
include MyModule
end

x = MyModule::MyOtherClass.new
x.goo
# wow!

y = MyClass::MyOtherClass.new
y.goo
# wow!

האם ניתן לעשות include ו/או extend גם למודולים כאלו שמכילים מחלקות? – בוודאי. חשבו על Module ומחלקה כ hash (\”שק תכונות\”) שניתן להרכיב אותם (עם כמה מגבלות) אחד על השני.

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

מתודות ו\”שליחת הודעות\”

בשונה משפות כמו ג\’אווה / ++C בהם מתייחסים לx.foo כ \”method invocation\”, ברובי (כמו ב Smalltalk או Objective-C) מתייחסים להפעלת מתודות כשליחת הודעות.

למשל:

class MyClass
def foo
puts
\'yay!\'
end
end

x = MyClass.new

x.foo
# \'yay!\'
x.send :foo # \'yay!\'

קראנו ל foo ב 2 אופנים:

  • x.foo היא רמת ההפשטה הגבוהה, \”כאילו\” foo היא תכונה של המופע x.
  • x.send :foo היא האופן בו הדברים קורים בפועל ברובי – שליחת הודעה בשם foo ל x.

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

הנה כמה הבדלים עקרוניים:

  • ניתן לשלוח כל הודעה (בעזרת המתודה send) שראינו למעלה. זהו בעצם syntactic sugar למה שמתרחש באמת.
  • ניתן להענות לכל הודעה, גם ל\”הפעלת\” מתודה שלא הוגדרה מראש במחלקה – ע\”י מימוש המתודה method_missing במחלקה.
  • בעזרת method_missing ניתן לעכב הודעות, להתעלם מהודעות, וכו\’.

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

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

    הכלי של method_missing מאפשר כח רב ברובי – להענות למתודות שלא הוגדרו מראש על ידי המחלקה.
    מימוש ברירת המחדל של method_missing הוא לזרוק exception, והוא נמצא בתוך module בשם kernel ש\”מחובר\” למחלקה Object.

    האם שתי דרכי הפעולה זהות?
    לא. אם תקחו את הדוגמה למעלה ותהפכו את foo למתודה private – תראו שדרך ההפעלה הראשונה (בעזרת נקודה) – נכשלת, אבל הדרך השניה (send) מצליחה. מדוע?

    אכיפת ה visibility ברובי נעשית כאשר משתמשים במנגנון הנקודה, ו send פשוט עוקפת את המנגנון הזה. send היא בעצם מתודה public של המחלקה Object (הבת הישירה של BasicObject) – שתפעיל את send_internal של המפרשן של רובי.
    כלומר: ע\”י send ניתן לקרוא, מכל מקום בקוד, למתודות private של מחלקות אחרות. מה עוצר מבעדנו לכתוב קוד שקורא ל private members של מחלקות אחרות ללא מגבלה? – משמעת עצמית בלבד.

    היררכיות ההורשה ברובי

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

    • ברובי יש מנגנון של הורשה (inheritance) בין מחלקות – וזו הורשה יחידה.
    • ברובי, כפי שראינו, יש מנגנון של ה Modules שהוא מנגנון של mixins.
    • ראינו שיש גם מחלקות יחידניות (singleton classes) שהן אלמנט טכני, \”מאחורי הקלעים\”, אבל הידיעה אודותיהן עוזר להבין כמה מההתנהגויות של רובי – שאחרת היה קשה להבין.

    הנה דוגמה פשוטה של הורשה:

    class Person
    def say_my_name
    puts
    \'john smith\'
    end
    end

    class
    Employee < Person

    end

    Employee.new.say_my_name # john smith

    אין פה שום דבר מפתיע.

    בואו נסבך מעט. האם אתם יכולים להסביר את התוצאה הבאה?

    module SongsWeLike
    def say_my_name
    puts
    \"destiny\'s child\"
    end
    end

    class
    Person
    include SongsWeLike
    end

    class
    Employee < Person

    end

    Employee.new.say_my_name # destiny\'s child

    puts Employee # Employee
    puts Employee.superclass # Person
    puts Employee.superclass.superclass # Object

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

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

    בדוגמה למעלה, כאשר Person חיבר את המודול SongsWeLike, המפרשן יצר מחלקה יחידנית ורשם אותה כ superclass של המחלקה Person.

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

    הנה האופן שבו נראית ההיררכיה באמת:

    אם נפלט לכם עכשיו במקרה \”?WTF\” – זה בסדר. זו באמת היררכיה גדולה, הכוללת כמה אלמנטים טכניים. נסביר את הכללים:

    • במידה ויש למחלקה שלנו מחלקה יחידנית משלה (נניח שהגדרנו מתודה ישירות על המופע) – היא תכנס להיררכיה מעל המופע.
    • כפי שאמרנו כבר: כל מודול אותו מחברים למחלקה – מוסיף מחלקה יחידנית מעל המחלקה שהוסיפה אותו. המחלקה היחידנית מחוברת למודול וחושפת את המתודות / קבועים שלו.
      • משמעות ראשונה של המבנה הזה הוא שמודול לעולם לא יוכל \”להסתיר\” מתודות של המחלקה עם אותו השם: כיוון החיפוש אחר מתודה הוא מלמטה למעלה – תמיד קודם ניתקל במחלקה ורק אז במודולים שהיא הוסיפה.
      • אם מוסיפים למחלקה כמה מודולים – אזי המחלקות היחידניות של המודולים יתווספו להיררכיה בסדר הפוך לסדר החיבור שלהם למחלקה, קרי LIFO. 
    • המבנה הזה אולי נראה גדול ומורכב – אבל הוא מפשט את ה runtime של רובי: בעת קריאה למתודה, המפרשן פשוט צריך לטייל על ההיררכיה מלמטה למעלה – בלי \”לחשוב הרבה\”. פעולה של חיפוש אחר מתודה מתרחשת תכופות בזמן ריצה – ולכן חשוב מאוד שהיא תתבצע במהירות.

    super

    מה קורה כאשר אנו רוצים להרחיב מחלקת-אב, אך עדיין להשתמש בחלק מהפונקציונליות שלה?
    בדומה לג\’אווה (שיש את this), יש ברובי מילה שמורה בשם super.

    class MySuperClass
    def foo(num)
    puts
    \'super \' + num.to_s
    end
    end

    class
    MyClass < MySuperClass
    def foo(n)
    puts
    \'duper:\'
    super
    super n + 1
    end
    end

    MyClass.new.foo 7 # duper:, super 7, super 8

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

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

    class MySuperClass
    def foo(num)
    puts
    \'super \' + num.to_s
    end
    end

    class
    MyClass < MySuperClass
    def foo(n)
    n
    = 0
    super
    super n + 1
    end
    end

    MyClass.new.foo 7 # super 0, super 1

    אני מניח שהשיקול התכנוני מאחורי התנהגות זו הוא צמצום הגודל של ה local table (המקבילה ברובי ל Activation Frame של ++C) – תחת ההנחה שמדובר במקרה קצה שלא יבלבל הרבה מפתחי רובי.

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

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

    האם סיימנו עם ההפתעות? – חס וחלילה!

    ניתן לחשוב בתמימות ש super תמיד קורא למתודה של ה superclass עם אותו השם. זה הגיוני, אבל אם אתם זוכרים ההיררכיה יכולה להכיל מחלקות יחידניות – מה שעלול להוביל להתנהגות לא צפוייה ו/או לא רצויה.
    במקום זאת, בקריאה ל super רובי תפעיל את אלגוריתם החיפוש אחר מתודה עם השם של המתודה ממנה היא הופעלה – החל מה superclass של המחלקה הנוכחית ומעלה. כלומר: תעלה לאורך שרשרת ההורשה עד שהיא מוצאת מתודה בשם של המתודה ממנה היא נקראה. אם היא הגיעה ל BasicObject ולא מצאה מתודה בשם הצפוי – היא או תתחיל לחפש (שוב – החל מה superclass שלנו) מתודה בשם method_missing.

    מצד אחד – זה הגיוני, מצד שני – עלול להפתיע.

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

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

    ריבוי-צורות (polymorphism) ברובי

    ל OO יש שלושה עקרונות בסיסיים:

    • הכמסה – דיברנו!
    • הורשה – דיברנו!
    • ריבוי צורות…. – נדבר מייד.
    חשוב לציין שריבוי צורות נחשב עיקרון חשוב יותר מהורשה. רעיון ההורשה הוא פשוט \”פוטוגני\” יותר – ולכן מקבל יותר \”זמן מסך\” בספרות ה OO.
    ובכן… כבר ציינו בתחילת הפוסט שבשפת רובי אין מבנים המקבילים ל interface או abstract class בג\’אווה. כיצד אם כן ניתן להשיג ריבוי-צורות בלעדיהם??

    בעצם – האם איי פעם שפה מנעה מאיתנו יישום של רעיונות בכך שלא סיפקה לנו כלים מוכנים ליישם אותם? עיכבה – כן, אבל לא מנעה. חשבו כיצד מתכנתי ג\’אווהסקריפט שמיישמים encapsulation בשפה – למרות שהיא לא קיימת ככלי בשפה (למי שלא מכיר: ע\”י דפוס עיצוב בשם Module / Revealing Module או פשוט ע\”י קונבנציה של קו תחתון בתחילת מתודות שהן \”פרטיות\” – ומשמעת שאין לגשת אליהן מבחוץ).

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

    הנה ה\”דפוס\” המקובל ליישום ריבוי-צורות ברובי:

    class AbstractHerald
    def announce
    raise
    NotImplementedError, \'You must implement the announce() method\'
    end
    end

    class
    OptimisticHerald < AbstractHerald
    def announce
    puts
    \'life is good!\'
    end
    end

    class
    PessimisticHerald < AbstractHerald
    def announce
    puts
    \'... life sucks!! :(\'
    end
    end

    x = OptimisticHerald.new
    puts x.is_a?
    AbstractHerald # true
    puts x.is_a? Hash # false

    האכיפה על כך שנממש את המתודה announce – נמצאת ב runtime (מוזר בג\’אווה, טיפוסי ברובי).
    בעזרת ?is_a – אנו יכולים לבדוק אם מופע שביידנו יורש ממחלקה מסויימת.

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

    מה עם ריבוי ממשקים?
    נדמיין לרגע מחלקה בג\’אווה בשם User שניתן לגשת אליה דרך הממשק UserInfo לפעולו ת של קריאת נתונים, ודרך הממשק UserMaintainance – לפעולות עדכון על פרטי המשתמש (היא מממשת את שני הממשקים). המחלקות השונות במערכת מוגבלות לעשות על User רק את מה שהממשק שבידן מאפשר להן – הגבלה שנאכפת ע\”י השפה.

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

    כיצד עושים זאת? המתודה ?respond_to בודקת האם מחלקה (וההיררכיה שלה, המודולים שמחוברים אליה, וכו\’) יודעים לטפל במתודה (ע\”פ השם שלה – לא ע\”פ מספר הפרמטרים!).
    אם השתמשתם ב method_missing יהיה עליכם לדרוס את המתודה ?respond_to_missing ולהצהיר על אלו מתודות אתם מגיבים – בכדי ש ?respond_to תמשיך לספק תשובה נכונה (מזכיר את equals ו hash בג\’אווה).

    puts x.respond_to? :announce # true
    puts x.respond_to? :quit_job # false

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

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

    בפועל, סביר שתשתמשו ב ?respond_to בנקודות בהן:

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

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

    מה עושים כדי למנוע בכל-זאת תקלות? הרבה מאוד unit tests.
    למפתחי ג\’אווה זה עשוי להשמע מחריד: \”שפה שאנו לא סומכים עליה ב 100%?!\”, אולם מניסיוני בג\’אווהסקריפט – המצב הזה עשוי לשרת דווקא לטובה.
    הקומפיילר של ג\’אווה מספק ביטחון מסוים (בעצם: תחושת ביטחון מסוימת) – שמהווה מכשול למפתחי ג\’אווה להשקיע הרבה בבדיקות יחידה / אוטומציה. דווקא בשפות דינאמיות שהמפתחים מודעים יותר לכך שהם \”חשופים לתקלות\” – המוטיבציה להשקעה בבדיקות גבוהה יותר, ולעתים גם קל יותר לכתוב ולתחזק בשפות הללו את הבדיקות – מה שיכול להוביל בסה\”כ למערכת אמינה יותר [ב].

    Structs

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

    אתם אולי מכירים structs מ ++C או #C – שם \”הקטע\” שלהם היא רציפות בזכרון / יכולת שמירה על ה stack של ה thread. ברובי אין כאלו דברים – והשיקולים הם שיקולים של מהירות פיתוח ותחזוקה. Struct נמצא על הרצף בין Hash (מבנה הנתונים מטיפוס Dictionary – ללא מתודות או קבועים, או הורשה) למחלקות. הוא קצת יותר מ Hash וקצת פחות ממחלקה.

    Struct הוא בעצם generator של מחלקה פשוטה, שכוללת כמה משתני מופע – ו getters / setter למשתנים הללו. היא חוסכת, מצד אחר, הקלדה של מחלקות משעממות, ומצד שני מספקת מבנה מעט יותר יציב / מוגדר-היטב מ Hash. היתרונות העקריים הם:

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

    class MyClass
    MyMessage = Struct.new(:source, :target, :text)

    def foo
    some_condition
    = false
    MyMessage.new(\'a\', \'b\', \'c\') unless some_condition
    end

    def goo
    MyMessage.new \'a\', \'b\' # text will be nil
    end

    end

    msg = MyClass.new.foo
    puts msg.text
    , msg.source # c, a

    קצת חבל שה IDE בו אני משתמש, RubyMine, לא מספק auto-complete ממשי ל Structs.

    אם אתם רוצים להוסיף איזו מתודה או שתיים ל struct – אפשר.
    Struct::new יכולה לקבל גם בלוק (של מתודות קבועים) ותמיד אפשר \”לפתוח את ה Struct להרחבה\”. כל עוד זה נעשה בצמוד ליצירה שלו – אני מאשר. 🙂

    הנה אופן השימוש בבלוק:

    MyMessage = Struct.new(:source, :target, :text) do
    def encryptText
    self.text = self.text.gsub(/./, \'*\')
    end
    end

    msg = MyMessage.new \'me\' , \'you\', \'a secret\'
    msg.encryptText
    puts msg.text
    # ********

    סיכום

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

    סקרנו את תכנות מונחה-העצמים ברובי ואני מקווה שהסקירה, למרות שנדחסה כולה לפוסט אחד – היא עדיין מספיק מקיפה ועמוקה.
    שמעו: רובי היא שפה מורכבת. יותר מפייטון (ברור!), יותר מג\’אווהסקריפט (דאאא), ויותר מג\’אווה. אני משווה את המורכבות שלה רק ל ++C (אני לא משווה עדיין ל Scala…). זה לפחות הרושם שלי – בתור אחד שכתב בכולן (לא ממש בסקאלה).

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

    ——

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

    http://rubymonk.com/learning/books מקור טוב ללמידת רובי לעומק

    ——

    [א] אם אתם רוצים לראות את שרשרת ההיררכיה של מחלקה – פשוט הפעילו עליה את המתודה ancestors.

    [ב] מפתחי ג\’אווה, ענו לעצמכם בכנות: איזה אחוז משגיאות הקוד המשמעותיות הקומפיילר \”תופס\”? ואיזה אחוז הוא לא?



    התיאוריה המאוחדת: קוד, תכנון וארכיטקטורה

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

    • חלוקת המערכת למודולים / תתי מערכות
    • ניהול תלויות (בין המודולים)
    • יצירת הפשטות (Abstractions) והגדרת API
    • תיעוד הארכיטקטורה.
    כמעט כל העקרונות והטכניקות של הגדרת ארכיטקטורה (למשל Quality Attributes או חלוקה ל Views) הן הנחיות כיצד לבצע פעולות בסיסיות אלו בצורה נכונה יותר.”ניתוח Quality Attributes” היא טכניקה שכתבתי עליה בפוסט הזה והזה.

    תכנון מונחה אובייקטים – OOD

    תחום ה Object Oriented Design הוא תולדה של רעיונות שהתפרסמו במספר ספרים / מאמרים משפיעים – אך בניגוד למה שניתן לחשוב, אין הגדרה “חד-משמעית וברורה” מהם “עקרונות ה Object Oriented Design”.
    2 המודלים המקובלים ביותר להגדרת OOD כיום הם:

    חוצמזה ניתן בהחלט להזכיר את תנועת ה Patterns (קרי POSA, PLOP, GOF) שלא ניסחה חוקים אלא “תיעדה” תבניות עיצוב מוצלחות – אבל יש לה השפעה ניכרת על הדרך בה אנו עושים היום Design (ולא תמיד לטובה).

    העשור האחרון

    זרם ה Agile השפיע גם הוא רבות על OOD וקידם כמה רעיונות:
    • “כשאתה מקודד – אתה בעצם עושה Design” (מקור: TDD) –> ומכאן רעיונות כמו “Design by Tests/Coding”
    • ההכרה שביצוע Design או הגדרת ארכיטקטורה הם Waste – שיש לנסות ולייעל אותם (“Just Enough Software Architecture”)
    • ההבנה שחיזוי העתיד הוא דבר בלתי-מציאותי, גם על ידי אנשים נבונים למדי, במיוחד במוצרים חדשים אך גם במוצרים קיימים. ירידת קרנם של “העיקרון הפתוח-סגור” (מתוך SOLID) ו “(Predictable Variations (PVs” (מתוך GRASP) והצבת סימני שאלה בפני כמה מהעקרונות האחרים…

    התאוריה המאוחדת

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

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

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

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

    אפשר לראות שמות שונים (“גבוהים” ו”נמוכים”) לרעיונות דומים – אבל ההקבלה הרעיונית יפה למדי.

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

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

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

    עקרון ארכיטקטוני: Interface Segregation Principle 
    עקרון זה אומר שמודול לא אמור להיות תלוי ב interfaces רבים שאינם בשימוש. אם נוצר מצב כזה – יש לפצל את ה Interfaces כך שהמודול יהיה תלוי, עד כמה שאפשר, רק ב Interfaces שהוא משתמש בהם בפועל. העיקרון נכון מאוד גם למתודות בתוך interface יחיד (רמת ה Design) או לפרמטרים בחתימה של פונקציה בתוך הקוד (רמת הקוד).

    עוד היבט קוד שאני מאמץ מעקרון ה ISP הוא לנסות ולהימנע משימוש בספריות (כגון “Open Source”) שיש לי מעט שימוש בהן. אני אשתדל לא להכליל במערכת ספרייה של אלף שורות קוד – אם אני משתמש רק ב 50 מהן. אני מעדיף למצוא ספרייה אחרת או אפילו לכתוב אותן שורות קוד לבד. מדוע? א. סיכוי לבעיות מ 950 שורות קוד לא רלוונטיות, ב. מסר לא ברור האם נכון “להשתדל” להשתמש במתודות אחרות בספריה או לא. ג. אם צריך לשנות / לדבג – ייתכן וצריך להבין הרבה קוד בדרך שלא רלוונטי למקרה שלנו.

    אפשר להראות כמה דוגמאות של עקרונות ש”עוברים פחות נקי”:

    • מקבילה ל”ניהול תלויות” ברמת הקוד – לא מצאתי בדיוק. הסתפקתי בעקרון של הימנעות ממשתנים גלובליים.
    • לרעיון של חלוקת מודולים ע”פ “Unit Of Work” (כך שקבוצות פיתוח שונות יוכלו לעבוד במקביל עם מינימום תלות) – אני לא חושב שיש הקבלה אמיתית ברמת קוד.
    • העיקרון האלמותי של (DRY (Do Not Repeat Yourself הוא “No Brainer” בקוד, אבל הופך לנושא מורכב ולא חד-משמעי ברמת הארכיטקטורה.

    בסופו של דבר – יש הרבה מאוד חפיפה בין עקרונות “ארכיטקטורה” לעקרונות “קוד”, כך שאין כ”כ חדש ללמוד. הרעיונות חוזרים על עצמם בשינוי אדרת. חלק מהעקרונות (למשל KISS = Keep It Simple Stupid) הם פשוט אוניברסליים.

    עדכון: אף עיקרון בעצם לא דורש ש”נגרום לתוכנה לעבוד”. כן, כן! גם זה חלק בעל חשיבות 🙂 גם בקוד, גם בתכנון וגם בארכיטקטורה. עד כמה שזה נשמע משעשע – לעתים אנחנו שוכחים את זה (בעיקר “ארכיטקטים”).

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

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

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

    קושי מסוים בשימוש ב SLAP הוא שאין “מד רמת-הפשטה”, כזה שנכוון אותו לשורה בקוד והוא יגיד לנו “רמה 6!” או “רמה 7!”. זה הכל בראש שלנו כמפתחים ובני-אדם אינטליגנטים. לפעמים יהיו “סתירות” כך שיוחלט שפעולה X תהיה פעם אחת ברמה n ופעם אחרת ברמה n+1. אני אומר: זה אנושי. זהו עקרון חשוב – פשוט קחו אותו בפרופורציה (“We figured they were more actual guidelines”).

    סיכום

    כפי שאולי אתם שמים לב, התחלתי לאחרונה לתקוף את נושא הארכיטקטורה וה OOD. זה נושא גדול ומורכב, עם זוויות רבות לכל עניין.
    במקום להסתגר כשנה וחצי ולהוציא בסוף פוסט באורך של ספר (גישת ה Waterfall), אני מנסה לתקוף את הנושא בצורה אג’ילית: בעזרת nibbles (“ביסים קטנים”). כמו ערמה של דוקים לפרק אחד אחד – עד אשר אוכל להגיע לגרעין הקשה של העניין.

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

    פ.ס. : הערות, מחשבות, ביקורות – יתקבלו בהחלט בשמחה!

    שאלות על Object Oriented Desgin

    לפני כשבוע נתקלתי בוויכוח הבא:

    במערכת כלשהי, באזור לו נקרא “Sub-Project 3”, מחלקה A (בג’אווה) קראה למתודה במחלקה B, אשר דרשה כפרמטר איזה ערך. הערך יכול להיות אחד מ 3 ערכים קבועים – ועל כן המפתחים יצרו enum (נקרא לו ENUM_X). עד כאן – יפה וטוב.

    מפתח אחר גילה שבדיוק אותו enum (נקרא לו ‘ENUM_X) מוגדר במקום אחר בפרויקט, ודרש שמחלקות A ו B ישתמשו ב enum המקורי – כך שלא יתוחזק “קוד כפול”.

    המפתח אשר כתב את הקוד במקור טען: “חבל לייצר reference לעוד תת פרויקט ב Build בשביל כזה דבר קטן. שיהיו שני enums זהים – לא יקרה שום דבר.”- “אבל אם תשנה אחד ותשכח את השני?! – מה אז?”

    הוויכוח התלהט והגיע לראש הקבוצה (!).

    מה דעתכם? במי אתם הייתם מצדדים?

    כיצד לסיים את הוויכוח?

    לפני שאספר לכם מה הייתה הצעתי (שנדחתה פה-אחד ע”י 2 הצדדים, כדרך אגב) ארחיב את הדילמה:

    מי שקצת בקיא ב”תיאוריה” של הנדסת תוכנה או Object Oriented Design (בקיצור OOD) – יכול לטעון:
    “שכפול קוד הוא אם כל רוע”. “יש עיקרון חשוב שאומר שאין לשכפל קוד: כל שינוי קונספטואלי צריך להתרגם בדיוק לנקודה אחת בקוד בה עושים שינוי”. “עקרון זה נקרא Don’t Repeat Yourself Principle (בקיצור DRY) – וזהו עיקרון ידוע.”
    קל להתחבר לטיעון הזה: אותו מכירים אותו, כנראה, מקורס התכנות הראשון שלנו.

    האם זהו הטיעון המנצח שיפתור את הדיון?

    הממ… לא בטוח.
    הנה טיעון מלומד אחר:

    “אסור למודול להיות תלוי בחלקי-ממשק שאין לו בהם שימוש”. במקרה שלנו יצרנו תלות לא רצויה בכל “Sub-Project 7” – כלומר בהרבה מחלקות וממשקים שאין לנו בהם שימוש. הממ… נשמע חמור!
    עיקרון זה נקרא The Interface Segregation Principle.

    האם ייתכן שעקרונות ה OOD סותרים זה את זה?

    כמה שאלות

    • האם יכול אדם, המכיר את 2 העקרונות והוא בעל כושר שכנוע, להחליט באופן רגשי במי הוא מצדד וכל פעם לשלוף את “הטיעון התאורטי המתאים” בכדי להנחית “טיעון מנצח”? האם הוא יכול לעשות זאת מבלי להיות מודע לכך ולהאמין שהוא “רק פועל ע”פ התאוריה”?
    • בהינתן שחוקי ה OOD סותרים לעתים אחד-את-משנהו, האם ישנם חוקים “חזקים יותר” שיש להעדיף?
    • נניח ונוותר על אחד החוקים או שניהם – איזה “נזק” יתרחש? מה ההשלכות של “לא לציית לחוקים”? האם המאמץ הנוסף שבציות לחוקי ה OOD – משתלם?
    • האם OOD היא מתודולוגיה מוצלחת? האם, לאחר כל השינויים בשיטות העבודה שחלו בעשור האחרון – היא עדיין יעילה או רלוונטית?
    עסקתי הרבה בחיי ב Object Oriented Design: למדתי, למדתי עוד, ניסיתי, יישמתי, שאפתי ליישום “מושלם”, הנחיתי אחרים במתודולוגיה וכו’.
    עדיין, ברגע זה, כשאני עומד ושואל את עצמי את השאלות הנ”ל – אין לי תשובה ברורה.
    במשך שנים, פעלתי ע”פ כללי הנדסת-תוכנה שלמדתי. פעלתי? – נלחמתי בחירוף נפש, אפשר לומר.
    ניסיתי להעמיק כמה שיותר ולעשות את המירב.
    כיום אני יודע לומר שנצמדתי במידה רבה, לזרם בתוכנה שנקרא “Defensive Programming”. זרם ששפת ג’אווה ו JEE היו אולי רגע השיא שלו. הוא מתבטא ברעיונות כגון:
    • “על המתכנת צריך להגן על התוכנה בפני המפתחים – כולל הוא עצמו”.
    • עשה כל מה שתוכל כדי להפחית סיכונים לבאגים.
    גישה זו יצרה הרבה משמעת (discipline), אך גם הובילה להמלצות כגון כתיבת מחלקה singleton בג’אווה בתוך enum על מנת להבטיח singleton “שפשוט אי אפשר לקלקל” [א].
    מאז, נחשפתי לזרמים אחרים, אולי כמעט הפוכים – שגם הם יכולים לעבוד יפה. הבנתי (פעם נוספת) שאין אמת אחת.

    עקרונות ה OOD – למבחן!

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

    חלוקה הקוד למחלקות או מודולים

    • (The Single Responsibility Principle (SRP
    • (Don’t Repeat Yourself Principle (DRY
    • Encapsulation
    • High-Cohesion / Low-coupling Principle
    • The Common Closure / Reuse Principle

    הגדרת הפשטות (abstactions) / אינטראקציה בין מחלקות

    • The Open-Closed Principle
    • The Liskov Substitution Principle
    • The Release-Reuse Equivalency Principle
    • The Stable Abstraction Principle
    ניהול תלויות (מחלקות עד מודולים במערכות)
    • The Interface Segregation Principle + גרסת הקוד שלו
    • (Single Layer Of Abstraction Principle (SLAP
    • The Dependency Inversion Principle
    • The Acyclic Dependencies Principle
    • The Stable Dependencies Principle
    עדכון: עקרונות אחרים של תכנון מערכת:

    עדכון 2: הנה איזו רשימה דומה של בחור אחר.

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

    נראה לי שאבחר כמה מהעקרונות הנ”ל – ואתחיל לנתח אותם יותר לעומק.
    הערות ומחשבות יתקבלו בשמחה.

    ליאור

    נ.ב: האא כן – איזה פתרון אני הצעתי לדילמה שהצגתי בתחילת הפוסט? אני הצעתי פשוט להשתמש במחרוזת (string) במקום enum וכך להימנע בכלל מתלויות. מה הייתרון של ENUM על מחרוזת פשוטה? Type Safety? ובכן… ה speller ב IDE ימצא את רוב שגיאות הכתיב. בדיקות-היחידה אמורות לתפוס את השאר…

    —-

    [א] בספר Effective Java 2nd Edition, פריט מס’ 3, ע”מ 18. אפשר להתייחס לכל סדרת ספרי ה … Effective – כספרים של זרם ה “Defensive Programming”.