253. Implementing a fluent syntax - part 6

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

ראינו בעבר כי אחד הדברים השימושיים והיפים בC# הוא היכולת להוסיף Extension Methods לInterfaces (טיפ מספר 84).

מה שאולי לא טריוויאלי הוא העובדה שאפשר לנצל עובדה זו גם עבור Fluent syntax:

נניח שיש לנו את הFluent Syntax הטיפ מסובך שיצרנו בחלק 2.5 של הסדרה:

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
27
28
29
30
31
32
33
34
public interface IQ1
{
IQ2<T> WithColumn<T>(string name);
}
public interface IQ2<T>
{
IQ2<S> WithColumn<S>(string name);
IQ3<T> MakePrimaryKey();
IQ4 WithDefaultValue(T value);
}
public interface IQ3<T>
{
IQ5 WithDefaultValue(T value);
IQ6<S> WithColumn<S>(string name);
}
public interface IQ4
{
IQ5 MakePrimaryKey();
IQ2<S> WithColumn<S>(string name);
}
public interface IQ5
{
IQ6<T> WithColumn<T>(string name);
}
public interface IQ6<T>
{
IQ5 WithDefaultValue(T value);
IQ6<S> WithColumn<S>(string name);
}

נוכל להוסיף Extension Method לInterfaceים. לדוגמה, יש לנו את הפונקציה WithColumn שיוצרת לנו עמודה באיזשהו שם וטיפוס. יש לנו את הפונקציה MakePrimaryKey שהופכת את העמודה גם להיות Primary Key של הטבלה.

נוכל ליצור איזשהי פונקציה שמאחדת אותם:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static IQ3<T> WithPrimaryKeyColumn<T>(this IQ1 state, string name)
{
return state.WithColumn<T>(name).MakePrimaryKey();
}
public static IQ3<T> WithPrimaryKeyColumn<T>(this IQ4 state, string name)
{
return state.WithColumn<T>(name).MakePrimaryKey();
}
public static IQ3<T> WithPrimaryKeyColumn<T,S>(this IQ2<S> state, string name)
{
return state.WithColumn<T>(name).MakePrimaryKey();
}

ואז נוכל לכתוב דברים כאלה:

1
2
3
4
dataSet.
AddTable("DEPARTMENT").
WithPrimaryKeyColumn<int>("PK").
WithColumn<string>("NAME");

וכאלה:

1
2
3
4
5
dataSet.
AddTable("DEPARTMENT").
WithColumn<string>("NAME").
WithDefaultValue("John Doe").
WithPrimaryKeyColumn<int>("PK");

אבל למרבה הצער לא כאלה:

1
2
3
4
dataSet.
AddTable("DEPARTMENT").
WithColumn<string>("NAME").
WithPrimaryKeyColumn<int>("PK");

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

מאחר ויש כאן פרמטר שלא יכול להיות מזוהה בזמן קימפול באופן Implicitly (זה הפרמטר בשם T), עלינו לציין את כל הפרמטרים בזמן קימפול באופן Explicitly.

פתרון אפשרי לבעיה יכול להיות למשל שIQ2 ירש מטיפוס לא גנרי (למשל IQ1).

(כנראה זה הפתרון הנכון, וגם עדיף שIQ4 יירש מIQ1, ככה גם יהיה לנו רק Extension Method בודד, במקום 3 שצוינו מעלה)


השאלה היא האם הדבר הזה טוב? למה צריך את זה בכלל? מצד אחד זה טוב, מאחר וזה מאפשר לנו להוסיף עוד Syntax לFluent Syntax קיים וכך לעשות את הFluent Syntax אפילו טוב יותר!

מצד שני, זה יכול להיות קשה להוסיף Fluent Syntax משלנו (למשל, אולי נצטרך להוסיף עוד ממשקים/מחלקות), ואם הFluent Syntax כבר שייך לנו, אז מה אכפת לנו להוסיף לו כבר עוד פונקציות?

לדעתי אין תשובה חד משמעית. אם הFluent Syntax לא שייך לנו (למשל שייך לאיזשהו Open Source כמו Rhino Mocks או Ninject), אז הדבר הזה מאפשר לנו להרחיב את הSyntax כך שיתאים לצרכים שלנו. אם הSyntax שייך לנו, אין לזה כל כך יתרון מלהוסיף את הפעולות הרלוונטיות לממשקים של הSyntax, אבל אולי זה בכל זאת יכול להתאים: למשל, במקרה כאן, כל מי שאי פעם ירש מהממשקים שציינתי כאן, יקבל חינם את הפונקציהWithPrimaryKeyColumn, כך שאולי זה כן שווה את זה…

בכל מקרה, זה נחמד.

המשך יום צף טוב!

שתף