170. Factory by Attributes

היום נראה דוגמה נחמדה לשימוש בAttributeים.

נניח שאנחנו רוצים לממש Factory של צורות, כלומר מחלקה אשר בונה לנו instance של צורה לפי השם שלה.

ראינו שני סוגי מימושים בטיפים שמספרם 50 ו139.

תזכורת: המימוש הכי פשוט הוא משהו כזה:

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 class ShapeFactory
{
public static Shape CreateByName(string name)
{
switch (name)
{
case "Circle":
{
return new Circle();
}
case "Triangle":
{
return new Triangle();
}
case "Square":
{
return new Square();
}
default:
{
return null;
}
}
}
}

אבל הוא גם המימוש הכי מכוער.

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

אז יאללה, לעבודה!


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

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
26
public static class ShapeFactory
{
public static Shape CreateByName(string name)
{
// TODO : implement this
return null;
}
[ShapeBuilder("Triangle")]
private static Shape BuildTriangle()
{
return new Triangle();
}
[ShapeBuilder("Square")]
private static Shape BuildSquare()
{
return new Square();
}
[ShapeBuilder("Circle")]
private static Shape BuildCircle()
{
return new Circle();
}
}

המתודה CreateByName תדע לאיזו מתודה (למתודה כזו אני אוהב לקרוא Builder) ללכת לפי הstring שמצוין בAttribute מעליה.

תחשבו שבמתודות הBuilderים, יכולה להיות לוגיקה יותר מסובכת…

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

ראשית עלינו לממש את הAttribute. זה די קל:

1
2
3
4
5
6
7
8
9
10
[AttributeUsage(AttributeTargets.Method, Inherited = true)]
public class ShapeBuilderAttribute : Attribute
{
public string Name { get; private set; }
public ShapeBuilderAttribute(string name)
{
this.Name = name;
}
}

שימו לב שאנחנו שמים AttributeUsage של מתודות בלבד.

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

המימוש שלה הוא לא קשה:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public static Shape CreateByName(string name)
{
Type factoryType = typeof (ShapeFactory);
MethodInfo[] allStaticMethods =
factoryType.GetMethods(BindingFlags.Static |
BindingFlags.NonPublic |
BindingFlags.Public);
IEnumerable<MethodInfo> builderMethods =
from method in allStaticMethods
let builderAttributes = method.GetCustomAttributes(typeof(ShapeBuilderAttribute), true)
from ShapeBuilderAttribute builderAttribute in builderAttributes
where builderAttribute.Name == name
select method;
MethodInfo requestedBuilder = builderMethods.Single();
return (Shape)requestedBuilder.Invoke(null, null);
}

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

אחרי זה אנחנו קוראים לה ועושים הסבה לShape. ראו גם טיפים מספר 95,97,147,148,154,167,168.

כעת נוכל לקרוא לפונקציה:

1
Shape triangle = ShapeFactory.CreateByName("Triangle");

די מגניב.


יש פה חסרון שיש פה קריאה איטית בReflection. הראתי פעם (טיפ מספר 151) כיצד ניתן לשפר זאת באמצעות Delegate.CreateDelegate.

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

הרעיון הוא ליצור Dictionary<string, Func<Shape>> סטטי שיכיל מיפוי של השמות לBuilderים (כמו בטיפ מספר 50) ולמלא אותו בפונקציות שיש מעליהן Attribute בConstructor הסטטי באמצעות הפונקציה Delegate.CreateDelegate. אולי אני אראה איך עושים את זה מתישהו.

המשך ערב בר סגולה

שתף