אני מרגיש שקצת רימיתי בכתיבה של הInterfaceים של הSyntax, כי לא כל כך ברור איך חשבתי על הקשרים הנכונים בין הSyntaxים.
למען האמת, כשכתבתי את הטיפ, ניסיתי להתאים את הקוד כך שהוא יעבוד, אבל לא כל כך היה ברור למה הוא עובד.
לאחר מכן חשבתי על זה ואפשר להסתכל על הבעיה בצורה אחרת:
ננסה לממש את מה שכתבתי בסוף הטיפ הקודם – שאפשר לקרוא לWithDefaultValue לכל היותר פעם אחת לעמודה, ולMakePrimaryKey פעם אחת לטבלה.
אנחנו מעוניינים שתמיד הפעולה הראשונה שתקרה היא WithColumn, לאחר מכן אנחנו מרשים שיקרו הפעולות WithColumn, WithDefaultValueוMakePrimaryKey, אבל עם הסייגים הבאים:
אפשר לקרוא לMakePrimaryKey רק פעם אחת
בין כל שני WithColumn אפשר לקרוא לWithDefaultValue רק פעם אחת
אם נסמן $ a = \text{WithColumn}$, $ b =\text{MakePrimaryKey}$, $ c =\text{WithDefaultValue}$, אז השפה היא כל המילים שמתחילות ב$ a$, מכילות $ b$ לכל היותר פעם אחת, ומכילות $ c$ לכל היותר פעם אחת בין כל שני $ a$ים.
לשפה הזו אפשר לבנות אוטומט מתאים:
כאשר כל המצבים פה הם מצבים מקבלים, ובמעברים שלא מופיעה בהם אות מסוימת, מדובר במעבר למצב מלכודת, כלומר מצב לא חוקי. (כלומר אם כתבנו $ b $ פעמיים במילה או $ c$ יותר מפעם אחת בין שני $ a $ים, לא נוכל לתקן את המילה)
בהנחה שאין טעויות באוטומט, נוכל לתרגם זאת לממשקים:
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
publicinterfaceIQ1
{
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);
}
publicinterfaceIQ4
{
IQ5 MakePrimaryKey();
IQ2<S> WithColumn<S>(string name);
}
publicinterfaceIQ5
{
IQ6<T> WithColumn<T>(string name);
}
public interface IQ6<T>
{
IQ5 WithDefaultValue(T value);
IQ6<S> WithColumn<S>(string name);
}
איך התרגום התבצע? פשוט יצרנו ממשק לכל מחלקה. כעת דאגנו ששימוש בפונקציה המתאימה (כלומר הוספת אות מהא"ב), יוביל אותנו למצב המתאים לפי האוטומט.
לדוגמה, ב$ Q_3 $ יש מעבר ל$ Q_5 $ אם מקבלים $ c $, אבל מעבר ל$ Q_6 $ אם מקבלים $ a $.
לכן בממשק IQ3<T> כשאנחנו קוראים לפונקציהWithDefaultValue (שאותה מייצגת האות $ c $), אנחנו דואגים להחזיר IQ5. לעומת זאת, כשאנחנו קוראים לWithColumn (שאותה מייצגת האות $ a $), אנחנו דואגים להחזיר IQ6.
הדבר הזה מאפשר לנו לאכוף את מה שרצינו: בהינתן הExtension Method הבא:
אז פעם קודמת ראינו איך אפשר להתחיל לממש Fluent syntax.
אחת הבעיות שיש עם המימוש הזה, הוא שהSyntax הוא חסר זכרון.
מה זאת אומרת? נניח שיש לנו באמת את האפשרות להפוך איזשהי עמודה להיות PrimaryKey. לא נרצה שתהיה לנו אפשרות להפוך את שאר העמודות להיות PrimaryKey. עם זאת, במימוש הנוכחי, נוכל לקרוא לMakePrimaryKey כמה פעמים שנרצה:
1
2
3
4
5
6
7
8
dataSet.
AddTable("DEPARTMENT").
WithColumn<int>("PK").
MakePrimaryKey().
WithColumn<string>("NAME").
WithDefaultValue("John Doe").
MakePrimaryKey();
// Compiles
נוכל לפתור זאת ע"י הפיכת הSyntax להיות בעל זכרון. הדבר הזה כרוך ביצירת הרבה ממשקים לכל אחד מהמצבים:
אבל ברגע שהפעלנו את הפונקציה MakePrimaryKey, לא נוכל להפעילה יותר!
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
dataSet.
AddTable("DEPARTMENT").
WithColumn<int>("PK").
WithDefaultValue(0).
MakePrimaryKey().
WithColumn<string>("NAME").
WithDefaultValue("John Doe").
MakePrimaryKey(); // Won't compile
dataSet.
AddTable("DEPARTMENT").
WithColumn<int>("PK").
WithDefaultValue(0).
MakePrimaryKey().
MakePrimaryKey(). // Won't compile
WithColumn<string>("NAME").
WithDefaultValue("John Doe");
dataSet.
AddTable("DEPARTMENT").
WithColumn<int>("PK").
WithDefaultValue(0).
MakePrimaryKey().
WithColumn<string>("NAME").
MakePrimaryKey(). // Won't compile
WithDefaultValue("John Doe");
מגניב ביותר! 😃
החיסרון הוא, כמובן, שצריך ליצור יותר Interfaceים ולהרכיב אותם בצורה הנכונה. אבל אם אנחנו רוצים למנוע שימוש יותר מפעם אחת במשהו, אז זה בהחלט שווה את ההשקעה. (לדוגמה אם חשוב לנו שלא יהיה אפשר להגדיר את Default Value פעמיים, נוכל לטפל בזה ע"י הוספת Interfaceים מתאימים ש"זוכרים" גם את הסטאטוס של MakePrimaryKey וגם את הסטאטוס של WithDefaultValue)
לפעמים אנחנו צריכים “לקנפג” איזשהו אובייקט. לפעמים קינפוג זה הוא לא כל כך פשוט, למשל יצירת DataSet עם שתי טבלאות וRelation בין שני DataTableים נראה בערך כך:
הדבר יכול להיות שימושי אם הקריאה לפונקציה היא דינאמית דרך Reflection. לדוגמה, אפשר לציין שם איך לאתחל את הפרמטר לפני הפעלת הפונקציה, איזו וואלידאציה אנחנו רוצים על הפרמטר (למשל שהוא מספר חיובי) וכו’.
לפני שנפעיל פונקציה זו בReflection נוכל לבדוק את הפרמטרים ולהתאים לפי זאת את הקריאה לפונקציה.
בפועל השימושי העיקרי בFeature זה הוא של הFramework. למשל, בMarshaling יש Attribute בשם Out המציין שפרמטר מסוים צריך להסתרלז לפרמטר בחזרה מהפונקציה.
כאמור, השימוש בזה הוא בעיקר בשביל מתודות שמריצים בReflection