מונחה עצמים
מתחילת
שנות ה-80 עד שנות ה90 פותחה גישה חדשה בתיכנות, הנקראת מונחה עצמים. גישה זו אפשרה
למתכנת לפשט את הבעיות העמודות מולו, בדומה לתפישה לה הוא רגיל.
העולם
שלנו מורכב מעצמים, שלפעמים לעצמים האלה יש קשר עם עצמים אחרים.
מונחה
עצמים הוא בעל 3 עקרונות עיקריים: הורשה, הכמסה ופלומורפיזם.
הכמסה: ההכמסה
הוא עקרון בו אנחנו מסתירים את את התכונות של המחלקות שלנו. ונעזרים בפעולות
שיאפשרו לנו לעשות פעולות על האובייקט עצמו.
הורשה: ההורשה
הוא עקרון בו מחלקה אחת היא נגזרת של השניה, כלומר בגלל שקיים קשר של מחלקה אחת
שמרחיבה את השניה יש בינהם קשר של הורשה. לדוגמה: מחלקה המייצגת יונק ומחלקה
המייצגת דולפין. מכיוון שלדולפין יש תכונות, ופעולות של כל יונק הוא יורש מימנו
ומרחיב את תכונותיו ופעולתיו (כמו לצלול, לכמה זמן מסוגל להיות מתחת למים,צלילי
דיבור וכו')...
פולימורפיזם:
פולימורפיזם הוא רב-צורתיות, כלומר מדובר על העקרון של להסתכל על עצם אחד בכמה
צורות (כל פעם כעצם אחר). כל זה מתאפשר הודות לעקרון ההורשה. לדוגמה: אנו יכולים
להסתכל על אובייקט הדולפין שלנו כיונק, ולהתייחס אך ורק לעובדה שהוא יונק. עקרון
זה יאפשר לנו גמישות רבה בתיכנות. כמו לדוגמה לעשות מערך של יונקים, שיכיל למעשה
גם דולפינים, גם בני אדם, וגם פילים.
עכשיו
שהבנו בערך כל עקרון נראה איך זה בא לידי ישום, ואת הטריקים המתאפשרים לנו בC#.
הכמסה:
אנו נרצה
להסתיר את התכונות של מחלקות שלנו (שהם למעשה התבנית של העצמים שלנו) על מנת לא
לאפשר למחלקות אחרות אפשרות גישה חופשית להתעסק עם המחלקה שלנו.
לכן
קיימים לנו מילות גישה שמגדירות לנו למי תיהיה גישה לכל תכונה, מאפיין או פעולה
במחלקה.
המילות
גישה הן: private,public,protected.
private תאפשר גישה רק
למי שבתוך המחלקה, וכל מי שמחוצה לו לא יכול לגשת אילו(לא ניתן דרך האובייקט).
public תאפשר גישה לכל המחלקות,
גם מי שבתוך המחלקה וגם מי שבחוץ(דרך האובייקט).
protected תאפשר גישה רק
למי שבתוך המחלקה, או למי שנגזר מימנה(יורש מהמחלקה).כלומר כל מי שלא חלק
מהמחלקה או נגזר מימנה יראה אותה כ"private" ומי שחלק מהמחלקה ונגזר מימנה יראה אותו
כ"public".
כך
יתאפשר לנו לשלוט למי, ואיך תיהיה גישה לאובייקטים שלנו.
לדוגמה
המחלקה:
class Human
{
private String name; //שם
private int age; גיל//
בנאי למחלקה המקבל שם בן אדם וגיל ומאתחל את תכונות הבן אדם//
public Human(string human_name,int human_age)
{
name=human_name;
age=human_age;
}
//פעולה מאחזרת גיל
public int GetAge()
{
return age;
}
//פעולה מאחזרת שם
Public string GetName()
{
return name;
}
פעולה המעדכנת את הגיל//
Public void SetAge(int new_age)
{
If(new_age>=0)
age=new_age;
}
//פעולת יום הולדת מגדילה את גיל בשנה
Public void Brithday()
{
age++;
}
פעולה המקבלת מחרוזת ו"צועקת"(מדפיסה) למסך את המחרוזת//
public void Yell(string something)
{
Console.WriteLine(something+”!!!!”);
}
}
מה בעצם
ראינו כאן? ראינו כי מוגדרות לנו התכונות גיל, ושם שהם עם גישת private, וזו על מנת למנוע
ממחלקות אחרות לגשת אליה מלבד היא עצמה. יש לנו פעולה בונה שמאתחלת את שם הבן אדם
וגילו, פעולה המחזירה את גילו,פעולה שמחזירה את שמו, פעולה המגדירה לו גיל חדש,
פעולה המעלה את גילו בשנה, ופעולה המדפיסה את מה שהאדם רוצה לצעוק וכל פעולות אלה
בעלות גישה public, כלומר ניתן לגשת אליהם דרך האובייקט.
שוב,
שימו לב שלא ניתן לגשת לתכונות האווביקט בלי לעבור דרך פעולה בונה/מאחזרת/מעדכנת.
למה זה
טוב? אנו רוצים שרק כאשר יוצרים את האובייקט בן אדם יכולו להגדיר את שמו (ולכן רק
בבנאי ניתן להגדיר את השם) אך לקבל את שמו נוכל תמיד (ולכן יהיה לו פעולה מאחזרת [GET]). בנוסף לזה אנו רוצים שכאשר מעדכנים את גילו
שתיהיה לנו בקרה על הגיל החדש שמעדכנים... לא יתכן שיתנו גיל שלילי (בן אדם לא
יכול להיות פחות מגיל 0). וכך אנו שולטים לחלוטין איך העצם שלנו מיוצג. נכון שזה
נפלא? J
הורשה
כפי שהסברתי בהקדמה, הורשה מאפשרת לנו להרחיב מחלקה אחרת ובכך
למנוע מאיתנו לכתוב קוד שוב ושוב, ובנוסף לזה לנצל את הקשר ההורשה לעקרונות נוספים
כמו פולימורפיזם (שעל כך יפורט בהמשך).
ההורשה בשפת התיכנות C# תכתב ע"י הסימן : (נקודותיים). נקח את הדוגמה בה יש
לנו יונק ובן אדם בעולם שלנו. לכל יונק יש גיל והאפשרות ללדת ילדים, ולכל בן אדם
יש שם ,גיל ,האפשרות ללדת ילדים, לחגוג לו יום הולדת, ולתת לו קצת לצעוק.
כפי שנו רואים הבן אדם הוא למעשה הרחבה של היונק הזה, מכיוון שבנוסף
לגיל ולאפשרות שלו ללדת ילדים יש לו גם שם (שמה לעשות ליונקים לא תמיד נוטים לתת
שם בלידה),יום הולדת, ואפשרות לצעוק.
איך כל זה יכתב?
Class Mamal //מחלקת יונק
{
Protected int age;
Protected int num_of_children;
פעולה בונה מקבלת גיל ומספר ילדים//
Public Mamal(int age,int num_of_children)
{
this.age=age;
this. num_of_children= num_of_children;
}
//פעולה מאחזרת גיל
public int GetAge()
{
return age;
}
פעולה המעדכנת את הגיל//
Public void SetAge(int new_age)
{
If(new_age>=0)
age=new_age;
}
//פעולה היולדת ילד
Public void Labor()
{
num_of_children++;
}
}
כעת
נכתוב את המחלקה בן אדם. שוב, בגלל שהאדם מרחיב את המחלקה יונק אין צורך שנכתוב את
כל התכונות והפעולות שוב, וכן כל מה שנותר הוא לירוש מיונק.
class Human : Mamal //מחלקת בן אדם היורשת מיונק
{
private String name; //שם
בנאי למחלקה המקבל שם בן אדם וגיל ומאתחל את תכונות הבן אדם//
public Human(string human_name,int human_age): base(human_age,0)
{
name=human_name;
}
//פעולה מאחזרת שם
Public string GetName()
{
return name;
}
//פעולת יום הולדת מגדילה את גיל בשנה
Public void Brithday()
{
age++;
}
פעולה המקבלת מחרוזת ו"צועקת"(מדפיסה) למסך את המחרוזת//
public void Yell(string something)
{
Console.WriteLine(something+”!!!!”);
}
}
נכון שזה
פשוט נפלא? כתבנו הרבה פחות ולמעשה הוספנו רק מה שהיה חסר לבן אדם מבחינת פעולות
ותכונות.
בדוגמה
הזאת יש מספר דברים שיש להשים אליהם לב:
1.במחלקה
יונק(Mamal) הגדרנו את התכונות כprotected על מנת לשמור על עקרון ההכמסה, שכן המחלקה
היורשת בן אדם(Human) יכולה לגשת לתוכנות המחלקה ממנה היא יורשת (יונק), כפי שאנו
רואים בפעולה גיל, אך עדיין לא ניתן לשנות את התכונות מחוץ למחלקה יונק והמחלקה
הנגזרת מימנה בן אדם.
2.בבנאי
כתבנו את השורה הבאה:
public Human(string human_name,int human_age): base(human_age,0)
נקודותיים
base אומר שאנו מעבירים לבנאי של המחלקה שממנה אנו יורשים את הפרמטרים
הנדרשים לבנאי שלה (גיל, וכמות ילדים).
העברנו
את הפרמטר human_age לage בנאי של יונק מכיוון שזה מה שנו מקבלת
בבנאי של Human וזה הגיל שאנו רוצים שיאותחל, ולפרמטר num_of_children אנו מכניסים את הפרמטר 0 מכיוון שאנו יוצרים בן
אדם הוא למעשה ללא ילדים (בעל 0 ילדים).
כעת
שיצרו בתוכנית הראשית(לדוגמה) אווביקט בן אדם זה יראה כך:
Human adam=new Human(“Aviad”,22);
כך נוצר
אובייקט בן אדם עם השם אביעד, וגילו 22, והוא ללא ילדים והשם משתנה נקרא adam.
במידה
ונרצה לאביעד יוולד ילד ( אללה יסטר) נכתוב:
adam.Labor();
בדומה
לכך ניצור אובייקט יונק:
Mamal beast=new Mamal(10,5);
כך יצרנו
אובייקט יונק בגיל 10 עם 5 ילדים ששם המשתנה נקרא beast.
הערות
חשובות:
כמובן
שנוכל לגשת לפעולות/תכונות/מאפייני האווביקט בהתאם לגישה שלהם הגדרנו.
כך
לדוגמה לא נוכל לרשום את הדבר הבא:
adam.name=”Carmel”;
או
best. num_of_children=11;
זה
מכיוון שname הוא private ומ
num_of_childrenהוא protected.
פולימורפיזם
לפעמים
אנו רוצים להסתכל על אווביקטים מסוימים בצורה שונה. כל אדם הוא גם יונק.
וכן
לפעמים יהיה נוח לנו יותר להסתכל עליו כיונק, דוגמה לנוחות כזאת היא אם נרצה לעשות
מערך של יונקים ובין היונקים האלה יהיו בני אדם, ויונקים אחרים (לדוגמה בעתיד נרצה
גם פילים).
אך לפני
שנדגים מערך נראה קודם איך מסתכלים על אווביקט בצורה שונה.
אז איך
אנו מסתכלים על אווביקט בצורה שונה? זו ע"י המרה מפורשת או מרומזת.
המרה
מרומזת:
Mamal yonek1=new
Human(“Aviad”,22”);
המרה
מרומזת היא לאתחל משתנה לאדם ולהשים אותו בטיפוס שמימנו הוא נגזר.
כמו
שהוצג בדוגמה איתחלנו בן אדם ושמנו אותו במשתנה yonek1 מטיפוס יונק(Mamal).
כעת אנו
מסתכלים על האוביקט yonek1 כיונק, ולא כאל בן אדם. ולכן לא נוכל לדוגמה לאחזר
את השם שלו או לגרום לו לצעוק ע"י הפעולה Yell.
כעת אם
נרצה להסתכל עליו שוב כבן אדם, נוכל לעשות זאת ע"י המרה מפורשת:
Human human1=(Human)yonek1;
כעת שנו
מסתכלים עליו שוב כבן אדם נוכל להשתמש בכל התכונות והפעולות שלו כבן אדם (לדוגמה
לאחזר את השם שלו ולגרום לו לצעוק ע"י הפעולה Yell.
אם זאת,
יש כמה מקרים יוצאי דופן שבהם דברים יקרו בניגוד לצורת הסתכלות.
בוא ניתן
דוגמה למקרים כאלה:
ניצור
מערך של יונקים שחלק מהם יהיו יונקים פשוטים, וחלק מהם יהיו בני אדם.
Mamal[] arr=new Mamal[3];
Arr[0]=new Mamal(10,0); //נוצר יונק בתא 0 עם הסתכלות של יונק
Arr[1]=new Human(“Aviad”,22); נוצר
בן אדם בתא 1 עם הסתכלות של יונק//
Arr[2]=new Mamal(5,1); //נוצר יונק בתא 2 עם הסתכלות של יונק
בגלל
שהמערך הוא מטיפוס
Mamal יש המרה מרומזת לכל האווביקטים שבתוך המערך
להסתכל על המערך כמערך יונקים, לכן לא נוכל בשום פנים ואופן לגשת
לפעולות/תכונות/מאפיינים של בן אדם.
בוא נגיד
שאנו מוסיפים לרגע איזה פעולת הדפסה למחלקה יונק שנקראת Print
ותפקידה
להדפיס את התכונות של היונק.
Class Mamal
{
…
Public void Print()
{
Console.WriteLine(“the mamal age ”+age+” number of
children:”+ num_of_children);
}
}
כעת אם
נריץ את הפעולה Print על המערך באמצעות לולאה כך:
for(int
i=0;i
arr[i].Print();
התוכנית
תדפיס לאחר הרצת הקוד את הדבר הבא:
the mamal age 10 number of children 0
the mamal age 22 number of children 0
the mamal age 5 number of
children 1
לכאורה
נראה רצוי מה שעשינו, אך אם היינו רוצים להדפיס אובייקט מטיפוס בן אדם הדפסה אחרת
לגמרי? שגם כוללת את שמו.
לשם כך
קיימות המילות השמורות virtual וoverride.
virtual נרשום בפעולות שמהם נרצה שזה יתעלם מצורת
ההסתכלות של האובייקט, את virtual נרשום במחקלת הבסיס
(המחלקת הראשונה שממנה אנו יורשים).
Override עובדת בחפיפה לvirtual, גם אותה נרשום
בפעולות שנרצה שיתעלמו מצורת ההסתכלות של האובייקט, רק שאותה נרשום במחלקות
הנגזרות( לאחר שרשמנו בבסיס את virtual).
לדוגמה:
נערוך את
המפעולה Print במחלקה יונק (Mamal):
Class Mamal
{
…
public virtual void Print()
{
Console.WriteLine(“the mamal age ”+age+” number of
children:”+ num_of_children);
}
}
מכיוון שMamal היא מחלקת הבסיס רשמנו virtual. כעת נערוך את המחלקה הנגזרת בן-אדם(Human) שלה אנו נרצה פעולת הדפסה אחרת:
Class Human
{
…
public override void Print()
{
Console.WriteLine(“the Human+”name+” age ”+age+” number of
children:”+ num_of_children);
}
}
מכיוון
שזו המחלקה הנגזרת, ואנו רוצים שהפעולה Print של האדם תתעלם ותדרוס את המחלה Print של היונק, אנו רשמנו את המילה השמורה override.
כעת אם
נריץ את התוכנית עם לולאת ההדפסה יודפס:
the mamal age 10 number of children 0
the Human Aviad age 22 number of children 0
the mamal age 5 number of
children 1
ובכך
התעלמנו מצורת ההסתכלות של המערך על הפעולה Print.
ומה אם
היינו רוצים להדפיס פעולות שלא משותפות לכל היונקים ולבני האדם? לדוגמה היינו
רוצים להפעיל את הפעולה Yell רק במידה והתא במערך arr שלנו הוא בן אדם.
איך נעשה
זאת?
קיימת
מילה שמורה נוספת לפתרון בעיה מהסוג הזה.. היא נקראת is. מילה זו תאפשר לנו לבדוק אם ניתן להסתכל על
האווביקט אחרת.
לדוגמה
נעבור על המערך arr שלנו ונפעיל את הפעולה Yell אך ורק אם ניתן הלהסתכל עליו כבן אדם. נרשום
זאת כך:
for(int
i=0;i
{
if(arr[i] is Human)
{
Human h=(Human)arr[i];
h.Yell();
}
}
כך אנו
בודקים אם ניתן להסתכל עליו כבן אדם, ואם ניתן אז אנו נעשה לו המרה מפורשת לבן
אדם, נשים אותו באובייקט h מטיפוס Human ונפעיל את הפעולה צעק (Yell).
כמובן
שיכלנו לרשום זאת גם בשורה אחת:
((Human)arr[i]).Yell();
אך חשוב
להשים לב לסוגריים, שכן קודם אנו ממירים אותו לHuman ורק לאחר מכן מפעילים עליו את הפעולה צעק (Yell),
שכן אם
נרשום את זה:
(Human)arr[i].Yell();
זה לא
יעבוד.. מיפני שלנקודה (.) יש קדימות גבוהה יותר ולכן הוא ינסה לגשת למערך במקום הI לזמן את Yell ורק
לאחר מכן להמירו לHuman.