340. More about computing a square root

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

כעקרון אפשר לחשב שורש של מספר בשיטות שראינו בפעם הקודמת, אבל החסרון של השיטות הוא יעילות זמן הריצה שלהן: יעילות זמן הריצה שלהן הוא $ O\left(\sqrt{n}\right) $.

בכדי לשפר זאת ניתן לשים לב לנקודה הבאה:

הסדרה $ f(n) = n^2 $ היא סדרה עולה. נדמיין אותה כמו מערך אינסופי שבמקום ה$ n $ שלו נמצא הערך $ n^2 $.

כעת, אנחנו מעוניינים למצוא את התא הכי גדול שעבורו מתקיים $ f(n) \ge m $. (לאיזשהו m נתון)

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public static int Sqrt(int n)
{
int left = 1;
int right = n;
int currentIndex = (1 + n) / 2;
while (!(currentIndex * currentIndex <= n &&
(currentIndex + 1) * (currentIndex + 1) > n))
{
if (currentIndex * currentIndex > n)
{
currentIndex = (currentIndex + left)/2;
}
else
{
currentIndex = (currentIndex + right)/2;
}
}
return currentIndex;
}

זו שיטה נחמדה ואנחנו מקבלים פה יעילות חבל על הזמן של $ O \left(\log{n} \right) $.

למרבה הצער יש פה הרבה פעולות כפל, כך שלממש את זה באסמבלי זה לא להיט.

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

אז איך נממש את האלגוריתם? נשים לב ש

$ (a + b)^2 = a^2 + 2ab + b^2 $

ולכן

$ (a + 2^m)^2 = a^2 + 2^{m + 1} \cdot a + 2^{2m} $

כעת נתחיל בחיפוש החזקה הגבוהה ביותר של 2 כך שהריבוע שלה קטן גדול מהמספר:

1
2
3
4
5
6
7
8
9
10
11
int right = 1;
int rightSquared = 1;
while (rightSquared <= n)
{
right *= 2;
rightSquared *= 4;
}
int current = right / 2;
int currentSquared = rightSquared / 4;

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

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
public static int Sqrt(int n)
{
int right = 1;
int rightSquared = 1;
while (rightSquared <= n)
{
right *= 2;
rightSquared *= 4;
}
int current = right / 2;
int currentSquared = rightSquared / 4;
int currentSuccesorSquared = currentSquared + 2*current + 1;
int currentPower = current;
int currentPowerSquared = currentSquared;
while (!((currentSquared <= n) &&
(currentSuccesorSquared > n)))
{
int additionSquared =
currentPowerSquared +
2*currentPower*current +
currentSquared;
if (additionSquared <= n)
{
currentSquared = additionSquared;
current = current + currentPower;
currentSuccesorSquared = currentSquared + 2*current + 1;
}
currentPower /= 2;
currentPowerSquared /= 4;
}
return current;
}

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

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

מה שחשוב לשים לב אליו זה שאין פה כפל בדברים שאינם חזקות של 2 (currentPower היא חזקה של 2 וגם 2 היא חזקה של 2) ולכן פוטנציאלית ניתן לכתוב את האלגוריתם הזה באסמבלי בלי יותר מדי סיבוכים, אבל זה כבר פחות כיף מהאלגוריתם שהצגתי פעם שעברה…

המשך שבוע מושרש לטובה.

הערה: יש ערך די מעניין על חישוב inverse square root עבור ערכי float (וייצוג floating point בכלליות) בויקיפדיה (כולל הקוד המקורי מ-Quake שחישב אותו 😃) אם מתעסקים בחישובי שורש.

שתף

339. Computing a square root

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

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

1
2
3
4
5
6
7
8
9
10
11
public static int Sqrt(int n)
{
int currentNumber = 0;
while (currentNumber * currentNumber <= n)
{
currentNumber++;
}
return (currentNumber - 1);
}

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

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

