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, כשכל מה שהייתם צריכים מהם זה לממש שתי פונקציות.

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

שתף