232. Visitor design pattern

אחד הDesign patterns החזקים שקיימים נקרא Visitor.

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

הדבר הזה גם מאפשר לנו “להוסיף” מתודה וירטואלית למשפחה של טיפוסים, מבלי לשנות את הקוד שלהם.

איך זה עובד?

נניח שיש לנו את המשפחה הבאה של טיפוסים:

1
2
3
4
5
6
7
8
9
10
11
12
13
public abstract class CarElement
public class Wheel : CarElement
public class Door : CarElement
public class Engine : CarElement
public class Body : CarElement
public class Car : CarElement
{
public IEnumerable<CarElement> Elements
{
get;
private set;
}
}

כעת אנחנו מעוניינים להוסיף אפשרות של לצלם רכיב של המכונית.

בלי שימוש בVisitor, השיטה היא להוסיף פונקציה וירטואלית בCarElement:

1
2
3
4
public abstract class CarElement
{
public abstract void ShootPhoto();
}

ולדאוג לממש אותה בכל מחלקת בת.

יש שתי בעיות בגישה הזאת:

הבעיה הראשונה היא שעל כל פעולה שאנחנו מעוניינים להוסיף, אנחנו נצטרך להוסיף פונקציה וירטואלית כזאת. הבעיה עם זה היא שזה יכול לנפח את המחלקות שלנו. מה שיכול לקרות זה שCarElement "יממש" את הAnti-pattern שנקרא God Class, שהוא נהיה ענק ועמוס בפונקציות, כך שאנחנו לא כל כך רוצים להוסיף אליו עוד פונקציות.

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


כדי לפתור את בעיות אלו, אפשר להשתמש בDesign Pattern ששמו Visitor.

לפי Design pattern זה, יש רק פונקציה אבסטרקטית אחת שצריך לממש:

1
2
3
4
public abstract class CarElement
{
public abstract void Accept(ICarElementVisitor visitor);
}

כאשר הממשק ICarElementVisitor נראה ככה:

1
2
3
4
5
6
7
8
public interface ICarElementVisitor
{
void Visit(Wheel wheel);
void Visit(Door door);
void Visit(Engine engine);
void Visit(Body body);
void Visit(Car car);
}

המימוש בכל אחת מהמחלקות בנות מתחשב במבנה של המחלקה:

למשל:

1
2
3
4
5
6
7
public class Wheel : CarElement
{
public override void Accept(ICarElementVisitor visitor)
{
visitor.Visit(this);
}
}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Car : CarElement
{
public IEnumerable<CarElement> Elements
{
get;
private set;
}
public override void Accept(ICarElementVisitor visitor)
{
foreach (CarElement element in Elements)
{
element.Accept(visitor);
}
visitor.Visit(this);
}
}

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

למשל:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class CarPhotoShooter : ICarElementVisitor
{
public void Visit(Wheel wheel)
{
Console.WriteLine("Taking a photo of the wheel");
}
public void Visit(Door door)
{
Console.WriteLine("Taking a photo of the door");
}
// ...
public void Visit(Car wheel)
{
Console.WriteLine("What a nice car!");
}
}

ואז נוכל לקרוא לפונקציה זו כך:

1
2
3
ICarElementVisitor visitor = newCarPhotoShooter();
Car car = new Car();
car.Accept(visitor);

נוכל אפילו לקרוא לפונקציה זו כאילו היא הייתה פונקציה וירטואלית רגילה, ע"י שימוש בExtension Methods 😃

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

המשך יום דינאמי טוב

שתף