260. Convert ChangeType

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

במחלקה Convert קיימת הפונקציה ChangeType המאפשרת לנו להמיר לטיפוס נתון לפי הType שלו:

1
2
3
4
5
object theYear = Convert.ChangeType("2011",typeof (int));
Console.WriteLine(theYear.Equals(2011));// True
object theYearString = Convert.ChangeType(2011, typeof(string));
Console.WriteLine(theYearString.Equals("2011"));// True

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

ההגבלה העיקרית כאן, היא שאין לנו יכולת לציין פרטים על ההמרה, כמו עם איזה Format string להשתמש (למשל, עבור תאריכים) ואפשרויות נוספות.

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

סופ"ש מומר לטובה!

שתף

259. Hexadecimal representation of numbers

בהמשך לפעם הקודמת,

אנחנו מסוגלים להפוך מספר לייצוג ההקסה-דצימלי שלו ע”י שימוש בפונקציה ToString וציון פורמט מתאים:

למשל:

1
string oneTwoThree = 123.ToString("x"); // 7b

אפשר גם ככה:

1
string oneTwoThree = 123.ToString("X"); // 7B

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

כמובן אפשר לעשות גם דברים כאלה:

1
2
3
4
5
string oneTwoThree = 0x123.ToString("X"); // 123
string inDecimal = 0x123.ToString(); // 291
string oneTwoThree = 0x7B.ToString("X"); // 7B
string inDecimal = 0x7b.ToString(); // 123

למי שלא מכיר, ציון ה0x לפני המספר, מציין שאנחנו כותבים בהקסה-דצימלית.


מה בנוגע להמרות לבסיסים אחרים? לא מצאתי אנלוג לConvert.ToInt32, אם מישהו משהו כזה בתוך הFramework, אני אשמח לשמוע 😃

המשך יום הקסה דצימלי טוב!

שתף

258. Convert ToInt32

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

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

1
2
3
int fantastic = Convert.ToInt32("100", 2); // 4
int fifty = Convert.ToInt32("62", 8); // 50
int evil = Convert.ToInt32("11f018be6", 16); // 4815162342

לפני שאנחנו ממהרים לשמוח ולהתחיל לבצע מלא המרות מכל מיני בסיסים, יש פה קאץ’:

הדבר הזה עובד על כל בסיס, כל עוד הוא 2,8,10 או 16 😃

למי שלא מאמין, הנה קטע הקוד מתוך הReflector:

1
2
3
4
5
6
7
8
9
public static int ToInt32(string value, int fromBase)
{
if (((fromBase != 2) && (fromBase != 8)) && ((fromBase != 10) && (fromBase != 0x10)))
{
throw new ArgumentException(Environment.GetResourceString("Arg_InvalidBase"));
}
return ParseNumbers.StringToInt(value, fromBase, 0x1000);
}

יאללה!

יום בבסיס 2 טוב.

שתף

257. System Convert

הFramework מספק לנו את הטיפוסים “הבסיסיים” הבאים: bool, char, string, System.DateTime, System.DateTimeOffset ואת שאר הטיפוסים הנומריים.

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

למרבה הצער, רוב הפונקציות האלה הן חסרות תועלות: או שהן זורקות Exception על חוסר מימוש, או לחלופין הן לא נחוצות, מאחר וכבר קיים implicit cast מתאים.

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

לדוגמה, נניח שאנחנו מעוניינים להמיר מספר ממשי למספר טבעי. הדרך שאנחנו רגילים אליה היא באמצעות Explicit cast:

1
2
double real = 2.7182;
int integer = (int) real;

אלא שיש עם שיטה זו בעיה קלה (תלוי איך מסתכלים על זה) – ההסבה תמיד מקצצת את המספר.

לפעמים היינו רוצים אולי לקחת את העיגול של המספר. הפונקציה ToInt32 שמקבלת double תיתן לנו את העיגול של המספר

(ראו זאת גם כאזהרה!)

1
2
double real = 2.7182;
int integer = Convert.ToInt32(real); // 3

אם העיגול הזה הוא בעייתי, אפשר קודם לקרוא לMath.Round המאפשר לנו לשלוט בעיגול.


