תוכן עניינים הקדמה
הכרות עם schemes
תבניות נתונים
תבניות
משפטי בקרה
משתנים לקסיקלים
רקורסיה
קלט/פלט
macros
מבנים
alises and tables ממשק המערכת
מחלקות ואובייקטים
jumps
אי-דטרמיניסטיות
מנועים
shell scripts

אתר ללימוד מקיף> פרק 12 - מחלקות ואובייקטים

פרק 12 - מחלקות ואובייקטים


הנושאים בפרק זה:
12.1.     מערכת אובייקטים פשוטה
12.2.     מחלקות הן מופע בעצמן
12.3.     ירושה מרובה


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

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



לתחילת העמוד

12.1 מערכת אובייקטים פשוטה

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

יעיל להגדיר בהתחלה מחלקות ע"י שימוש במבנה הנקרא standard-class. מבנה זה מכיל שדות עבור התכונות, מחלקות העל והמתודות. שני השדות הראשונים ייקראו slots ו-superclass בהתאמה.
נשתמש בשני שדות עבור המתודות: שדה ה-method names - יחזיק את רשימת המתודות של המחלקה ואילו שדה ה-method-vector יחזיק את וקטור הערכים של מתודות המחלקה.

להלן הגדרה של standard-class:

(defstruct standard-class
  slots superclass method-names method-vector)

אנו יכולים להשתמש ב-make-standard-class שזו פרוצדורת יצירת standard-class כדי ליצור מחלקה חדשה.
לדוגמא:

(define trivial-bike-class
  (make-standard-class
   'superclass #t
   'slots '(frame parts size)
   'method-names '()
   'method-vector #()))

למחלקות מורכבות יותר יש מחלקות-על שאינן טריוויאליות ומתודות שיידרשו מס' רב של איתחולים סטנדרטיים שנרצה להסתיר במהלך תהליך יצירת המחלקה. לצורך כך נגדיר macro הנקרא: create-class שייבצע את הקריאה המתאימה ל-make-standard-class.

(define-macro create-class 
  (lambda (superclass slots . methods) 
    `(create-class-proc 
      ,superclass 
      (list ,@(map (lambda (slot) `',slot) slots)) 
      (list ,@(map (lambda (method) `',(car method)) methods)) 
      (vector ,@(map (lambda (method) `,(cadr method)) methods)))))

נדחה את הגדרת פרוצדורת create-class-proc לשלב מאוחר יותר.

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

(define make-instance 
  (lambda (class . slot-value-twosomes) 
 
    ;Find `n', the number of slots in `class'. 
    ;Create an instance vector of length `n + 1', 
    ;because we need one extra element in the instance
    ;to contain the class. 
 
    (let* ((slotlist (standard-class.slots class)) 
           (n (length slotlist)) 
           (instance (make-vector (+ n 1)))) 
      (vector-set! instance 0 class) 
 
      ;Fill each of the slots in the instance 
      ;with the value as specified in the call to 
      ;`make-instance'. 
 
      (let loop ((slot-value-twosomes slot-value-twosomes))
        (if (null? slot-value-twosomes) instance 
            (let ((k (list-position (car slot-value-twosomes)
                                    slotlist))) 
              (vector-set! instance (+ k 1)  
                (cadr slot-value-twosomes)) 
              (loop (cddr slot-value-twosomes))))))))

להלן דוגמא לקביעת מופע למחלקה:

(define my-bike
  (make-instance trivial-bike-class
                 'frame 'cromoly
                 'size '18.5
                 'parts 'alivio))

זה קושר את my-bike למופע:

#(<trivial-bike-class> cromoly 18.5 alivio)

כאשר <trivial-bike-class> הוא נתון ב-Scheme (וקטור נוסף) שהוא הערך של trivial-bike-class, כפי שמוגדר לעיל.

הפרוצדורה class-of מחזירה את המחלקה של המופע:

(define class-of
  (lambda (instance)
    (vector-ref instance 0)))

ההנחה היא כי הארגומנט של class-of יהיה מופע המחלקה. סביר להניח כי נרצה ליצור class-of שיחזיר ערך מתאים עבור כל סוג של אובייקט Scheme שנזין לתוכו.

