כעת אנחנו יכולים לאפשר שינוי ההתנהגות של הCalculator ע"י מימושים שונים של NumberAdder, למשל:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
publicclassSlowNumberAdder : INumberAdder
{
publicintAdd(int a, int b)
{
int result = a;
for (int i = 0; i < b; i++)
{
result++;
}
return result;
}
}
או מימוש טיפה יותר טוב 😃
1
2
3
4
5
6
7
publicclassBetterNumberAdder : INumberAdder
{
publicintAdd(int a, int b)
{
return a + b;
}
}
וכך נוכל להחליף את ההתנהגות של הפונקציה שלנו ע"י שינוי הAdder מבחוץ:
1
2
3
4
5
ICommutativeCalculator slowerCalculator =
new StrategyBasedCommutativeCalculator(new SlowNumberAdder());
ICommutativeCalculator slowCalculator =
new StrategyBasedCommutativeCalculator(new BetterNumberAdder());
בסה"כ אנחנו רואים שאנחנו יכולים לשפר את המחלקה שלנו ע"י שיפור "האלגוריתם" הפנימי שלה.
הדבר שימושי במידה ויש לנו מחלקה שמשתמשת באלגוריתם ואנחנו רוצים לאפשר לשפר או לשנות את האלגוריתם מבחוץ. כמובן, לא בהכרח חייב להיות לנו אלגוריתם מתמטי, אלא יכול להיות מדובר גם ב"שירות", למשל שירות שמספק לנו אפשרות לשלוף מDatabase.
בנוסף, הדבר יכול להיות שימושי אם נוכל לקנפג את הStrategy בו אנחנו משתמשים מבחוץ – כך נוכל להחליף התנהגות של מחלקה מבלי לשנות קוד.
הדבר גם מאפשר Mocking בחלק מהמקרים.
אני מניח שכולנו השתמשנו בדברים כאלה, רק לא ידענו שקוראים לזה Strategy.
הכרנו בעבר את הObject Initializer המאפשר לאתחל ערכים של המחלקה בצורה נוחה. (ראו גם טיפ מספר 87)
בהמשך הכרנו גם את הFeature של Default Value Arguments (הFeature של C# 4.0 – ראו גם טיפ מספר 243)
כעת יש לנו שתי דרכים עיקריות לאתחל מחלקה:
אופציה ראשונה – להעביר את כל הערכים בConstructor, ולסמן את הערכים שיש להם ערכים דיפולטיים עם Default argments
אופציה שניה – לאתחל את כל הProperties של המחלקה בעזרת Object Initializer (ולאתחל את יתר השדות בערכים הדיפולטיים שנקבע)
לדוגמה, את המחלקה הזאת ניתן לאתחל בשתי דרכים:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
publicclassPerson
{
publicPerson()
{
IsMarried = false;
NumberOfChildren = 0;
}
publicPerson(string name, bool isMarried= false, int numberOfChildren = 0)
{
Name = name;
IsMarried = isMarried;
NumberOfChildren = numberOfChildren;
}
publicstring Name { get; set; }
publicbool IsMarried { get; set; }
publicint NumberOfChildren { get; set; }
}
ניתן לאתחל בשתי דרכים:
הדרך הראשונה:
1
Person person = new Person(name: "Yosi", isMarried: true);
הדרך השנייה:
1
2
3
4
5
Person person = new Person()
{
Name = "Yosi",
IsMarried = true,
};
נשווה בין שתי השיטות:
היתרון בשיטה הראשונה היא שהיא מאפשרת לנו שהFields של המחלקה יהיו readonly (ראו גם טיפ מספר 14), בנוסף היא יכולה לחייב אותנו לאתחל פרמטר מסוים.
החסרונות שלה הם הבאים:
נוכל לשים בתור Default argument value רק ערכים שהם Compile-time
בכל פעם שנשנה את הערכים הדיפולטיים, נצטרך לקמפל מחדש את כל הDLLים שמשתמשים במחלקה שלנו – זאת מאחר וDefault argument values נכתבים Hard-coded בתוך הקוד בזמן קימפול (ראו גם טיפ מספר 244)
היתרון של השיטה השנייה היא שנוכל לשים בתור Default argument value גם ערכים שהם לא Compile-time, ובמידה ונשנה ערך דיפולטי, זה לא יגרור קימפול מחדש של DLL שמשתמש במחלקה שלנו. החסרון הוא, כמובן, שהProperty הוא לא readonly (במידה ונרצה שהוא readonly), ושאנחנו לא יכולים לחייב את המשתמש לאתחל שדות מסוימים, אלא אם נקבל אותם בConstructor.
למה זה טוב? כרגע זה נראה שזה טיפה יותר מסובך מקריאה פשוטה לפונקציה.
אז ככה:
קודם כל אנחנו יכולים לראות את השמות של הפרמטרים בצורה יותר ברורה
דבר שני, הסדר של הפרמטרים לא משנה יותר – כי הפרמטרים הם Named
בנוסף, אנחנו יכולים לתת ערכים דיפולטיים לפרמטרים (שימו לב שאת היתרונות האלה אנחנו מקבלים גם מNamed arguments וDefault argument values של C# 4.0)
היתרון המשמעותי של הDesign pattern הזה הוא שאנחנו לא חייבים להריץ את הפונקציה ישר, אלא אנחנו יכולים להריץ אותה גם בהמשך. הדבר הזה מאפשר את היתרונות הבאים:
אנחנו יכולים לשנות פרמטרים לפני ההרצה – אם למשל חלק מהפרמטרים לא ידועים לנו בשלב כלשהו של התכנית שלנו, אבל חלק כן, אנחנו יכולים למלא בכל חלק בתכנית את הפרמטרים שאנחנו מכירים ולהריץ את המתודה כשמילאנו את כל הפרמטרים
בנוסף, האופן בו אנחנו מריצים את הפונקציה הוא נתון לבחירתנו! אנחנו יכולים להחליט אם אנחנו מעוניינים בהרצה סינכרונית, או א-סינכרונית, אולי בכלל נפנה לשירות – על כל זאת אנחנו יכולה להשפיע ע"י מימוש פונקציה הרצה משלנו בתוך המחלקה!
אישית יצא לי להשתמש בPattern הזה בהקשרי GUI (יצירת Commandים) ובהרצה של Stored Procedures ע"י מחלקות Strongly-typed.
אם נגדיר פונקציה כך, הקומפיילר לא יזהה אותה כפונקציה עם פרמטרים דיפולטיים ולא נוכל להשמיט את הפרמטרים הדיפולטיים בקריאה אליה.
אז מה זה עושה בכלל? אם נריץ את הפונקציה בReflection (כמו בטיפ הקודם) עם Missing.Value, נקבל את הערך הדיפולטי בפרמטרים שלא ציינו.
הFeature הזה הוא Cross-Language (הוא מתאים לכל השפות שכתובות מעל הFramework) וכנראה לא יותר מדי שימושי מאחר ובC# 4.0 יש כבר דרך טובה יותר להגדיר ערכים דיפולטיים, אבל נחמד להכיר שיש דבר כזה.
דרך אגב, אם נסתכל בReflection על מתודה שקימפלנו בC# 4.0 עם פרמטרים דיפולטיים, נראה שיש מעל הפרמטרים הדיפולטיים את הAttribute ששמו Optional, אבל אין את DefaultParameterValue. מעניין העניין.
ראינו בטיפ מספר 243 את הFeature שנוסף בC# 4.0 המאפשר לנו לציין ערכים דיפולטיים למתודה שלנו, מבלי לייצר הרבה Overloadים.
ראינו שמאחורי הקלעים מה שנעשה הוא פשוט הכנסה Hard-Coded של ערכים אלו בכניסה לפונקציה.
קיים באינטרנט Framework בשם ASP.net MVC 4. Framework זה מאפשר לנו לבנות אתרי אינטרנט בצורה מאוד נוחה. (אם מישהו מכיר Ruby on rails – Asp.net MVC הוא החיקוי של מיקרוסופט לROR)
אחד הדברים המגניבים שעשו שם זה להגדיר שבמידה וניגשים לurl מסוים, התשתית יודעת לפרסר את הurl לController (מחלקה) המתאים ולפונקציה המתאימה עם הפרמטרים המתאימים. למשל אם ניגש ל
מה שקורה זה שIsOptional מחזיר לנו האם לפרמטר יש ערך דיפולטי, וDefaultValue מביא לנו את הערך הדיפולטי.
אם תסתכלו אחורה, תראו שגם בFrameworkים הקודמים היו את הProperties האלה. איך זה יכול להיות?
ובכן, Reflection הוא לא ספציפי לC# - מסתבר שיש שפה שהיא אחות של C# בשם VB.net, שם היו פרמטרים דיפולטיים גם לפני Framework 4.0, וזו הייתה הדרך לגשת אליהם.
בקיצור, יש פה הרבה כוח שאפשר לנצל, במיוחד לתשתיות שיכולות להסתכל על הפרמטרים הדיפולטיים האלה ולעשות איתם משהו.
בהמשך לטיפ על Single Abstract Method הבא אליו בJava 8, הנה עוד Feature שמגיע אלינו מJava 8:
הFeature נקרא Virtual Extension Methods.
על Extension Methods בC# דיברנו כבר לא מעט (טיפים 66-75 וגם אחר כך).
בגדול Extension Methods נותנים לנו שני דברים מגניבים:
אופציה “להוסיף” מתודות לTypeים שכבר קיימים (טיפ מספר 75 למשל)
אופציה לקבל בחינם מתודות לממשקים ע”י שימוש בפונקציות שהם חושפים (טיפ מספר 84)
היכולת הראשונה יכולה לאפשר לנו יכולות יפות (כמו הרבה מהדוגמאות שראינו), אבל יכולה גם לזבל לנו את הIntellisense בExtension Methods לא רלוונטיים במידה והוספנו Reference ועשינו using לא נכון.
בנוסף, ייתכן כי נשתמש בExtension Method שאין לו “תמיכה של היצרן”, כלומר הוא בד”כ לא נכתב על ידי מי שכתב את המחלקה המקורית, אלא ע”י מישהו חיצוני.
בJava החליטו שהם מממשים Extension Methods בצורה אחרת, כנראה בגלל הבעיות האלה.
מה שקורה כאן זה שכשנממש את הממשק Turnable, נצטרך לממש רק את הפונקציה TurnLeft, ונקבל בחינם מימושים לפונקציות TurnRight וTurnOpposite.
עם זאת, קיימת לנו האופציה לדרוס את TurnRight וTurnOpposite למימושים אחרים משלנו.
אם נרצה למנוע מהמממש של הממשק שלנו את האפשרות של לדרוס את הפונקציות האלה, נוכל להחליף את המילה default בfinal:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
interfaceTurnable
{
// Must override
publicvoidturnLeft();
// Can override
publicvoidturnOpposite()default
{
for (int i = 0; i < 2; i++)
{
this.turnLeft();
}
};
// Can't override
publicvoidturnRight()final
{
for (int i = 0; i < 3; i++)
{
this.turnLeft();
}
};
}
זה מזכיר קצת את הסיפור של מחלקה אבסטרקטית.
הדבר הזה מאפשר להתגבר על החסרונות של Extension Methods:
נוכל להוסיף "Extension Methods" רק לממשקים שאנחנו כתבנו.
מצד אחד זו הגבלה, כיוון שלא נוכל להוסיף Extension Methods יפים לטיפוסים שלא שלנו.
מצד שני, זה מאפשר יותר סדר ושליטה על המתודות שמופיעות בIntellisense, ומאפשר Api נקי יותר.
אישית, הדבר שהכי אהבתי כאן זה העובדה שאפשר לממש Extension Method ולדרוס את המימוש הדיפולטי. זה Feature שחסר בC#, וכנראה בלתי אפשרי לעשות בגלל האופן בו מימשו Extension Methods בשפה.
(x => Convert.ToInt32(x), x => Convert.ToString(x));
string three = myConverter.ConvertBack(3);
כעקרון כאן זה נראה פחות יפה. זה בד"כ יפה אם אנחנו לא צריכים לציין פרמטרים גנריים וכו’, אבל העקרון הוא שבמידה ורוב המימושים שלנו הם יחסית פשוטים, ומשתמשים בהם במקום אחד (כלומר אין בהם Reuse), אז הדבר הזה יכול להיות מאוד נוח.