טמפלייט – The Template Method Pattern

שלום לכולם,
היום אנחנו ממשיכים במסע שלנו ברחבי ה-design pattern.
והפעם נדבר על template method pattern.


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

כל הסידרה הזאת על design pattern מבוססת על הספר הנהדר Head First Design Patterns

אין דבר כזה יותר מדי קפה

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

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

הוראות להכנת קפה:
1. הרתיחו מים
2. שימו קפה בתוך כוס
3. מזגו את המים לתוך הכוס
4. הוסיפו סוכר וחלב לפי הצורך

הוראות להכנת תה:
1. הרתיחו מים
2. שימו תיון בתוך כוס
3. מזגו את המים לתוך הכוס
4. הוסיפו לימון


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


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

יצירת אבסטרקציה למשקה עם קפאין
יצירת אבסטרקציה למשקה עם קפאין

הפונקציות boidWater ו-pourWaterInCup משותפות לשתי המחלקות.
ולכן הן עוברות להיות ב-CaffeineBeverage
אבל ה-addBaseIngredients וה-addCondiments משתנות בין שני המשקאות שלנו
ולכן הן יהיו אבסטרקטיות

האם פספסנו משהו?

האם עשינו עבודה טובה עם ה-design שלנו?
פספסנו משהו?
לקפה ותה יש עוד משהו במשותף?

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

1. להרתיח מים
2. לשים מרכיב בסיסי בתוך הכוס (תיון או קפה)
3. למזוג את המים הרותחים לתוך הכוס
4. להוסיף תוספות למשקה


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


אבל צעדים 2 ו-4 הם ממומשים ב-sub classes, ולכן אין להם אבסטרקציה
אבל בפועל, הם עושים את אותה פעולה, עם משתנים שונים
אז אולי יש דרך שנוכל ליצור לזה אבסטרקציה?

ליצור אבסטרקציה עבור preprareRecipe

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

הבעיה הראשונה שיש לנו היא שקפה משתמש ב-putCoffeeInCup וב-AddSugarAndMilk
ואילו תה משתמש ב-putTeaBagInCup וב-addLemon:

TeaCoffee
prepareRecipeprepareRecipe
boilWater boilWater
putTeaBagInCupputCoffeeInCup
pourWaterToCuppourWaterToCup
addLemonaddSugarAndMilk
השוואה בין prepareRecipe של קפה ושל תה

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

עכשיו הקוד שלנו יראה ככה:

אחרי שעשינו את הצעד הזה, אנחנו יכולים להסתכל על דוגמה מלאה
נתחיל עם ה-CaffeineBeverage:

קודם כל נשים לב שהמחלקה הזאת היא אבסטרקטית, בדיוק כמו ב-design שעשינו
וגם אפשר לרואת את ה-prepareRecipe שכתבנו למעלה
הגדרנו אותה כ-final כי אנחנו לא רוצים שה-sub classes שלנו יוכלו לשנות את האלגוריתם
בנוסף, הפונקציות addBaseIngridientToCup ו-addCondiments הן אבסטרקטיות
על מנת לאפשר לכל sub class לממש את זה בעצמן

הקוד הבא שלנו יהיה תה וקפה

בדיוק כמו ב-design המחלקה Tea יורשת מ-CaffeineBeverage
והיא צריכה לממש את שתי הפונקציות האבסטרקטיות
אותו דבר לגבי קפה.

אז מה עשינו עד עכשיו?

לסיכום ביניים של התהליך שעשינו עד עכשיו בפוסט:
לקחנו שני מתכונים יחסית דומים, אחד להכנת קפה ואחד להכנת תה
הוצאנו מהם את מה שהיה משותף, ועשינו לזה אבסטרקציה
ובעצם הגדרנו מחלקה ששולטת באלגוריתם של הכנת המשקה – CaffeineBeverage
והיא תלויה ב-sub classses שלה על מנת לבצע צעדים ספיציפיים בתוכו

הכירו את טמפלייט – Template Method

בלי לשים לב ממימשנו את Template Method Design Pattern
אם נסתכל על המבנה של CaffeineBeverage :
prepareRecipie היא ה-tempate method שלנו
למה? בגלל

  • היא מתודה בסופו של דבר
  • היא מגדירה מהו ה-template, מהו האלגוריתם להכנת המשקאות

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

ה-template method מגדיר את הצעדים של האלגוריתם 
ומאפשר ל-sub classesלספק את המימוש של צעד אחד או יותר

מה template method נותן לנו?

לפניאחרי
קפה ותה מנהלים את ההצגהה-CaffeineBeverage מנהל את ההצגה. יש לו אלגוריתם והוא מגן עליו
יש לנו קוד משוכפל בין מחלקותאנחנו מנצלים את המחלקה הראשית על מנת למנוע שכפולי קוד ככל הניתן
שינויים באלגוריתם דורשים שיני של כל המחלקות הקיימות, כלומר לשינויים רביםהאלגוריתם חי במקום אחד, ושינוי שלו דורש שינוי במקום אחד בלבד
על מנת להוסיף משקה חדש, דרושה עבודה רבה של שכפול וכתיבת הרבה קודה-template method pattern מספק לנו תשתית כך שמשקאות חדשים יכולים להתחבר אליה, ולכן משקאות חדשים מממשים מעט מתודות
הידע על האלגוריתם, ואיך מממשים אותו, מפוזר בין הרבה מחלקותהידע של האלגוריתם מרוכז במקום אחד, ותלוי ב-sub classes לצעדים ספציפיים
לפני ואחרי שימוש ב template method pattern

