תיכנות מונחה עצמים 2


פורסם ב 22/05/2010 ע"י האקדמיה לפיתוח לאנדרואיד

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

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

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

שימו לב, במהלך השיעור נשתמש במונחים הבאים:
אובייקט = עצם מסוג מחלקה מסויימת שהגדרנו (לדוגמא Car myCar).
מתודה = פונקציה שנמצאת בתוך מחלקה מסויימת.

בואו נתחיל:

עיקרון 2 – הורשה (Inheritance)

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

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

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

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

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

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

[sourcecode language="java" l="12"]
public class Car {

protected String color;
protected int speed;
protected int gasInTank;

public void increaseSpeed()
{
this.speed++;
}

public void decreaseSpeed()
{
this.speed–;
}

public void addGas(int gasQuantity)
{
this.gasInTank = this.gasInTank + gasQuantity;
}

public int getGas()
{
return this.gasInTank;
}

public String getColor()
{
return this.color;
}

public Car (String carColor)
{
this.color = carColor;
this.gasInTank = 0;
this.speed = 0;
}
}
public class Cadillac extends Car {
public Cadillac(String carColor) {
super(carColor);
}

public void increaseSpeed()
{
if (this.speed <= 200)
this.speed++;
}
}

public class Susita extends Car {
public Susita(String carColor) {
super(carColor);
}

public void increaseSpeed()
{
if (this.speed <= 120)
this.speed++;
}
}
[/sourcecode]

שימו לב לדגשים הבאים:

1. שורות 1 עד 38 כמעט זהות להגדרת המחלקה Car שהגדרנו בשיעור הקודם, למעט שלוש שינויים שאסביר בהמשך.

2. בשורה 40 הגדרנו מחלקה חדשה בשם Cadillac אשר יורשת ומרחיבה (extends) את מחלקת Car. כלומר המחלקה Cadillac מקבלת את כל התכונות והפעולות של Car וזאת מבלי הצורך להגדיר אותם מחדש.

3. שורות 42 עד 44 מכילות את הבנאי של המחלקה, אך נתעלם כרגע ממנו ונדבר עליו בהמשך.

3. בשורה 46 "דרסנו" את המתודה increaseSpeed וכתבנו לה קטע קוד חדש, המתאים למכונית מסוג קדילאק.

4. בשורה 53 הגדרנו מחלקה חדשה נוספת בשם Susita אשר יורשת ומרחיבה את מחלקת Car.

5. שורות 55 עד 57 מכילות את הבנאי של המחלקה, אך גם ממנו נתעלם כרגע.

5. בשורה 59 "דרסנו" את המתודה increaseSpeed וכתבנו לה קטע קוד חדש, המתאים למכונית מסוג סוסיתא.