$ 1 = 1 \\ 1 + 3 = 4 \\ 1 + 3 + 5 = 9 \\ 1 + 3 + 5 + 7 = 16 \\ 1 + 3 + 5 + 7 + 9 = 25 \\ \dots \\ 1 + 3 + 5 + 7 + \dots + (2n - 1) = n^2 $

לכן אפשר לפתור את הבעיה כך:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static int Sqrt(int n)
{
int currentOdd = 1;
int numOfIterations = 0;
while (n > 0)
{
numOfIterations++;
currentOdd += 2;
n -= currentOdd;
}
return numOfIterations;
}

שימו לב שיותר קל לכתוב קוד שקול לזה בAssembly 😃

המשך יום עם שורשים טובים.

שתף

338. Computing Fibonacci element in logn

בהמשך לפעם הקודמת, אם מבקשים מאיתנו לכתוב קוד שמחשב את המספר הפיבונאצ’י הnי, בד”כ נכתוב משהו כזה:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static int Fibonacci(int n)
{
int previous = 0;
int current = 1;
for (int i = 0; i < n; i++)
{
int temp = current;
current = current + previous;
previous = temp;
}
return previous;
}

זה מחשב את המספר הפיבונאצ’י הnי בסיבוכיות $ O \left(n\right) $.

מסתבר שגם כאן אפשר לחשב בצורה יותר מהירה: יהי $ \varphi $ מספר המקיים $ \varphi^2 = \varphi + 1 $ (למשוואה זו קוראים משוואת חתך הזהב ולאיבר החיובי המקיים משוואה זו קוראים חתך הזהב)

אזי מסתבר כי $ \varphi^n = F_n \cdot \varphi + F_{n - 1} $ כאשר $ F_n $ הוא מספר הפיבונאצ’י הn. (הוכחה פשוטה באינדוקציה)

כעת מתקיים:

$ \varphi^{n + m} = F_{n + m} \cdot \varphi + F_{n + m - 1} = \\ \varphi^{n + m} = \varphi^n \cdot \varphi^m = \\ \left(F_n \cdot \varphi + F_{n - 1} \right) \cdot \left(F_m \cdot \varphi + F_{m - 1} \right) = \\ F_n \cdot F_m \cdot \varphi^2 + F_{n - 1} \cdot F_m \cdot \varphi + F_n \cdot F_{m - 1} \cdot \varphi + F_{n - 1} \cdot F_{m - 1} = \\ F_n \cdot F_m \cdot \varphi + F_n \cdot F_m + F_{n - 1} \cdot F_m \cdot \varphi + F_n \cdot F_{m - 1} \cdot \varphi + F_{n - 1} \cdot F_{m - 1} = \\ (F_n \cdot F_m + F_{n - 1} \cdot F_m + F_n \cdot F_{m - 1}) \cdot \varphi + (F_n \cdot F_m + F_{n - 1} \cdot F_{m - 1}) $

כלומר

$ F_{n + m} \cdot \varphi + F_{n + m - 1} = \\ (F_n \cdot F_m + F_{n - 1} \cdot F_m + F_n \cdot F_{m - 1}) \cdot \varphi + (F_n \cdot F_m + F_{n - 1} \cdot F_{m - 1})$ $ F_{n + m} = F_n \cdot F_m + F_{n - 1} \cdot F_m + F_n \cdot F_{m - 1} \\ F_{n + m - 1} = F_n \cdot F_m + F_{n - 1} \cdot F_{m - 1} $

אפשר לחשב בצורה דומה למה שראינו פעם שעברה את הפיבונאצ’י הn ע"י הסחבות עם ארבע זוגות של מספרים (הנוכחי והקודם של התוצאה והנוכחי והקודם של החזקה הנוכחית של 2).

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

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
public struct FibonacciPair
{
private readonly int mCurrent;
private readonly int mPrevious;
public FibonacciPair(int current, int previous)
{
mCurrent = current;
mPrevious = previous;
}
public int Previous
{
get { return mPrevious; }
}
public int Current
{
get { return mCurrent; }
}
public static FibonacciPair operator *(FibonacciPair first, FibonacciPair second)
{
return new FibonacciPair
(first.mCurrent*second.mCurrent + first.mPrevious*second.mCurrent + first.mCurrent*second.mPrevious,
first.mCurrent*second.mCurrent + first.mPrevious*second.mPrevious);
}
}

