תכנות מונחה עצמים (OOP) - פולימורפיזם - כלליות מול התמחות

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

כלליות מול התמחות

 
מאת: ארז קלר

דוגמת קודהורדת דוגמאות קוד 
 

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

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

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

דוגמה :

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

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

דוגמת קוד - GeneralizationVsSpecification:
מחלקת הבסיס BaseShape:

 1  : class BaseShape 
 2  : { 
 3  :     private int x; 
 4  :     private int y; 
 5  :     public BaseShape(int x, int y) 
 6  :     { 
 7  :         this.x = x; 
 8  :         this.y = y; 
 9  :     } 
 10 :     public void MoveTo(int x, int y) 
 11 :     { 
 12 :         this.x = x; 
 13 :         this.y = y; 
 14 :     } 
 15 :     public virtual void Print() 
 16 :     { 
 17 :         Console.WriteLine("Shape position:x={0},y={1}", x, y); 
 18 :         Console.WriteLine("Area: {0}", CalcArea()); 
 19 :         Console.WriteLine("Permeter: {0}", CalcPerimeter()); 
 20 :     } 
 21 :     public virtual double CalcArea() 
 22 :     { 
 23 :         return 0; 
 24 :     } 
 25 :     public virtual double CalcPerimeter() 
 26 :     { 
 27 :         return 0; 
 28 :     } 
 29 : } 
המטלה MoveTo (שורה 10) היא מטלה כללית (גנרית), הזזת צורה ממקומה היא זהה לכל הצורות ואין כל הבדל.
לכן היא תמומש במתודה רגילה שמשותפת לכל הצורות המשתתפות במשחק.
מימוש של אלגוריתם זהה לכל המחלקות יהיה במתודה רגילה.

למטלה CalcArea (שורה21) ול-CalcPerimeter (שורה 25) יש מימוש שונה בין הצורות השונות, לכל צורה יש נוסחה מתמטית אחרת ולכן הן יוגדרו כמתודה וירטואלית על מנת שנוכל להביא לידי ביטוי את השוני.

למתודה Print (שורה 15) נתייחס אחר כך כי היא מייצגת מקרה מעניין המשלב קוד גנרי וקוד כללי.

המחלקה הנגזרת Circle:

 1  : class Circle : BaseShape 
 2  : { 
 3  :     private int radius; 
 4  :     public Circle(int x, int y, int radius) 
 5  :         : base(x, y) 
 6  :     { 
 7  :         this.radius = radius; 
 8  :     } 
 9  :     public override double CalcArea() 
 10 :     { 
 11 :         return Math.PI * Math.Pow(radius, 2); 
 12 :     } 
 13 :     public override double CalcPerimeter() 
 14 :     { 
 15 :         return 2 * Math.PI * radius; 
 16 :     } 
 17 :     public override void Print() 
 18 :     { 
 19 :         base.Print(); 
 20 :         Console.WriteLine("Radius: {0}", radius); 
 21 :     } 
 22 : } 
המימוש של CalcArea (שורה 9) ושל CalcPerimeter (שורה 13) מביא לידי ביטוי את ההתמחות של המחלקה Circle, את הנוסחאות המתמטיות הייחודיות לעיגול.


 
המחלקה הנגזרת Rectangle:
 1  : class Rectangle : BaseShape 
 2  : { 
 3  :     private int height; 
 4  :     private int width; 
 5  :     public Rectangle(int x, int y, int height, int width) : 
 6  :         base(x, y) 
 7  :     { 
 8  :         this.height = height; 
 9  :         this.width = width; 
 10 :     } 
 11 :     public override double CalcArea() 
 12 :     { 
 13 :         return height * width; 
 14 :     } 
 15 :     public override double CalcPerimeter() 
 16 :     { 
 17 :         return height + width; 
 18 :     } 
 19 :     public override void Print() 
 20 :     { 
 21 :         base.Print(); 
 22 :         Console.WriteLine("Height = {0}, Width = {1}", height, width); 
 23 :     } 
 24 : } 