לסיכום, הגדרנו 3 מחלקות:
1. Car מחלקה המייצגת "מכונית", כל מכונית שנרצה למען האמת.
2. Cadillac מחלקה המרחיבה את "מכונית" ויוצרת מימוש ספציפי למתודה increaseSpeed שמתאים לקדילאק (כלומר, מהירות המכונית מוגבלת ל- 200 קמ"ש).
3. Susita מחלקה המרחיבה את "מכונית" ויוצרת מימוש ספציפי למתודה increaseSpeed שמתאים לסוסיתא (כלומר, מהירות המכונית מוגבלת ל- 120 קמ"ש).

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

אנדי
טוב, הוא בטח לא התכוון להגיד שאת קדילאק…

Protected

עכשיו, אחרי שהבנו כיצד עובדת הורשה בתיכנות מונחה עצמים, זה הזמן לחזור ולדבר שוב על כימוס.
שימו לב שבמחלקה Car ביצענו שינוי קטן בשורות 3,4,5 בכך שההגדרה של המשתנים שונתה מ- private ל- protected.
כפי שנאמר בשיעור הקודם, הגדרה של משתנה או מתודה כ- private אינה מאפשרת לאף אחד מחוץ למחלקה עצמה להשתמש בה. הדבר נכון גם למחלקה שיורשת אותה.
אך יחד עם זאת, יתכן ובהורשה יהיו מקרים שנרצה לתת רק למחלקה שיורשת ומרחיבה את האובייקט להשתמש באותם משתנים או מתודות. לשם כך משתמשים ב protected.

כלומר במידה ולא היינו משנים את ההגדרה שבשורות 3,4,5 ל- protected, המחשב לא היה מכיר את this.speed בשורות 43,44,51,52 והיה נותן לנו הודעת שגיאה.
protected בעצם נכנס תחת ההגדרה של כימוס אבל רלוונטי רק במקרה של הורשה. בכל מקרה אחר, משהו שהוגדר כ protected מתנהג כ private לכל דבר.

הדבר נכון גם לגבי מתודות. לדוגמא, במידה והמתודה increaseSpeed במחלקה Car היתה פרטית (private), לא היה ניתן להרחיב אותה במחלקות Cadillac ו Susita.

Super

עוד לגבי הורשה, שימו לב לדוגמא הבאה:

[sourcecode language="java" l="12"]
public class Car {

public void increaseSpeed()
{
this.speed++;
}

}

public class Cadillac extends Car {

public void increaseSpeed()
{
if (this.speed <= 200)
this.speed++;
}

}

Cadillac myCadillac = new Cadillac("Black");
myCadillac.increaseSpeed();
[/sourcecode]

כדי לקצר, הכנסתי כאן רק את קטעי הקוד הרלונטיים ומסביב שמתי נקודות.
בשורה 20 יצרנו אובייקט חדש בשם myCadillac מסוג Cadillac ובשורה 21 אנחנו קוראים למתודה increaseSpeed.
במקרה הזה, רק קוד ההרחבה שהוגדר ב Cadillac (שורות 12 עד 15) ירוץ ואילו הקוד באובייקט Car (שורות 4 עד 6) לא ירוץ.

אבל מה במקרה ובו קוד המחלקה ממנה אנו יורשים הוא קוד שכן נרצה להריץ?
לדוגמא, יתכן ובמתודה increaseSpeed במחלקה של Car אנחנו מבצעים סידרה שלמה של פעולות שרלוונטיות לכל סוגי המכוניות, כמו העלאת המהירות, הפחתה של הדלק ועוד…
ואילו במחלקה Cadillac נרצה לבצע את כל הדברים הללו כבסיס + דברים שקשורים רק לקדילאק שלנו, לדוגמא לוודא כי מהירות הרכב לא עוברת את ה- 200 קמ"ש.

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

[sourcecode language="java" l="12"]
public class Car {

public void increaseSpeed()
{
this.speed++;
}

}

public class Cadillac extends Car {

public void increaseSpeed()
{
if (this.speed <= 200)
super.increaseSpeed();
}
}

Cadillac myCadillac = new Cadillac("Black);
Cadillac.increaseSpeed();
[/sourcecode]

שימו לב לשינוי שביצענו בקוד של המתודה increaseSpeed בתוך Cadillac. אנחנו בודקים האם המהירות קטנה או שווה ל- 200 (שורה 14) ואם התנאי מתקיים, אנו קוראים ל super.increaseSpeed ואז בפועל המחשב מבצע את הקוד של Car.increaseSpeed (שורות 4 עד 6).

באופן זהה לחלוטין, עובד הנושא גם בבנאים. שימו לב לדוגמא הבאה:

[sourcecode language="java" l="12"]
public class Car {

public Car(String carColor)
{
this.color = carColor;
}

}

public class Cadillac extends Car {

public void Cadillac()
{
super("Black");
}

}
[/sourcecode]

בשורה 12 הגדרנו למחלקת Cadillac בנאי, אשר קורא באופן יזום בעזרת super (שורה 14) לבנאי של Car (שורה 03). שימו לב שאלו הן השורות שביקשתי שתתעלמו מהן בדוגמא הראשונה שהבאנו בשיעור, למעלה.
יתרה מכך, כאשר למחלקת האב (Car במקרה שלנו) יש בנאי המקבל פרמטר (carColor), חובה עלינו לקרוא לבנאי האב (Car) באופן יזום, מתוך הבנאי של מחלקת הבן (Cadillac), אחרת המחשב יתן לנו הודעת שגיאה.

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

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

עיקרון 3 – רב-צורתיות (Polymorphism)

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

וירטואליות

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

[sourcecode language="java" l="12"]
public class Garage {

public void changeCadillacWheel(Cadillac carToFix)
{

}
public void changeSusitaWheel(Susita carToFix)
{

}

}

Cadillac myCadillac = new Cadillac("Black");
Susita mySusita = new Susita("Blue");

Garage gameGarage = new Garage();
gameGarage.changeCadillacWheel(myCadillac);
gameGarage.changeSusitaWheel(mySusita);
[/sourcecode]

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

[sourcecode language="java" l="12"]
public class Garage {

public void changeWheel(Car carToFix)
{

}

}

Cadillac myCadillac = new Cadillac("Black");
Susita mySusita = new Susita("Blue");

Garage gameGarage = new Garage();
gameGarage.changeWheel(myCadillac);
gameGarage.changeWheel(mySusita);
[/sourcecode]

שימו לב ל"רב הצורתיות" המתבטאת פה. ברגע שהמתודה changeWheel מקבלת פרמטר אחד מסוג Car, היא מסוגלת בעצם "לאמץ" את כל שאר הצורות שיש ל Car (והם Cadillac ו Susita כמובן) וזאת ללא הגדרה ספציפית של הסוגים הללו. כלומר המחשב יודע לזהות שהאובייקטים myCadillac ו mySusita הם מסוג (או מהצורה) Car ולכן הוא מאפשר לנו להשתמש בהם במתודה changeWheel.

בואו ונראה עוד דוגמאות לרב-צורתיות בתיכנות מונחה עצמים.

העמסת פרמטרים

בואו נחזור רגע למחלקת המכונית שלנו. במחלקה הגדרנו מתודה בשם increaseSpeed אשר מעלה את מהירות הרכב בקמ"ש אחד כל פעם. זוכרים?

[sourcecode language="java" l="12"]
public class Car {

public void increaseSpeed()
{
this.speed++;
}

}
[/sourcecode]

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

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

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

[sourcecode language="java" l="12"]
public class Car {

public void increaseSpeed()
{
this.speed++;
}

public void increaseSpeed(int kamash)
{
this.speed = this.speed + kamash;
}

}

Car myCar = new Car("White");
myCar.increaseSpeed();
myCar.increaseSpeed(25);
[/sourcecode]

שוב אנחנו עדים לרב צורתיות, בכך שלמתודה אחת יש צורות שונות (כלומר שני מימושים שונים).

סיכום

כמו נושא המחלקות וההורשה, גם נושא הרב-צורתיות הינו נושא מורחב. אני ממליץ לכולכם לקרוא קצת אודות ממשקים (interfaces) והעמסת אופרטורים (operator overloading) כדי לראות דוגמאות נוספות כיצד הרב צורתיות באה לידי ביטוי.

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

אנדי

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

נתראה בשיעור הבא!

Share

האקדמיה לפיתוח לאנדרואיד

בואו ללמוד לפתח לאנדרואיד! אתם בטח זוכרים אותי בתור Andy מהבלוג הישן!

11 Comments

  1. תיקון
    22/05/2010 בשעה 21:11

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

    1. אנדי
      22/05/2010 בשעה 21:19

      צודקת!
      תודה על התיקון 🙂

  2. קוסטה
    23/05/2010 בשעה 13:51

    נדמה לי שאתה סתם מסבך את שאר האנשים פה בשבילי זה נראה מיותר מה רק ב INT וDOUBLE הם אתה יכול לעשות את כול זה ב לולאת FOR או WHILE או בIF למשל
    int x=input.nextInt();
    if(x==1)
    {int speed=input.nextInt: וכדומה ככה אתה יכול ליצור את כול זה בבסיס ולא לעשות את זה מסובך כול כך אבל זאת רק דעתי יש כאלה שיהיה להם יותר קל במקום לכתוב משהו בסיס וליצור לעצמם עצמים

  3. אנדי
    23/05/2010 בשעה 19:37

    היי קוסטה.

    אני לא בטוח שאני כל כך מצליח להבין את מה שכתבת…

  4. guest
    24/05/2010 בשעה 05:16

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

    אנדי כתב בקשר לשימוש ב-super בבנאי:
    "יתרה מכך, כאשר למחלקת האב (Car במקרה שלנו) יש בנאי המקבל פרמטר (carColor), חובה עלינו לקרוא לבנאי האב (Car) באופן יזום, מתוך הבנאי של מחלקת הבן (Cadillac), אחרת המחשב יתן לנו הודעת שגיאה"
    אני רוצה להוסיף שכאשר קוראים לבנאי האב, זו צריכה להיות השורה הראשונה בבנאי (זה נעשה בצורה הזו גם אם אין קריאה מפורשת, כלומר כאשר יש בנאי ללא פרמטרים).
    להרחבה והבהרות ראו את השורה הזו:
    "Invocation of a superclass constructor must be the first line in the subclass constructor"
    בקישור הזה:
    http://java.sun.com/docs/books/tutorial/java/IandI/super.html

    עבור אלו מכם שקיבלו את ההמלצה של אנדי וקוראים קצת על ממשקים (interfaces): אני רוצה להרחיב קצת את ההמלצה ולהציע למתקדמים שביניכם לקרוא גם על מחלקות ומתודות אבסטרקטים (Abstract Methods and Classes).
    אפשר להתחיל פה:
    http://java.sun.com/docs/books/tutorial/java/IandI/abstract.html

    1. אנדי
      24/05/2010 בשעה 09:51

      תותח!

      תודה רבה על המידע!

  5. יניב
    24/05/2010 בשעה 16:22

    היי אנדי.
    בג'אווה אי אפשר לבצע ירושה יותר מדור אחד…

  6. יניב
    24/05/2010 בשעה 16:24

    כלומר מחלקה מסויימת לא יכולה לרשת מכמה מחלקות רק מאחת

    1. אנדי
      24/05/2010 בשעה 20:09

      טוב שהבהרת, כי זה היה יכול להיות לבלבל פה אנשים 🙂

      אני אחדד.
      מחלקה יכולה לרשת מחלקה שיורשת מחלקה שיורשת מחלקה…
      אך בג'אווה, מחלקה לא יכולה לרשת ("להרחיב") יותר ממחלקה אחת בו זמנים.

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

  7. אורי
    26/05/2010 בשעה 20:07

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

  8. chewy
    08/09/2011 בשעה 13:18

    כשאני מעתיק את הקוד הראשון אני מקבל הודעת שגיאה:
    The public type Cadillac must be defined in its own file

השאר תגובה