כעת נוכל לחשב את הפיבונאצ’י הn בצורה דומה לפעם הקודמת: אנחנו מחשבים את $ \varphi^n $ בעזרת המחלקה של FibonacciPair בצורה דומה לזו של פעם קודמת:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public static int Fibonacci(int n)
{
FibonacciPair currentPower = new FibonacciPair(1, 0);
FibonacciPair result = new FibonacciPair(0, 1);
while (n > 0)
{
if (n % 2 == 1)
{
result *= currentPower;
}
currentPower *= currentPower;
n /= 2;
}
return result.Current;
}

נראה לי שזה אולי טיפה יותר מפתיע ממה שראינו פעם קודמת… מספר הפיבונאצ’י ה$ n $ ב$ \log{n} $!

המשך יום פיבונאצ’י טוב.

שתף

337. Computing power in logn

כשבאים אלינו בבקשה לכתוב פונקציה שמחשבת חזקה של מספר, הפתרון בד”כ נראה כך:

1
2
3
4
5
6
7
8
9
10
11
public static double Power(double a, int n)
{
double result = 1;
for (int i = 0; i < n; i++)
{
result *= a;
}
return result;
}

זה פתרון מאוד פשוט, אבל הוא בסיבוכיות $ O(n) $.

אפשר לשפר אותו ע"י חישוב חזקות שהן חזקות של 2 ולהתבסס על הייצוג הבינארי של מספר ועל חוקי החזקות:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public static double Power(double a, int n)
{
double result = 1;
double currentPower = a;
while (n > 0)
{
if (n % 2 == 1)
{
result *= currentPower;
}
currentPower *= currentPower;
n /= 2;
}
return result;
}

יש כאן רק $ \log(n) $ איטרציות.

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

בכל מקרה, כתבתי את זה בעיקר כדי להראות שזה אפשרי ומגניב. צריך לזכור כמובן שיש בFramework את הפונקציה Math.Pow ועדיף להשתמש בה בד"כ.

שיהיה שבוע חזק.

שתף

336. About strongly typed and etc

שמתי לב שהרבה פעמים אנשים (כולל אני) משתמשים במושג Strongly typed/Weakly typed בשביל לתאר גישה בטוחה לאובייקטים בזמן קימפול.

זה שגוי להשתמש במושגים האלה, והנה הסבר מעמיק ל4 סוגים של שפות תכנות:

  • Statically typed language – שפה בה הטיפוסים ידועים מראש בזמן קימפול. רוב השפות מסוג זה אוכפות את התנאי הזה ע”י כך שנצטרך לציין את הטיפוס של המשתנים שלנו לפני שנוכל להשתמש בהם בפעם הראשונה. C#, Java, C ועוד שפות הן שפות שהן Statically typed.
  • Dynamically typed language – שפה שבה הטיפוסים מפוענחים בזמן ריצה. ההפך מStatically typed. למשל Python, Ruby וjs הן שפות שהן Dynamically typed, מאחר ומפוענח הטיפוס של המשתנה בפעם הראשונה שנכנס אליו ערך.
  • Strongly typed language – שפה שבה הטיפוסים נאכפים. למשל, אם יש לנו משתנה מסוג int, לא נוכל להתייחס אליו כString (כלומר להכניס אותו למשתנה של string או לקרוא לפונקציות של string) בלי לבצע איזושהי הסבה. Python, C#, Ruby, Java וכו’ הן שפות שהן Strongly typed!
  • Weakly typed language – שפה ניתן להתעלם מהטיפוסים. ההפך משפות Strongly typed. למשל, VBScript היא Weakly typed – בVbScript למשל ניתן לשרשר את המחרוזת ‘12’ ואת הInteger 3 כדי לקבל את המחרוזת ‘123’. לאחר מכן ניתן להתייחס למחרוזת כמספר 123 ללא הסבה מפורשת.