כאמור לכל טיפוס בסיסי קיים overload מתאים לConvert.ToInt32, למשל:

1
2
3
4
5
6
7
8
9
string text = "42";
int integer = Convert.ToInt32(text); // 42
//int integer = (int) text; // Doesn't compile
//int integer = (int) (object) text; // Throws an exception
bool theTruth = true;
int integer = Convert.ToInt32(theTruth); // 1
//int integer = (int) theTruth; // Doesn't compile
//int integer = (int) (object) theTruth; // Throws an exception

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

1
2
double real = Convert.ToDouble("2.7182"); // 2.7182
bool theTruth = Convert.ToBoolean("True"); // true

המשך יום מומר לטובה!

שתף

256. About boxing and custom casts operators

[מבוסס על הפוסט הבא]

כולנו מכירים הסבות Custom של הFramework, למשל:

1
2
double real = 3.14;
int integer = (int) real; // 3

מה שקורה כאן זה בעצם שאין שום קשר של ירושה בין int וdouble, אבל מתבצעת הסבה ע"י Custom explicit cast. (ראו גם טיפ מספר 197)

בעיה שבד"כ נתקלים בה בהקשר זה היא הבאה: נניח שיש לנוobject שמחזיק double:

1
2
3
double real = 3.14;
object boxed = real;
int integer = (int) boxed; // Boom!

משום מה, הקוד הזה שנראה בערך אותו הדבר, לא עובד…

אבל אם נעשה משהו כזה:

1
2
3
double real = 3.14;
object boxed = real;
int integer = (int) (double) boxed; // 3

זה יעבוד.


מה בעצם קורה כאן?

כאשר אנחנו מכניסים לobject אובייקט שהוא Value-Type, מתבצע תהליך שנקרא Boxing. בתהליך הזה הValue-Type נעטף ע"י איזשהו אובייקט על הHeap שמצביע אליו.

נניח שיש לנו טיפוס A וטיפוס B.

נניח שיש לנו אובייקט מסוג A ואנחנו מעוניינים להמירו בטיפוס מסוג B בצורה Explicit.

הקומפיילר יודע להבחין בין שני סוגים של הסבות:

  • הסבה ראשונה: Up-cast – הסבה כזו אומרת את הדבר הבא: יש לי יותר ידע מהקומפיילר, אני יודע שהאובייקט A שאני מחזיק הוא למעשה אובייקט מסוג B, ואני דורש מהקומפיילר להתייחס אל האובייקט כאובייקט מסוג B.
  • הסבה שנייה: Custom cast – הסבה כזו אומרת את הדבר הבא: אני יודע שהאובייקט שאני מחזיק הוא מסוג A, אבל אני יודע שיש איזשהי דרך להמיר אותו לסוג B. אני דורש מהקומפיילר לבצע את ההמרה.

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


שאלה המתבקשת היא למה? למה הקומפיילר מפרש את הקריאה שלנו בתור בקשה לעשות unboxing לint? למה שלא ינסה לבדוק מהו הטיפוס האמיתי של האובייקט boxed וינסה משם לחפש דרך אחרת להפוך אותו לint?

התשובה היא כזאת: ביצוע פעולה של unboxing היא פעולה יחסית בזבזנית. כעת נניח שאנחנו רוצים לבצע unboxing ואז לחפש אופרטור הסבה מתאים, ואז להפעיל אותו. דבר ראשון, הפעולה שמתוארת כאן היא אפילו יותר כבדה מunboxing, שהרי אחר כך אנחנו מחפשים פונקציית המרה מתאימה וקוראים לה. דבר שני, הפעולה התמימה הזאת של "הסבה לint", הייתה מתקמפלת בסופו של דבר למספר שורות בIL, בניגוד לפעולה בודדת של unbox:

1
2
3
ldloc.1
unbox.any int32
stloc.2

אוקיי, ואם בכל זאת בא לי שהקומפיילר יבצע את התהליך המסובך שתואר למעלה?

אז זה אפשרי, בשביל זה בדיוק קיימת הפונקציה Convert.ToInt32, המבצעת חישוב הדומה לחישוב המסובך הנ"ל 😃:

1
2
3
double real = 3.14;
object boxed = real;
int integer = Convert.ToInt32(boxed); // 3

