יש הרבה דרכים לדחוף אובייקטים לתוך אוסף.
לשים אותם במערך, מחסנית, רשימה, מפה – רק תבחרו
יש סיכוי טוב, שבשלב מסוים נרצה לעבור על כל האיברים באוסף הזה.
בפוסט הזה אנחנו נראה איך אפשר לאחסן אובייקטים באוסף כלשהו, ועדיין לתת את האופציה לעבור עליהם
מבלי לחשוף את הדרך בה הם מאוחסנים, על מנת שנוכל לשמור על גנריות.
בואו נתתחיל
כמו כל הפוסטים בסדרה הזאת, גם הפוסט הזה מבוסס על הספר Head First Design Pattern
מקום אחד, שני תפריטים
הסיפור של הפוסט הזה הוא על מסעדה, אשר רוצה להגיש שני תפריטים שונים לאורך היום
תפריט בוקר, ותפריט ערב.
את תפריט הבוקר, אנחנו מקבלים ממסעה בשם בית הפנקייק
ואת תפריט הערב אנחנו מקבלים ממקום בשם הדיינר.
ככה התפריטים ממומשים כרגע:
הפריטים שהתפריטים האלו משתמשים בהם, הם אותו דבר:
הקוד די פשוט נכון?
אבל כל תפריט ממומש בדרך שונה:
כפי שאפשר לראות, כל תפריט ממומש בצורה שונה.
התפריט של בית הפנקייק משתמש ב arrayList, ואילו הדיינר משתמש במערך בגודל קבוע מראש
יש גם דמיון בין התפריטים.
שניהם ממלאים את האוסף שלהם בבנאי, ויש מתודה אשר מחזירה את כל האוסף החוצה.
מה הבעיה עם שני סוגים שונים?
כדי להבין מה הבעיה בכך שיש לנו שני מימושים שונים לתפריט,
בואו ננסה לכתוב קליינט אשר ידע לקבל את שני התפריטים הללו.
הקליינט שלנו יהיה המלצרית, ואלו הדרישות שקיבלנו עבורה
printMenu - מדפיסה את כל התפריט בוקר ואת תפריט הערב printBreakfastMenu - מדפיסה את תפריט הבוקר printDinnerMenu - מדפיסה את תפריט הערב printVegetarianMenu - מדפיסה את כל המנות הצימחוניות isItemVegetarina(name)- מחזירה האם הפריט הספציפי הזה הוא צמחוני או לא
הנה הניסיון הראשון שלנו לממש את המלצרית
קודם כל נתחיל מ-printMenu.
על מנת להדפיס את כל הפריטים משני התפריטים, קודם כל נצטרך לקבל את האוספים שלהם
הצורה אולי דומה, אבל ערכי ההחזרה הם שונים.
עכשיו אנחנו צריכים לעבור על כל האיברים, ולהדפיס אותם:
כיוון שיש לנו שני אוספים שונים, אנחנו צריכים לממש שתי לולאות שונות.
אחת עבור Array List ואחת עבור Array.
מימוש של כל פונקציה אחרת של Waitress יהיה בצורה דומה מאוד למה שראינו עכשיו.
אנחנו נקבל את שני התפריטים (או כל אחד מהם בנפרד, תלוי מה דרוש)
נעבור עליהם בלולאה, ונעשה את הפעולה הדרושה.
ואם נרצה להוסיף תפריט חדש, אנחנו נצטרך להוסיף לולאה חדשה.
לפי המימוש שכתבנו עכשיו עבור printMenus:
- אנחנו כותבים למימוש של התפריטים הללו, ולא לאבסטרקציה כלשהי (code to interface)
- אם נרצה להחליף את המימוש של התפריט של הדיינר, למשל, למימוש עם אוסף אחר, אנחנו נצטרך לשנות הרבה קוד
- המלצרית צריכה לדעת מה המימוש של כל תפריט איתו היא עובדת, על מנת לעשות את העבודה שלה. זה מפר את עקרון ההכמסה
- יש לנו קוד כפול ב printMenu. הקוד צריך שני לולאות שונות על מנת לעבור על שני תפריטים, ואם נוסיף עוד תפריט, יהיה לנו עוד לולאה
כיוון שקיבלנו את התפריטים שלנו כספריות, אנחנו לא יכולים לשנות אותם
אז אנחנו צריכים למצוא דרך לכתוב interface אשר ייצג את שני התפריטים, וכל תפריט חדש בעתיד.
זה יקל עלינו מאוד להרחיב קוד בעתיד, ולפשט את הקוד של המלצרית
הכמסה של איטרציות
הבעיה שלנו נוצרה כיוון שאנחנו מקבלים שני סוגים של אוספים לעשות עליהם איטרציה.
השאלה היא האם אנחנו יכולים להכמיס (to encapsulate) אותם?
כאשר אנחנו עושים איטרציה על ה-Array List, אנחנו משתמשים ב-size עבור התנאי של הלולאה
ואנחנו משתמשים ב-get על מנת לקבל את האיבר הספציפי
וכאשר אנחנו עושים איטרציה על ה-Array, אנחנו משתמשים ב-length עבור התנאי
ואנחנו משתמשים באופרטור [] כדי לקבל איבר ספציפי.
ננסה ליצור אובייקט בשם Iterator, אשר מכמיס את האיטרציה על האובייקטים:
כאשר נשתמש באובייקט החדש שלנו על ה-ArrayList זה יראה ככה
אנחנו מבקשים מה-breakfastMenu להביא לנו איטראטור של הפרטים שלו
וכל עוד (while) יש לו עוד פרטים לעבור עליהם, אנחנו נקבל ממנו את הפריט הבא.
כאשר המלצרית קוראת ל-hasNext ול-next, האיטראטור שלנו קורא ל-get מאחורי הקלעים.
עכשיו ננסה את האיטראטור על Array
הקוד נראה זהה לחלוטין
ההבדל הוא שכאן האיטראטור קורא לאופרטור [] מאחורי הקלעים.
הכירו את האיטראטור
הצלחנו להכמיס את האיטרציה, ויש מצב שזה יפתור לנו את כל הבעיות
וכמו שניחשתם זהו design pattern בשם Iterator design pattern.
הדבר הראשון שאתם צריכים לדעת עליו שהוא מסתמך על interface:
יש לו שתי מתודות
hasNext מחזירה בוליאני, אשר מייצג האם יש עוד איברים לעבור עליהם
המתודה next מחזירה את האיבר הבא ברשימה
עכשיו כשיש לנו interface הגיע הזמן לממש אותו
אנחנו יכולים לממש אותו עבור כל אחד מהאוספים שאנחנו מכירים
אנחנו נתחיל לממש אותו עבור הדיינר
איך זה נראה כקוד?
יש לנו פה את ה-interface עם שתי המתודות.
ואת המימוש שלו ל-diner.
ה-position מצביע על האיבר האחרון שעשינו עליו איטרציה.
הבנאי מקבל את המערך שעליו צריך לעשות איטרציה
המתודה next מחזירה את האיבר הבא ברשימה
והמתודה hasNext בודקת אם עברנו את האורך של המערך, או שהגענו ל-null.
בשני המקרים הללו היא תחזיר false, כלומר שאין לנו עוד איברים לעבור עליהם
עכשיו אנחנו נצטרך לעדכן את DinerMenu שלנו:
נשים לב שמחקנו את getMenuItems
ובמקומו הכנסנו את createIterator.
נתקן עכשיו את הקוד של המלצרית
הרבה יותר נחמד נכון?
בבנאי של המלצרית היא מקבלת את התפריטים שהיא צריכה לנהל
המתודה printMethod יוצרת את שני האיטרטורים, ואז היא משתמש בכל אחד מהם על מנת להדפיס את הפרטים.
אם נצטרך להוסיף תפריט חדש, פשוט ניצור עוד איטרטור ונשתמש בו.
וככה בעצם ירדנו מפונטציאל של לולאה עבור כל אוסף ללולאה אחת ויחידה.
אפשר לשפר עוד קצת?
קודם כל אולי כדאי להציג בשלב זה לג'אווה יש כבר iterator interface והוא נראה ככה
אז למה לא השתמשנו בו ישר על התתחלה?
ובכן רציתי שתראו איך אפשר להפוך אותו לגמיש, איך בעצם מי שכתב אותו, הפך אותו לגמיש.
אבל במקרה שלנו אין משמעות שהמלצרית תמחוק איברים מהתפריט.
אז אם אנחנו רוצים להשתמש במה שקיים היום כבר בג'אווה, ולא לכתוב הכל מאפס
זה יראה ככה:
במקרה הזה אנחנו יורשים מהאיטראטור של ג'אווה
וכאשר יקראו למתודה remove אנחנו נזרוק שגיאה
בנוסף אנחנו ניצור interface עבור כל התפריטים שלנו
ועכשיו הקוד של המלצרית שלנו יראה ככה
הגדרה רשמית של איטראטור
ראינו איך אפשר לממש את איטראטור עם מימוש שלכם
ראינו גם איך אפשר להשתמש במימוש של ג'אווה על מנת ליצור את אותן יכולות.
עכשיו נוכל להסתכל על ההגדרה הרשמית
איטראטור מספק לנו דרך לגשת לאלמנטים של אובייקט מבלי לחשוף את המימוש של דרך האחזקה שלהם
ברגע שאנחנו מכניסים את ה-design pattern הזה לקוד שלנו
הוא הופך להיות גנרי, ולא משנה עם איזה אוסף אנחנו נשתמש, הוא עדיין יוכל לעשות עליו איטרציה
מבחינת ה-UML הוא נראה ככה:
ה-aggregate הוא interface אשר מאגד את כל האובייקטים שמחזיקים איזשהו אוסף שעליו נרצה לעשות איטרציה
במקרה שלנו אלו ה-Menu
את האיטראטורים כבר הכרנו
הקליינט במקרה שלנו זו המלצרית.
סיכום
אנחנו נעצור פה הפעם
ראינו למה אנחנו צריכים דרך גנרית לעשות איטרציה על אובייקטים
מדוע זה חשוב, אבל לא סיימנו
בפעם הבאה נמשיך לאתגר את ה-design pattern הזה עוד
על מנת לשפר אותו
אם הגעתם עד לפה, אשמח לשיתוף של הפוסט על מנת להגדיל את התפוצה של הבלוג
וכמו תמיד, אשמח לקבל כל הערה, הארה או שאלה שיש לכם