צורה לך

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

public class Rectangle {

   private double width, height;

   private double area, perimeter;

}

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

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

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

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

public void setWidth(double w) {

   if(w > 0)

                width = w;

}

פשוט לגמרי. האם אתם יכולים לראות בעייתיות כלשהי בשיטה?

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

//…

Rectangle r = new Rectangle(4, 5);    // creates rectangle with width = 4, height = 5

System.out.println(r.getAarea());                            // the area is 20

r.setWidth(3);

System.out.println(r.getArea());                               // the area is still 20…

מה קרה כאן? כיוון שיש לנו תכונה בשם area, הוספנו גם שיטה בשם getArea() שמחזירה את ערכה של התכונה. בזמן יצירת המלבן מחושב שטחו ולפי הנתונים הוא מוצב להיות 20 (רוחב כפול גובה). אבל בשלב מאוחר יותר בתוכנית אנחנו משנים את רוחב המלבן באמצעות השיטה setWidth שכתבנו קודם. השיטה משנה את רוחב המלבן ל-3. ברור ששטח המלבן אמור להיות עכשיו 15, אבל מה ששמור במשתנה area הוא עדיין 20… נוצרה פה שגיאה חמורה, כיוון שעכשיו אנחנו לא יכולים לסמוך על התכונות של האובייקט שיספקו תמונת מצב נכונה על מצבו הפנימי של האובייקט!

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

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

public double area() { return width * height; }

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

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

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