(define class-of 
  (lambda (x) 
    (if (vector? x) 
        (let ((n (vector-length x))) 
          (if (>= n 1) 
              (let ((c (vector-ref x 0))) 
                (if (standard-class? c) c #t)) 
              #t)) 
        #t))) 

המחלקה של אובייקט Scheme שלא נוצר בעזרת standard-class תהיה t#, מחלקת האפס.
הפרוצדורות slot-value ו-set!slot-value ניגשות ומשנות את הערכים של מופע מחלקה:

(define slot-value
  (lambda (instance slot)
    (let* ((class (class-of instance))
           (slot-index
            (list-position slot (standard-class.slots class))))
      (vector-ref instance (+ slot-index 1)))))
 
(define set!slot-value
  (lambda (instance slot new-val)
    (let* ((class (class-of instance))
           (slot-index
            (list-position slot (standard-class.slots class))))
      (vector-set! instance (+ slot-index 1) new-val)))) 

כעת אנו יכולים לטפל בהגדרת create-class-proc. פרוצדורה זו לוקחת מחלקת-על, רשימת תכונות, רשימת שמות המתודות ווקטור מתודות ומבצעת את הקריאה המתאימה ל-make-standard-class. החלק המטעה היחיד הוא הערך הניתן לשדות התכונות. לא רק הארגומנטים של התכונות מסופקים ע"י create-class גם התכונות של מחלקת העל.
בנוסף עלינו לוודא שאין תכונות כפולות.

(define create-class-proc 
  (lambda (superclass slots method-names method-vector) 
    (make-standard-class 
     'superclass superclass 
     'slots      (let ((superclass-slots  
            (if (not (eqv? superclass #t)) 
                (standard-class.slots superclass) 
                '()))) 
       (if (null? superclass-slots) slots 
           (delete-duplicates 
            (append slots superclass-slots)))) 
     'method-names method-names 
     'method-vector method-vector))) 

הפרוצדורה delete-duplicates המופעלת על רשימה s, מחזירה רשימה חדשה שמכילה רק את המופע האחרון של כל אלמנט ב-s.

(define delete-duplicates
  (lambda (s)
    (if (null? s) s
        (let ((a (car s)) (d (cdr s)))
          (if (memv a d) (delete-duplicates d)
              (cons a (delete-duplicates d)))))))

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

(define send 
 (lambda (method instance . args) 
 (let ((proc 
       (let loop ((class (class-of instance)))
        (if (eqv? class #t) (error 'send) 
             (let ((k (list-position  
                     method 
                    (standard-class.method-names class))))
              (if k 
                (vector-ref (standard-class.method-vector class) k)
                (loop (standard-class.superclass class))))))))
      (apply proc instance args))))

כעת נוכל להגדיר מחלקות "מעניינות" הרבה יותר:

(define bike-class 
  (create-class 
   #t 
   (frame size parts chain tires) 
   (check-fit (lambda (me inseam) 
                (let ((bike-size (slot-value me 'size)) 
                      (ideal-size (* inseam 3/5))) 
                  (let ((diff (- bike-size ideal-size))) 
                    (cond ((<= -1 diff 1) 'perfect-fit) 
                          ((<= -2 diff 2) 'fits-well) 
                          ((< diff -2) 'too-small) 
                          ((> diff 2) 'too-big)))))))) 

כאן, מחלקת bike-class מכילה מתודה בשם check-fit, המקבלת כארגומנטים את האופניים ואת מידות נעלי בעליהם ומדווחת על התאמת האופניים לאדם.
נגדיר מחדש את my-bike:

(define my-bike
  (make-instance bike-class
                 'frame 'titanium ; I wish
                 'size 21
                 'parts 'ultegra
                 'chain 'sachs
                 'tires 'continental))

כדי לבדוק אם האופניים יתאימו לבעל מידת נעליים 32:

(send 'check-fit my-bike 32)

ניצור תת מחלקה ל-bike-class.

(define mtn-bike-class 
  (create-class 
    bike-class 
    (suspension) 
    (check-fit (lambda (me inseam) 
                (let ((bike-size (slot-value me 'size)) 
                      (ideal-size (- (* inseam 3/5) 2))) 
                  (let ((diff (- bike-size ideal-size))) 
                    (cond ((<= -2 diff 2) 'perfect-fit) 
                          ((<= -4 diff 4) 'fits-well) 
                          ((< diff -4) 'too-small) 
                          ((> diff 4) 'too-big)))))))) 

מחלקת mtn-bike-class מוסיפה תכונה הנקראת suspension ומשתמשת בהגדרות השונות במקצת עבור המתודה check-fit.



לתחילת העמוד

12.2 מחלקות הן מופע בעצמן

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

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

(define standard-class
  (vector 'value-of-standard-class-goes-here
          (list 'slots
                'superclass
                'method-names
                'method-vector)
          #t
          '(make-instance)
          (vector make-instance)))

יש שים לב כי הוקטור של standard-class מלא באופן חלקי בלבד:
הסימון value-of-standard-class-goes-here מתפקד כממלא מקום. כעת, כשהגדרנו את ערך standard-class, נוכל להשתמש בכך כדי להגדיר את המחלקות שלו, שהן בעצם הוא עצמו.
(vector-set! standard-class 0 standard-class)

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

עלינו כל הקריאות מהצורה:

(standard-class? x)
(standard-class.slots c)
(standard-class.superclass c)
(standard-class.method-names c)
(standard-class.method-vector c)
(make-standard-class ...)

בצורה הבאה:

(and (vector? x) (eqv? (vector-ref x 0) standard-class))
(vector-ref c 1)
(vector-ref c 2)
(vector-ref c 3)
(vector-ref c 4)
(send 'make-instance standard-class ...)



לתחילת העמוד

12.3 ירושה מרובה

קל לשנות את מערכת האובייקטים כך שתאפשר למחלקות לרשת מיותר ממחלקה אחת. נגדיר מחדש את standard-class כך שתהיה בה תכונה הנקראת class-precedence-list במקום superclass. ה-class-precedence-list של מחלקה מסוימת היא רשימה של כל מחלקות העל, לא רק מחלקת העל הישירה המצוינת במהלך יצירת המחלקה בעזרת create-class. השם מרמז כי מחלקות העל מסודרות בסדר מיוחד, כך שלמחלקות-על שנמצאות בתחילת הרשימה יש עדיפות על אלו שנמצאות בסוף הרשימה.

(define standard-class
	  (vector 'value-of-standard-class-goes-here
		   (list 'slots 'class-precedence-list 'method-names 
		   'method-vector)
		   '()
		   '(make-instance)
		   (vector make-instance)))

לא רק רשימת התכונות השתנתה כדי לכלול את התכונה החדשה, אלא גם תכונת ה-superclass תהיה עכשיו () במקום t#. זאת כיוון שה-class-precedence-list של ה-standard-class חייב להיות רשימה.
לכאורה היינו יכולים לציין זאת כ-(t#), אך אנו לא מזכירים את מחלקת האפס כיוון שהיא נמצאת בכל class-precedence-list של כל מחלקה. על המקרו create-class להשתנות כדי לקבל רשימה של מחלקות-על ישירות במקום מחלקת-על בודדת:

define-macro create-class
  (lambda (direct-superclasses slots . methods)
    `(create-class-proc
      (list ,@(map (lambda (su) `,su) direct-superclasses))
      (list ,@(map (lambda (slot) `',slot) slots))
      (list ,@(map (lambda (method) `',(car method)) methods))
      (vector ,@(map (lambda (method) `,(cadr method)) methods))
      )))

על ה-create-class-proc לחשב את ה- class-precedence-list ממחלקות העל הישירות ואת רשימת התכונות מה-class-precedence-list.

(define create-class-proc 
  (lambda (direct-superclasses slots method-names method-vector)
    (let ((class-precedence-list 
           (delete-duplicates 
            (append-map 
             (lambda (c) (vector-ref c 2)) 
             direct-superclasses)))) 
      (send 'make-instance standard-class 
            'class-precedence-list class-precedence-list 
            'slots 
            (delete-duplicates 
             (append slots (append-map 
                            (lambda (c) (vector-ref c 1)) 
                            class-precedence-list))) 
            'method-names method-names 
            'method-vector method-vector)))) 

הפרוצדורה append-map היא שילוב של append ושל map:

(define append-map
  (lambda (f s)
    (let loop ((s s))
      (if (null? s) '()
          (append (f (car s))
                  (loop (cdr s)))))))

על הפרוצדורה send לבדוק באמצעות ה- class-precedence-list משמאל לימין כאשר היא מחפשת מתודה כלשהי.

(define send 
  (lambda (method-name instance . args) 
    (let ((proc 
           (let ((class (class-of instance))) 
             (if (eqv? class #t) (error 'send) 
                 (let loop ((class class) 
                            (superclasses (vector-ref class 2)))
                   (let ((k (list-position  
                             method-name 
                             (vector-ref class 3)))) 
                     (cond (k (vector-ref  
                               (vector-ref class 4) k)) 
                           ((null? superclasses) (error 'send))
                           (else (loop (car superclasses) 
                                       (cdr superclasses)))) 
                     )))))) 
      (apply proc instance args)))) 

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