המימוש של CalcArea (שורה 11) ושל CalcPerimeter (שורה 15) מביא לידי ביטוי את ההתמחות של המחלקה Rectangle, את הנוסחאות המתמטיות הייחודיות למלבן.


Main:
 1  : class Program 
 2  : { 
 3  :     static void Main(string[] args) 
 4  :     { 
 5  :         BaseShape shape1 = new Rectangle(10, 20, 30, 40); 
 6  :         shape1.Print(); 
 7  :         BaseShape shape2 = new Circle(10, 20, 50); 
 8  :         shape2.Print(); 
 9  :     } 
 10 : } 
בשורה 5 ייחוס מטיפוס הבסיס BaseShape מקבל כתובת של אובייקט מהמחלקה הנגזרת Rectangle.
בשורה 6 פעילים את המתודה ()Print.
איזה מימוש יופעל? המימוש של המחלקה הנגזרת, משום ש- ()Print היא וירטואלית.

המטלה Print

המתודה BaseShape.Print (
שורה 15) מייצגת מקרה מאוד מעניין, היא אמורה להביא לידי ביטוי גם את המכנה המשותף וגם את הייחודיות של אחת מהמחלקות הנגזרות.
המכנה המשותף הוא הדפסת מיקום, הדפסת תוצאת חישוב השטח והדפסת תוצאת חישוב ההיקף.
הייחודיות באה לידי ביטוי בנתונים הקיימים במחלקות הנגזרות (Radius בעיגול, Height ו- Width במלבן).
אבל גם המכנה המשותף אמור להביא לידי ביטוי את השוני בחישוב המתמטי של ההיקף והשטח בין המחלקות (ולכן המתודות CalcArea ו- CalcPerimeter הן וירטואליות).

המתודה Circle.Print (שורה 17) מפעילה את המימוש הקיים בבסיס בשביל להדפיס את המכנה המשותף ולאחר מכן מדפיסה את radius בשביל להביא לידי ביטוי את הנתון הקיים במחלקה הנגזרת.
המתודה Rectangle.Print (
שורה 19) מפעילה את המימוש הקיים בבסיס בשביל להדפיס את המכנה המשותף ולאחר מכן מדפיסה את height ואת width בשביל להביא לידי ביטוי את הנתונים הקיימים במחלקה הנגזרת.

התהליך:
שלב ראשון - 
()Rectangle.Print:

 1  : public override void Print() 
 2  : { 
 3  :     base.Print(); 
 4  :     Console.WriteLine("Height = {0}, Width = {1}", height, width); 
 5  : } 
תחילה אנו רוצים להדפיס את המידע המשותף: המיקום (X,Y), השטח וההיקף ולכן נפעיל את המימוש הקיים במחלקת הבסיס BaseShape:
 1  : public virtual void Print() 
 2  : { 
 3  :     Console.WriteLine("Shape position:x={0},y={1}", x, y); 
 4  :     Console.WriteLine("Area: {0}", CalcArea()); 
 5  :     Console.WriteLine("PeriRectameter: {0}", CalcPerimeter()); 
 6  : } 
תחילה נדפיס את המיקום (X,Y) המוגדרים במחלקת הבסיס, לאחר צכן מרצה להדפיס את ערכי ההיקף והשטח.
הבעיה שהחישובים הללו הם חישובים מתמחים, דהיינו, לכל אחת מהמחלקות הנגזרות יש נוסחה מתמטית שונה לחישוב הערך, לכן הגדרנו את המתודות כוירטואליות ולכן יופעלו המימושים הקיימים במחלקות הנגזרות, הן יחשבו את נערך על פי אופיה של הצורה ויחזירו את הערך כדי שהוא יודפס.

בסיום הפעולה של המימוש הקיים במחלקת הבסיס נחזור למימוש הקיים במחלקה הנגזרת על מנת להדפיס את הערך של Height ו-Width.

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





 



כל הזכויות שמורות למחבר ©