שפת רובי היא שפת 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) למחלקה היחידנית - יכולה להיות לו אחת כזו, לכל היותר.
באופן דומה, גם מתודות מחלקה שראינו קודם לכן הן בעצם "מתודות ישירות" על אובייקט המחלקה - ומאוכסנות במחלקה היחידנית של המחלקה. מבלבל?
הנה דרך נוספת להגדיר 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
האם יש למודולים עוד תכונות ויכולות? - כן.
ניתן להגדיר צורות שונות של תלויות בין מודולים כך שחיבור של אחד למחלקה (ע"י 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 ניתן לעכב הודעות, להתעלם מהודעות, וכו'.
אם לא מוצאים את המתודה גם שם - אז מתחילים מהתחלה, אבל הפעם מחפשים אחר מתודהה בשם 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 או פשוט ע"י קונבנציה של קו תחתון בתחילת מתודות שהן "פרטיות" - ומשמעת שאין לגשת אליהן מבחוץ).
הצורך בריבוי-צורות הוא ממשי וגם מתכנתי רובי משיגים אותו, עם מעט הכלים שהשפה מספקת (והיא מספקת כלי או שניים שלא הזכרנו עדיין).
הנה ה"דפוס" המקובל ליישום ריבוי-צורות ברובי:
בעזרת ?is_a - אנו יכולים לבדוק אם מופע שביידנו יורש ממחלקה מסויימת.
חשוב להזכיר שנטייה טבעית כשמגיעים לשפה חדשה היא לחפש את המבנים המוכרים. חשוב להיזהר ולא להגזים, לא "לכתוב ג'אווה ב syntax של רובי". שפת רובי היא דינאמית מאוד מטבעה - וחשוב להבין ולהתרגל לסגנון זה.
מה עם ריבוי ממשקים?
נדמיין לרגע מחלקה בג'אווה בשם User שניתן לגשת אליה דרך הממשק UserInfo לפעולו ת של קריאת נתונים, ודרך הממשק UserMaintainance - לפעולות עדכון על פרטי המשתמש (היא מממשת את שני הממשקים). המחלקות השונות במערכת מוגבלות לעשות על User רק את מה שהממשק שבידן מאפשר להן - הגבלה שנאכפת ע"י השפה.
ברובי אין סוג כזה של אכיפה, ואכיפת הממשקים (שקיימים בדמיון או התפיסה של המתכנתים) - נעשית בצד המחלקה המשתמשת. לפעמים.
כיצד עושים זאת? המתודה ?respond_to בודקת האם מחלקה (וההיררכיה שלה, המודולים שמחוברים אליה, וכו') יודעים לטפל במתודה (ע"פ השם שלה - לא ע"פ מספר הפרמטרים!).
אם השתמשתם ב method_missing יהיה עליכם לדרוס את המתודה ?respond_to_missing ולהצהיר על אלו מתודות אתם מגיבים - בכדי ש ?respond_to תמשיך לספק תשובה נכונה (מזכיר את equals ו hash בג'אווה).
לכאורה, כל פעם שאתם שאתם מקבלים כארגומנט אובייקט אחר שאתם לא בטוחים (כיצד ניתן להיות בטוחים!?) שהוא יודע לענות למתודה מסויימת כלשהי - עליכם לבדוק זאת קודם ע"י ?respond_to. זה "תיק" לא קטן.
בפועל, סביר שתשתמשו ב ?respond_to בנקודות בהן:
שווה לומר זאת פעם נוספת: ?respond_to לא תבדוק לאלו פרמטרים המתודה מצפה - אלא רק שקיימת מתודה בשם שצויין. כלומר: הבדיקה היא "בערך" ולא לגמרי מדוייקת.
בעצם - האם איי פעם שפה מנעה מאיתנו יישום של רעיונות בכך שלא סיפקה לנו כלים מוכנים ליישם אותם? עיכבה - כן, אבל לא מנעה. חשבו כיצד מתכנתי ג'אווהסקריפט שמיישמים 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 (שזו דרך מהירה מאוד, ושימושית מאוד בהמון מקרים) לתאר מבנה נתונים - אין מקום ידוע בו מוגדרת הסכמה: איזו שדות קיימים? כיצד בדיוק הם נקראים? הסכמה, פעמים רבות, היא צירוף כל המפתחות בהם עשו שימוש איפשהו בקוד.
קצת חבל שה IDE בו אני משתמש, RubyMine, לא מספק auto-complete ממשי ל Structs.
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...). זה לפחות הרושם שלי - בתור אחד שכתב בכולן (לא ממש בסקאלה).
שיהיה בהצלחה!!
------
[א] אם אתם רוצים לראות את שרשרת ההיררכיה של מחלקה - פשוט הפעילו עליה את המתודה ancestors.
[ב] מפתחי ג'אווה, ענו לעצמכם בכנות: איזה אחוז משגיאות הקוד המשמעותיות הקומפיילר "תופס"? ואיזה אחוז הוא לא?
[א] אם אתם רוצים לראות את שרשרת ההיררכיה של מחלקה - פשוט הפעילו עליה את המתודה ancestors.
[ב] מפתחי ג'אווה, ענו לעצמכם בכנות: איזה אחוז משגיאות הקוד המשמעותיות הקומפיילר "תופס"? ואיזה אחוז הוא לא?
אין תגובות:
הוסף רשומת תגובה