320. Implementing the virtual method pattern in C# part 2

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

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

ובכן, תזכורת:

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

1
2
3
4
5
6
7
8
9
10
11
Square square = new Square() { Edge = 4 };
square.GetArea(); // 16
Shape shape = new Square() { Edge = 3 };
shape.GetPerimeter(); // 12
RegularTriangle triangle = new RegularTriangle(){Edge = 3};
triangle.GetPerimeter(); // 9
shape = triangle;
shape.GetPerimeter(); // 0 (GetPerimeter doesn't override..)

ובכן, ראינו שהקוד הזה יתרגם לשורות הבאות:

1
2
3
4
5
6
7
8
9
10
11
Square square = new Square() { Edge = 4 };
Square.GetArea(square);
Shape shape = new Square() { Edge = 3 };
Shape.GetPerimeter(shape);
RegularTriangle triangle = new RegularTriangle() { Edge = 3 };
RegularTriangle.GetPerimeter(triangle); // 9
shape = triangle;
Shape.GetPerimeter(shape);

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

1
Shape.GetPerimeter(shape);

תקרא במקום לפונקציה

1
Square.GetPerimeter(shape);

אז איך נוכל לעשות זאת?


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

מה כן אפשר לעשות? אפשר להשתמש בDelegateים!

איך נעשה את זה? כל Instance יחזיק את המתודות האלה:

1
2
3
4
5
6
7
public abstract class Shape
{
protected delegate double GetPerimeterSignature(Shape _this);
protected delegate double GetAreaSignature(Shape _this);
protected GetPerimeterSignature mGetPerimeter;
protected GetAreaSignature mGetArea;
}

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

1
2
3
4
5
6
7
8
9
public static double GetPerimiter(Shape _this)
{
return _this.mGetPerimeter(_this);
}
public static double GetArea(Shape _this)
{
return _this.mGetArea(_this);
}

ונדאג בConstructorים לאתחל את הDelegate במתודות הנכונות:

בShape זה קל:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public Shape()
{
mGetArea = InnerGetArea;
mGetPerimeter = InnerGetPerimeter;
}
public static double InnerGetPerimeter(Shape _this)
{
return 0;
}
public static double InnerGetArea(Shape _this)
{
return 0;
}

בSquare זה יותר קשה:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public Square()
{
mGetArea = InnerGetArea;
mGetPerimeter = InnerGetPerimeter;
}
public static double InnerGetPerimeter(Shape _this)
{
return GetPerimeter((Square) _this);
}
public static double InnerGetArea(Shape _this)
{
return GetArea((Square)_this);
}
public static double GetArea(Square _this)
{
return _this.Edge*_this.Edge;
}
public static double GetPerimeter(Square _this)
{
return 4*_this.Edge;
}

שימו לקסם: כשיכתבו את השורה הזאת למשל:

1
2
Shape shape = new Square() { Edge = 3 };
shape.GetPerimeter(); // 12

היא תתרגם ל

1
2
Shape shape = new Square() { Edge = 3 };
Shape.GetPerimeter(shape);

שתקרא ל

1
2
3
4
public static double GetPerimiter(Shape _this)
{
return _this.mGetPerimeter(_this);
}

שתקרא ל

1
2
3
4
public static double InnerGetPerimeter(Shape _this)
{
return GetPerimeter((Square) _this);
}

שתקרא ל

1
2
3
4
public static double GetPerimeter(Square _this)
{
return 4*_this.Edge;
}

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

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


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

בפעם הבאה אנחנו ננסה להקטין את המקום הזה בזכרון ע"י יצירת הDelegateים האלה פעם אחת.

המשך יום וירטואלי טוב!

שתף