שבוע עם הסבות בטוחות טוב!

שתף

255. Happy birthday

תוכן חסר

שתף

254. Convention over configuration

אחת הפרדיגמות העיצוב החשובות היא פרדיגמה בשם Convention over configuration.

פרדיגמה זו מעודדת להקטין את מספר ההחלטות שמפתח המשתמש בFramework שלנו צריך לקבל, מה שמביא לפשטות, אבל לאו דווקא ע”י איבוד גמישות.

למה הכוונה?

לדוגמה, נניח שיש לנו איזשהו Framework המאפשר לנו לשלוף מהDatabase אובייקטים משלנו. (שם קוד: DAL)

אחת הבעיות הראשונות שעשויות להיות לDAL כזה היא שהשמות בDatabase של הטבלאות והשמות של העמודות שלהן לאו דווקא מזדהים עם השמות של המחלקות והProperties של האובייקטים שלנו.

לכן בד”כ Framework כזה ידרוש מאיתנו מיפוי בין המחלקות והProperties שלנו לטבלאות או העמודות בDatabase:

המיפוי בדרך כלל יהיה באמצעות קובץ XML מעצבן, או בתוך הקוד באמצעות Metadata:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
[DbTable(Name = "PEOPLE")]
public class Person
{
[DbColumn(Name = "FIRST_NAME")]
public string Name
{
get;
set;
}
[DbColumn(Name ="LAST_NAME")]
public string LastName
{
get;
set;
}
[DbColumn(Name = "AGING")]
public int Age
{
get;
set;
}
}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
[DbTable(Name = "Person")]
public class Person
{
[DbColumn(Name = "Name")]
public string Name
{
get;
set;
}
[DbColumn(Name ="LastName")]
public string LastName
{
get;
set;
}
[DbColumn(Name = "Age")]
public int Age
{
get;
set;
}
}

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

מה שConvention over configuration אומר, זה את הדבר הבא:

הFramework שלנו יאפשר למפות את המחלקות/Properties לטבלאות/עמודות, אבל יאפשר גם הגדרה אוטומטית המבוססת קונבנציה:

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
[DbTable]
public class Person
{
[DbColumn]
public string Name
{
get;
set;
}
[DbColumn]
public string LastName
{
get;
set;
}
[DbColumn]
public int Age
{
get;
set;
}
}

אופציה נוספת יכולה להיות לא להכריח לשים כלום:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Person
{
public string Name
{
get;
set;
}
public string LastName
{
get;
set;
}
public int Age
{
get;
set;
}
}

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

מה שחשוב, זה שההגדרה האוטומטית מתבצעת בנוסף לאפשרות קינפוג, כלומר עדיין אפשר לעשות משהו כזה:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
[DbTable(Name = "PEOPLE")]
public class Person
{
public string Name
{
get;
set;
}
[DbColumn(Name = "LAST_NAME")]
public string LastName
{
get;
set;
}
public int Age
{
get;
set;
}
}

אבל אין חובה לעשות זאת אם השמות זהים!

למה זה טוב?

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

הבעיה העיקרית עם פרדיגמה זו היא לאנשים שחדשים לשפה/לFramework שלנו. הם עשויים לפספס את הפרטים אודות הקונבנציה ולקבל התנהגות מוזרה, או לחלופין להבין מה חלק מהקוד עושה (או איך העיצוב בדיוק עובד).

המשך יום טוב עם קונבנציה טובה!

שתף

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, כך שאולי זה כן שווה את זה…

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

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

שתף

252. Implementing a fluent syntax - part 5

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

מה היה לנו עד כה?

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

משהו שאני מרגיש שלא כל כך ברור, הוא מה לעשות כאשר יש לנו פעולה שמתמשכת על גבי כמה מתודות בSyntax:

למשל בטיפ הראשון ראינו משהו כזה:

1
2
3
4
5
6
7
8
9
10
11
12
dataSet.
AddTable("DEPARTMENT").
WithColumn<int>("PK").
MakePrimaryKey().
WithColumn<string>("NAME").
AddTable("EMPLOYER").
WithColumn<string>("NAME").
WithColumn<int>("DEPARTMENT_FK").
RelatedTo("DEPARTMENT").
WithParentKey("PK").
WithChildKey("DEPARTMENT_FK").
Named("EMPLOYER_TO_DEPARTMENT");

