הרכבה ואיטראטור – The Iterator and Composite Design Pattern

בפעם הקודמת דיברנו על איטראטור, ועצרנו שם באמצע.
בפוסט הזה אנחנו נמשיך את המסע להכיר עוד design patterns,
נכיר עוד עקרון עיצוב חדש, ואחד שאני בהחלט מאוד מאמין בו
ובנוסף נכיר עוד design pattern חדש בשם קומפוזיט (Composite)
בואו נתחיל

הבהרה – הפוסט הזה, כמו כל שאר הפוסטים בסדרה מבוסס על הספר הנהדר Head First Design Pattern

אחריות יחידה

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

UML המייצג את איטראטור
UML המייצג את איטראטור

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

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

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


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

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

Iterable

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

Iterable and Collection interface in Java
Iterable and Collection interface in Java

בלי ששמנו לב, כבר בקוד שראינו בפוסט הקודם, כבר השתמשנו ב-iterable
כל אוסף (Collection) בג'אווה מממש את ה-interface הזה.
אתם יכולים לראות, ש-iterable בעצם דורש שכל מי שמממש אותו, יממש פונקציה אשר מחזירה איטראטור.
בנוסף, הוא מוסיף עוד מתודה, forEach שמספקת לנו דרך לכתוב איטראציות בצורה יותר נוחה
באמצעות לולאות for each.
אם יש לנו קוד כזה:

אפשר להמיר אותו לקוד כזה:

יותר נחמד לא?
אבל רגע, יש לנו בעיה.
אתם זוכרים שבאחד התפריטים השתמשנו ב-Array?
ובכן Array הוא לא Collection, אז הוא לא מממש את Iterable.
וזו הסיבה למה לא הראיתי את הפתרון הזה בפוסט הקודם
אבל התפזרנו פה קצת, בואו נחזור לדבר על איטראטור.

תפריט חדש

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

בואו נסתכל מה קיבלנו הפעם:

נראה מוכר נכון?
רק שהפעם נתנו לנו HashMap, שזו בעצם מפה שהמפתח זה השם של הפריט.
שימו לב שאנחנו מקבלים איטראטור עבור הפריטים במפה (values) ולא לכל המפה

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

הוספנו את התפריט החדש בתור שדה
ואז קיבלנו את האיטראטור שלו ואנחנו מדפיסים אותו

מה עשינו עד עכשיו

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

אז בעצם אנחנו יצרנו הפרדה בין המלצרית למימוש של התפריטים
עשינו זאת על ידי כך שסיפקנו לה איטראטור גנרי כך שהיא יכולה לעשות איטרציה על התפריטים, מבלי הצורך לדעת מה עומד מאחורי הקלעים
בנוסף ראינו שאנחנו יכולים להביא עוד סוגים של Collection כמו למשל Hash Map, והמלצרית תדע להתמודד עם זה בקלות
בגלל שכל Collection בג'אווה מממש את ה-iterable interface, אשר דואג שהוא יוכל לספק iterator

אני מריח שכפול קוד

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

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

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

אילו החיים היו כאלה קלים

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

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

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

  • אנחנו צריכים מבנה נתונים בצורה של עץ, שירכיב תפריט, את תפריטי המשנה שלו, ואת הפריטים
  • אנחנו רוצים שתהיה לנו דרך קלה לעשות איטרציה על הפריטים של כל העץ, ותתי עצים בצורה קלה כמו שהיה לנו עכשיו עם האיטראטור

הכירו את Composite Design Pattern

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

קודם כל נגדיר מהו ה-composite design pattern

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

נשמע מושלם לא?
ה-design pattern הזה מאפשר לנו להגדיר מבנה נתונים בצורה של עץ,
שיכול לטפל בתתי עצים ועלים באותו מבנה.
בעצם הוא מאפשר לנו להגדיר תפריטים, תתי תפריטים ופריטים באותו מבנה.

איך זה נראה? ובכן ככה:

UML של Composite
UML של Composite

הקליינט משתמש ב-component interface על מנת לגשת למידע שנמצא בעץ שלנו
הקומפוננט מגדיר interface עבור כל האובייקטים בעץ, גם עבור העלים וגם עבור התתי עצים
הקומפוננט יכול להגדיר התנהגויות דיפולטיביות עבור כל המתודות שלו
עלה מגדיר את ההתנהגות עבור האלמנטים בעץ שלנו, והם ממשים רק את ה-operation
כמובן שלעלים אין ילדים.
הקומפוזיט מהווה את התתי עצים אצלנו. שימו לב לחץ בינו לבין הקומפוננט, מה שמייצג ילדים.

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

עיצוב מחדש של המערכת עם קומפוזיט

אז איך אנחנו מכילים את הקומפוזיט על המערכת שלנו?
קודם כל אנחנו נצטרך לבנות interface שמייצג את הקומפוננט.
הוא יהיה ה-interface עבור התפריטים ועבור הפריטים בתפריט.

UML של המערכת שלנו יחד עם קומפוסיט
UML של המערכת שלנו יחד עם קומפוסיט

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


רגע מה? לא אמרנו מימוש דיפולטיבי?
מה פתאום לזרוק שגיאה עבור כל אחד מהם? למה לא לעשות interface וזהו?
ובכן אלו שאולות מעולות, אבל אנחנו צריכים להבין
שחלק מהמתודות הללו רלוונטיות רק ל-MenuItem כמו למשל isVegetarian
ויש לנו מתודות שרלוונטיות רק ל-Menu, כמו למשל getChild
המתודה print היא ה-operation של הקומפוננט.

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

גם ב-Menu אנחנו נרחיב את הקומפוננט
נשים לב שלתפריט יכול להיות מספר רב של ילדים, אנחנו נשתמש ב ArrayList כדי להחזיק אותם
נשים לב שעכשיו הבנאי שלנו מקבל שם ותיאור. זה שונה ממה שהיה לנו לפני
אנחנו יכולים להשתמש ב-add וב-remove כדי לשלוט בילדים
נשים לב שאנחנו לא דורסים את getPrice ואת isVegetarian בגלל שאין הגיון לעשות את זה ב-Menu
זה למה מימשנו את זה בMenuComponent.
והיתרון הגדול שלנו הוא שעכשיו אנחנו יכולים להשתמש בלולאת for רגילה
אבל אם היינו רוצים, היינו יכולים להתשמש גם ב-iterator פה.

רגע לא אמרנו מחלקה אחת עם אחריות אחת?

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

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

מה הכוונה בשקיפות?
ובכן, בכך שאנחנו מאפשרים לקומפוננט להיות גם עלה וגם ניהול של תתי עצים
אנחנו מאפשרים ל-client להתנהל בצורה שקופה מולם, הוא לא צריך לחשוב
רגע זה עלה? זה תת עץ? ולשנות את ההתנהגות שלו.

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

סיכום

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

השני היה הקומפוזיט, אשר מאפשר לנו לבנות מבני נתונים דמויי עץ
וניהלנו דיון מעניין על פשרות בקוד

את כל הקוד אתם יכולים למצוא בגיטהאב הזה

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

השאר תגובה

Scroll to Top