249. Implementing a fluent syntax part 2.5

בהמשך לטיפ הקודם,

אני מרגיש שקצת רימיתי בכתיבה של הInterfaceים של הSyntax, כי לא כל כך ברור איך חשבתי על הקשרים הנכונים בין הSyntaxים.

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

לאחר מכן חשבתי על זה ואפשר להסתכל על הבעיה בצורה אחרת:

ננסה לממש את מה שכתבתי בסוף הטיפ הקודם – שאפשר לקרוא לWithDefaultValue לכל היותר פעם אחת לעמודה, ולMakePrimaryKey פעם אחת לטבלה.

אנחנו מעוניינים שתמיד הפעולה הראשונה שתקרה היא WithColumn, לאחר מכן אנחנו מרשים שיקרו הפעולות WithColumn, WithDefaultValueוMakePrimaryKey, אבל עם הסייגים הבאים:

  • אפשר לקרוא לMakePrimaryKey רק פעם אחת
  • בין כל שני WithColumn אפשר לקרוא לWithDefaultValue רק פעם אחת

מדובר בעצם בשפה הנוצרת מעל הא”ב

$ { \text{WithColumn}, \text{MakePrimaryKey}, \text{WithDefaultValue} } $

עם החוקים שרשמנו מעלה.

אם נסמן $ a = \text{WithColumn}$, $ b =\text{MakePrimaryKey}$, $ c =\text{WithDefaultValue}$, אז השפה היא כל המילים שמתחילות ב$ a$, מכילות $ b$ לכל היותר פעם אחת, ומכילות $ c$ לכל היותר פעם אחת בין כל שני $ a$ים.

לשפה הזו אפשר לבנות אוטומט מתאים:

249_1.png

כאשר כל המצבים פה הם מצבים מקבלים, ובמעברים שלא מופיעה בהם אות מסוימת, מדובר במעבר למצב מלכודת, כלומר מצב לא חוקי. (כלומר אם כתבנו $ 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
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);
}

איך התרגום התבצע? פשוט יצרנו ממשק לכל מחלקה. כעת דאגנו ששימוש בפונקציה המתאימה (כלומר הוספת אות מהא"ב), יוביל אותנו למצב המתאים לפי האוטומט.

לדוגמה, ב$ Q_3 $ יש מעבר ל$ Q_5 $ אם מקבלים $ c $, אבל מעבר ל$ Q_6 $ אם מקבלים $ a $.

לכן בממשק IQ3<T> כשאנחנו קוראים לפונקציהWithDefaultValue (שאותה מייצגת האות $ c $), אנחנו דואגים להחזיר IQ5. לעומת זאת, כשאנחנו קוראים לWithColumn (שאותה מייצגת האות $ a $), אנחנו דואגים להחזיר IQ6.

הדבר הזה מאפשר לנו לאכוף את מה שרצינו: בהינתן הExtension Method הבא:

1
2
3
4
public static IQ1 AddTable(this DataSet dataSet, string name)
{
// ...
}

כל החוקים שרצינו לאכוף נאכפים:

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
35
36
37
dataSet.
AddTable("DEPARTMENT").
WithColumn<int>("PK").
WithDefaultValue(0).MakePrimaryKey().
WithColumn<string>("NAME").
WithDefaultValue("John Doe");
// Compiles
dataSet.
AddTable("DEPARTMENT").
WithColumn<int>("PK").
WithDefaultValue(1).
WithDefaultValue(0).
MakePrimaryKey().
WithColumn<string>("NAME").
WithDefaultValue("John Doe");
// Doesn't Compile
dataSet.
AddTable("DEPARTMENT").
WithColumn<int>("PK").
WithDefaultValue(1).
MakePrimaryKey().
WithColumn<string>("NAME").
MakePrimaryKey().
WithDefaultValue("John Doe");
// Doesn't Compile
dataSet.
AddTable("DEPARTMENT").
WithColumn<int>("PK").
WithDefaultValue(1).
MakePrimaryKey().
WithDefaultValue(0).
WithColumn<string>("NAME").
WithDefaultValue("John Doe");
// Doesn't Compile

ממש מגניב!

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

שתף