זו פעולה בודדת שנפרשת על כמה מתודות, שהרי היא מתרגמת למשהו כזה:

1
2
3
4
5
6
DataRelation employerToDepartment =
new DataRelation("EMPLOYER_TO_DEPARTMENT",
departments.Columns["PK"],
employers.Columns["DEPARTMENT_FK"]);
employers.ParentRelations.Add(employerToDepartment);

אז איך עושים את זה?

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public interface ITableDefinitionSyntax
{
ITableOrColumnDefinitionSyntax<T> WithColumn<T>(string name);
IRelationSyntaxParentDefinition RelatedTo(string tableName);
}
public interface IRelationSyntaxParentDefinition
{
IRelationSyntaxChildDefinition WithParentKey(string key);
}
public interface IRelationSyntaxChildDefinition
{
IRelationSyntaxNameDefinition WithChildKey(string key);
}
public interface IRelationSyntaxNameDefinition
{
ITableDefinitionSyntax Named(string relationName);
}

כעת נממש אותם בצורה הפשוטה:

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
38
39
40
41
42
43
44
45
46
47
48
49
internal class TableDefinitionSyntax :
ITableDefinitionSyntax,
IRelationSyntaxParentDefinition,
IRelationSyntaxChildDefinition,
IRelationSyntaxNameDefinition
{
protected readonly DataTable mTable;
protected DataTable mParentTable;
protected DataColumn mParentKey;
protected DataColumn mChildKey;
public TableDefinitionSyntax(DataTable table)
{
mTable = table;
}
ITableOrColumnDefinitionSyntax<T> ITableDefinitionSyntax.WithColumn<T>(string name)
{
DataColumn column = mTable.Columns.Add(name, typeof(T));
return new TableOrColumnDefinitionSyntax<T>(mTable, column);
}
IRelationSyntaxParentDefinition ITableDefinitionSyntax.RelatedTo(string tableName)
{
mParentTable = mTable.DataSet.Tables[tableName];
return this;
}
IRelationSyntaxChildDefinition IRelationSyntaxParentDefinition.WithParentKey(string key)
{
mParentKey = mParentTable.Columns[key];
return this;
}
IRelationSyntaxNameDefinition IRelationSyntaxChildDefinition.WithChildKey(string key)
{
mChildKey = mTable.Columns[key];
return this;
}
ITableDefinitionSyntax IRelationSyntaxNameDefinition.Named(string relationName)
{
mTable.ParentRelations.Add(relationName,
mParentKey,
mChildKey);
return this;
}
}

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

נשים לב שבOverload של Add, השם של הRelation הוא רשות. נניח והיינו רוצים שהSyntax שלנו יתמוך בשם אופציונאלי. איך נוכל לעשות זאת?

נוסיף Member ששומר את הRelation:

1
protected DataRelation mRelation;

בפונקציה WithChildKey נדאג לשמור אותו בצד אחרי שהוספנו אותו:

1
2
3
4
5
6
7
8
IRelationSyntaxNameDefinition IRelationSyntaxChildDefinition.WithChildKey(string key)
{
mChildKey = mTable.Columns[key];
mRelation = mTable.ParentRelations.Add(mParentKey, mChildKey);
return this;
}

ובפונקציה Named נדאג לשנות לו את השם 😃:

1
2
3
4
5
6
ITableDefinitionSyntax IRelationSyntaxNameDefinition.Named(string relationName)
{
mRelation.RelationName = relationName;
return this;
}

כמובן, כדאי שנאפשר למשתמש לעשות עוד משהו פרט לNamed (שהרי Named זה אופציונאלי):

1
2
3
4
public interface IRelationSyntaxNameDefinition : ITableDefinitionSyntax
{
ITableDefinitionSyntax Named(string relationName);
}

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

שתף

251. Implementing a fluent syntax part 4

ראינו בפעמים הקודמות איך מממשים Fluent Syntax.

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

כמובן, זה מצב לא רצוי (שכידוע, פחות קוד, זה יותר טוב).