למשל, C# היא שפה שהיא Statically typed וStrongly typed. (החל מC# 4.0 היא גם Dynamically typed)

Python היא שפה שהיא Dynamically typed וStrongly typed.

שבוע חזק.

שתף

335. Collection{T} class

כולנו מכירים את הממשק ICollection אותו מממשים טיפוסים רבים בFramework, ביניהם List וT[].

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

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

דוגמא נוספת היא הקפצת אירוע – אנחנו מעוניינים להקפיץ אירוע כשהרשימה משתנה בתנאים מסוימים (למשל, כשנכנס איבר לא תקין, להקפיץ על כך אירוע)

אפשר לעטוף את List בICollection ולהוסיף לו את הפונקציונאליות שמעניינת אותנו, אבל יש דרך יותר פשוטה לבצע דבר כזה בFramework:

קיים הטיפוס Collection העוטף List, אבל בנוסף גם מאפשר לנו להתערב במה שקורה כאשר נוסף איבר, מסירים איבר, משתנה איבר במקום מסוים, או מרוקנים את האוסף:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class MyCollection<T> : Collection<T>
{
protected override void ClearItems()
{
// ...
}
protected override void InsertItem(int index, T item)
{
// ...
}
protected override void RemoveItem(int index)
{
// ...
}
protected override void SetItem(int index, T item)
{
// ...
}
}

במימושנו נוכל לקרוא לbase, ואז תקרה הפעולה הרגילה, אך נוכל להוסיף פונקציונאליות משלנו, למשל:

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
public class Club : Collection<Person>
{
private readonly int mMaxPeopleAllowed;
public Club(int maxPeopleAllowed)
{
mMaxPeopleAllowed = maxPeopleAllowed;
}
protected override void InsertItem(int index, Person item)
{
if (item.Age < 21)
{
throw new TooYoungException("Entrance is allowed only for people that are at least 21.");
}
if (this.Count == mMaxPeopleAllowed)
{
throw new ClubFullException("The number of allowed people in the club is limited to " +
mMaxPeopleAllowed);
}
base.InsertItem(index, item);
}
}

סופ"ש אוספים טוב.

שתף

334. Using brackets for hierarchy

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

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Person
{
public int Age { get; set; }
public string Name { get; set; }
public Address Address { get; set; }
}
public class Address
{
public string Street { get; set; }
public string City { get; set; }
public int ZipCode { get; set; }
}

אז נוכל לכתוב קוד כזה:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public static void WritePerson(Person person, XmlWriter writer)
{
writer.WriteStartElement("Person");
writer.WriteStartElement("Age");
writer.WriteValue(person.Age);
writer.WriteEndElement();
writer.WriteStartElement("Name");
writer.WriteValue(person.Name);
writer.WriteEndElement();
writer.WriteStartElement("Address");
writer.WriteStartElement("Street");
writer.WriteValue(person.Address.Street);
writer.WriteEndElement();
writer.WriteStartElement("City");
writer.WriteValue(person.Address.City);
writer.WriteEndElement();
writer.WriteStartElement("ZipCode");
writer.WriteValue(person.Address.ZipCode);
writer.WriteEndElement();
writer.WriteEndElement();
writer.WriteEndElement();
}

אבל כנראה קשה לעקוב אחרי מה קורה כאן. אפשר "להוסיף עומק" לפונקציה ע"י שימוש בScopeים:

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
public static void WritePerson(Person person, XmlWriter writer)
{
writer.WriteStartElement("Person");
{
{
writer.WriteStartElement("Age");
writer.WriteValue(person.Age);
writer.WriteEndElement();
}
{
writer.WriteStartElement("Name");
writer.WriteValue(person.Name);
writer.WriteEndElement();
}
{
writer.WriteStartElement("Address");
{
writer.WriteStartElement("Street");
writer.WriteValue(person.Address.Street);
writer.WriteEndElement();
}
{
writer.WriteStartElement("City");
writer.WriteValue(person.Address.City);
writer.WriteEndElement();
}
{
writer.WriteStartElement("ZipCode");
writer.WriteValue(person.Address.ZipCode);
writer.WriteEndElement();
}
writer.WriteEndElement();
}
}
writer.WriteEndElement();
}