הגדרה רשמית של template method pattern

ראינו את template method בפעולה
עכשיו נגדיר אותו בצורה רשמית

ה-template method design pattern מגדיר את השלד של האלגוריתם במתודה.
ומשהה את המימוש של חלק מהצעדים ל-sub classes.
Template method נותן ל-subclasses להגדיר מחדש חלק מהצעדים,
ללא שינוי במבנה האלגוריתם עצמו


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

UML של template method pattern
UML של template method pattern

ה-AbstractClass מכילה את ה tempate method.
והגדרה של מתודות אבסטרקטיות, אשר מייצגות צעדים שה-subclasses צריכים להגדיר מחדש.
היא עושה שימוש בצעדים הללו, אבל היא מנותקת מהמימוש עצמו, מה שנותן לה גמישות

שימו לב, שיכולות להיות הרבה concreteClass,
כל אחת מממשת את האלגוריתם בצורה שונה.

איך יראה קוד כזה בצורה גנרית?

בואו נסתכל מקרוב על מימוש של ה-design pattern
נתחיל מה-AbstractCalss

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

בנוסף אנחנו יכולים לראות את ה-template method שלה.
היא מגדירה את סדר הפעולות של האלגוריתם שלנו

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

template method with hooks

יש לי חידה בשבילכם
תסתכל על הקוד הבא, ותנסו לחשוב מה השינוי שעשיתי
ובשביל מה עשיתי אותו?

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

אבל רגע, הצעד הזה לא עושה כלום!
מה הקטע?
בשביל מה הוא שם?

צעדים כאלו נועדו לתת ל- subclasses את האפשרות לדרוס את הצעד הזה
אבל הן לא חייבות לעשות את זה

בעצם hook היא מתודה אשר מוגדרת ב abstarct class אבל מקבלת מימוש ריק
זה נותן ל-subclasses את את היכולת לשחק קצת עם האלגוריתם, אם הן רוצות
אלו נקודות באלגוריתם שאנחנו רוצים לתת ל-subclasses שלנו את האפשרות להוסיף התנהגות
אבל זו התנהגות אופציונאלית, לא כל ריצה של האלגוריתם מחייבת שימוש של הצעדים הללו.

בואו נסתכל על דוגמה, מהכיוון של הקפה


זה המחלקה CaffeineBeverage שהשתמשנו בה בתחילת הפוסט
אבל שינינו אותה קצת, הוספנו לה hook

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

הנה למשל, המחלקה CoffeeWithHook אשר דורסת את ההחלטה הדיפולטיבית:

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

שאלות טובות

כאשר יוצר template method, איך נדע מתי להשתמש ב-hooks? ומתי להשתמש במתודות אבסטרקטיות?

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

מה המטרה האמיתית של hook?

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

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

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

עקרון הוליווד – The Hollywood Principle

אמרתי בהתחלה שה-template method קיבל השראה מהוליווד
אבל לא סיפרתי לכם עד עכשיו מהי
ובכן הכירו את עקרון הוליווד:

אל תתקשר אלינו, אנחנו נתקשר אליך


קל לזכור נכון? אבל מה הקשר ל-template?

העקרון נותן לנו דרך להילחם בריקבון של תלויות – dependecy rot.

מה זה ריקבון של תלויות?
ריקבון של תלויות קורה כאשר יש לנו high level component אשר תלויה ב-low level component אשר תלויה ב high level component אשר תלויה ב side way componentn אשר תלויה……

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

עם עקרון הוליווד, אנחנו מאפשרים ל-low level component לחבר את עצמן למערכת (to hook )
אבל ה-high level component מחליטה מתי ואיך צריכים אותן.
במילים אחרות, ה-high level component אומרת ל – low level component:

אל תקראי לי, אני אקרא לך

אותו דבר קורה גם ב-template method pattern
ה-template method מאפשרת לכל ה -subclasses להתחבר אליה
אבל היא מחליטה מתי היא קוראת להן, ואיך

סיכום

בפוסט הזה הכרנו גם את template method pattern
וגם את עקרון הוליווד

ה-teplate method מגדיר את צעדי האלגוריתם, ומאפשר ל-subclasses לממש חלק מהצעדים
הוא יכול להגדיר צעדים שמשותפים לכולם, ובכך למנוע שכפול של קוד
הדרך שבה הוא דורש מימוש אצל ה subclasses הוא בשימוש עם מתודות אבסטרקטיות

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

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

דוגמאות מלאות של הקוד אפשר למצוא כאן

וכמו תמיד, אשמח לקבל כל שאלה, הערה או הארה שיש לכם

השאר תגובה

Scroll to Top