אחת האופציות לפתור את הבעיה היא הבאה:

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

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);
}

במקום לממש מחלקה עבור כל ממשק, נוכל לכתוב מחלקה אחת שמממשת את כל הממשקים!:

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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
internal class TableFluentSynax : IQ1
{
#region Members
public DataTable Table { get; set; }
#endregion
#region Implementation
protected TableColumnFluentSyntax<S> AddColumn<S>(string columnName)
{
DataColumn addedColumn =
this.Table.Columns.Add(columnName, typeof(S));
return new TableColumnFluentSyntax<S>()
{
Table = this.Table,
Column = addedColumn
};
}
#endregion
#region Interface Implementation
IQ2<T> IQ1.WithColumn<T>(string name)
{
return this.AddColumn<T>(name);
}
#endregion
}
internal class TableColumnFluentSyntax<T> : TableFluentSynax, IQ2<T>, IQ3<T>, IQ4, IQ5, IQ6<T>
{
#region Members
public DataColumn Column { get; set; }
#endregion
#region Implementation
private TableColumnFluentSyntax<T> SetDefaultValue(T defaultValue)
{
this.Column.DefaultValue = defaultValue;
return this;
}
private TableColumnFluentSyntax<T> MakePrimaryKey()
{
this.Table.PrimaryKey = new[] { this.Column };
return this;
}
#endregion
#region Interface Implementation
IQ5 IQ6<T>.WithDefaultValue(T value)
{
return this.SetDefaultValue(value);
}
IQ5 IQ3<T>.WithDefaultValue(T value)
{
return this.SetDefaultValue(value);
}
IQ4 IQ2<T>.WithDefaultValue(T value)
{
return this.SetDefaultValue(value);
}
IQ5 IQ4.MakePrimaryKey()
{
return this.MakePrimaryKey();
}
IQ3<T> IQ2<T>.MakePrimaryKey()
{
return this.MakePrimaryKey();
}
IQ6<S> IQ6<T>.WithColumn<S>(string name)
{
return this.AddColumn<S>(name);
}
IQ2<S> IQ4.WithColumn<S>(string name)
{
return this.AddColumn<S>(name);
}
IQ6<S> IQ3<T>.WithColumn<S>(string name)
{
return this.AddColumn<S>(name);
}
IQ2<S> IQ2<T>.WithColumn<S>(string name)
{
return this.AddColumn<S>(name);
}
IQ6<S> IQ5.WithColumn<S>(string name)
{
return this.AddColumn<S>(name);
}
#endregion
}

טוב, זה קצת ארוך אבל הרעיון הוא מאוד פשוט! יש שלוש פונקציות שאנחנו מממשים ושאר הפונקציות קוראות לפונקציות הנ"ל.

הפונקציות האלה הן SetDefaultValue, MakePrimaryKey וAddColumn. הן מחזירות לנו Instance מתאים של המחלקה שלנו (את הInstance הנוכחי, אם אנחנו עדיין עובדים על אותה עמודה, או Instance חדש אם עברנו לעבוד על עמודה חדשה)

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

למה בעצם יש פה שתי מחלקות? זוכרים את הExtension Method הזה?

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

הוא מחזיר Instance של IQ1, שזה בסה"כ ממשק המציין פעולה על טבלה (כלומר, הוספת עמודה). מאחר וזה קורה לפני שעוד יש לנו עמודה ביד, זה מרגיש קצת לא נכון לממש את IQ1 כבר בTableOrColumnDefinitionSyntax<T>. למה? כי מה יהיה הפרמטר הגנרי? אפשר לשים בפרמטר הגנרי כל דבר, אבל זה מרגיש לי קצת לא נכון.

בכל מקרה, עכשיו אחרי שיש לנו את המימוש הזה, אפשר לממש בכיף את הExtension Method הזה:

1
2
3
4
5
public static IQ1 AddTable(this DataSet dataSet, string name)
{
DataTable addedTable = dataSet.Tables.Add(name);
return new TableFluentSynax() {Table = addedTable};
}

ולהשתמש בכיף בFluent Syntax שפיתחנו עד כה:

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

וזה עובד!

שבוע צף טוב!

שתף