טיפה יותר ברור מה קורה כאן.

שימו לב שהדוגמה היא להמחשה, ולא היינו רוצים לראות פונקציות ארוכות כאלה בקוד שלנו, אבל אפשר להשתמש בScopeים הפנימיים גם בפונקציות קצרות לקוד ברור יותר (כשזה מתאים).

המשך יום היררכי טוב.

שתף

333. Object initializer alternative - mimicing with

Syntax של Object Initializer שהוצג בC# 3.0 הוא מאוד נוח ומאפשר לאתחל אובייקט בצורה יותר ברורה מקודם. (ראו גם טיפ מספר 87, 88)

הבעיה היא שאפשר להשתמש בו רק במקרה שאנחנו מאתחלים אובייקט דרך Constructor – אם אנחנו מאתחלים אובייקט בצורה אחרת, למשל איזשהו Factory, לא נוכל להשתמש בSyntax כזה לאתחול האובייקט.

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

מה שאנחנו יכולים לעשות זה משהו כזה:

1
2
3
4
5
6
IPerson person = Factory.CreateInstance<IPerson>();
{
var p = person;
p.Name = "Arik Einstein";
p.Age = 73;
}

שימו לב שהמשתנה p הוא משתנה שנמצא בתוך Scope פנימי, ולכן אנחנו יכולים לעשות מספר אתחולים כאלה:

1
2
3
4
5
6
7
8
9
10
11
12
13
IPerson person = Factory.CreateInstance<IPerson>();
{
var p = person;
p.Name = "Arik Einstein";
p.Age = 73;
}
IPerson person2 = Factory.CreateInstance<IPerson>();
{
var p = person;
p.Name = "Shalom Hanoch";
p.Age = 65;
}

הפעולה הזאת מחקה קצת את הKeyword ששמו With מVB, למי שמכיר.

המשך יום מאותחל לטובה.

שתף

332. Don't reinvent the wheel

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

עם זאת, זה לא אומר שצריך להמציא את הגלגל מחדש. למה הכוונה?

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

  • הוספת תלמיד
  • הסרת תלמיד
  • בדיקה האם תלמיד נמצא
  • אפשרות לקבל את כל התלמידים

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

לכן אם אנחנו רוצים את כל הפונקציונאליות הזאת, כנראה מתאים שהממשק שלנו יכיל או יממש ICollection. כמובן, אם אנחנו רוצים פונקציונאליות יותר מנוונת (למשל רק הוספת תלמיד ואפשרות לקבל את כל התלמידים), יכול להיות שיותר מתאים ליצור פונקציות מתאימות בממשק שלנו (ולא לרשת משום דבר).

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

עם זאת, כאשר זה לא מתאים, כדאי לא להשתמש בכוח. למשל, אנחנו יכולים כמעט תמיד לממש IDictionary, אבל זה בד”כ לא מתאים. ירושה מIDictionary תכריח את המתכנתים שבאים לממש את הממשק לממש כ30 פונקציות, וכך למעשה הם יוותרו על מימושו.

סוף שבוע לא מומצא מחדש טוב.

שתף

331. Why encapsulation matters - part 2

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

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

בית ספר מכיל שכבות שמכילות כיתות שמכילות מחנכת ותלמידים.

הפעולות שכתבתי שאפשר לעשות הן:

  • לגלות איזה שכבות יש בבית ספר
    • כיתות יש בשכבה מסוימת
      • איזה תלמידים יש בכיתה מסוימת
      • מי המחנכת של כיתה מסוימת
    • להוסיף כיתה לשכבה
      • להוסיף תלמיד לכיתה

אז הממשקים שלנו יראו כך:

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
public interface ISchool
{
IEnumerable<IGradeYear> GradeYears { get; }
}
public interface IGradeYear
{
string Name { get; }
IEnumerable<IGrade> Grades { get; }
void AddGrade(IGrade grade);
}
public interface IGrade
{
string Name { get; }
IEnumerable<IStudent> Students { get; }
ITeacher Teacher { get; }
void AddStudent(IStudent student);
}
public interface IStudent
{
string Name { get; }
}
public interface ITeacher
{
string Name { get; }
}

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

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

אז מה הרווחנו:

  1. אבסטרקציה – אפשר לממש את הממשקים האלה בכל דרך שבא לנו
  2. בהירות – יותר ברור איך להשתמש בממשקים האלה מבטיפוס Dictionary<string, Dictionary<string, List<string>>>
  3. שליטה – אנחנו יכולים לשלוט בפעולות שמבצעים על המבנה נתונים שלנו – אם בפעם הקודמת יכלו להכניס לנו זבל למבנה נתונים, עכשיו אנחנו יכולים לבצע וואלידציה ברמת הפונקציות של הממשק
  4. הכמסה – אנחנו חושפים רק פעולות שאנחנו מעוניינים שיבצעו על המבנה נתונים. אם למשל בפעם הקודמת יכלו לאפס לנו את המבנה נתונים, אנחנו כבר לא חושפים פונקציות כאלה.

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
private static IEnumerable<string> GetRelevantStudents
(Dictionary<string, Dictionary<string, List<string>>> schoolStudents)
{
List<string> result = new List<string>();
foreach (KeyValuePair<string, Dictionary<string, List<string>>> gradeYearToGrade in schoolStudents)
{
foreach (KeyValuePair<string, List<string>> grade in gradeYearToGrade.Value)
{
foreach (string student in grade.Value)
{
if (student.StartsWith("S"))
{
result.Add(student);
}
}
}
}
return result;
}

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

ועל מה רצים בforeachים? למה יש פה KeyValuePair של Dictionary?

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

לעומת זאת, אם נממש בפתרון שהצעתי כעת, זה יראה כך:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
private static IEnumerable<IStudent> GetRelevantStudents(ISchool school)
{
List<IStudent> result = new List<IStudent>();
foreach (IGradeYear gradeYear in school.GradeYears)
{
foreach (IGrade grade in gradeYear.Grades)
{
foreach (IStudent student in grade.Students)
{
if (student.Name.StartsWith("S"))
{
result.Add(student);
}
}
}
}
return result;
}

לדעתי זה יותר קריא בפעם הראשונה, ויותר ברור.

אז מה אני רוצה?

יותר אבסטרקציה בין הממשק למימוש – לדעתי אם בממשק מסוים (במובן של API) חושפים פונקציה שמקבלת או מחזירה איזשהו IDictionary, או שיש לה איזשהו Property שהוא IDictionary – כנראה חסרה פה אבסטרקציה.

באופן דומה – אם יש פונקציה שמחזירה או מקבלת מימוש קונקרטי של ממשק מסוים, למשל List<string> או List<MyClass>, כנראה חסרה פה אנקפוסלציה. לא צריך להשתגע וליצור ממשק לכל דבר, אבל גם לא להתעצל ולהשקיע באבסטרקציה. (ראו גם טיפ מספר 81)

תחשבו גם על זה שאם יש לכם פונקציה שמקבלת IDictionary כלשהו, אבל תכלס עשיתם את זה כדי שתוכלו לגשת לDictionary במקום מסוים (או לחלופין לרוץ עליו), אז אתם מונעים מאנשים אחרים להשתמש בפונקציה שלכם, כי הם צריכים לממש IDictionary, כשכל מה שהייתם צריכים מהם זה לממש שתי פונקציות.

המשך יום מוכמס טוב.

שתף