שלום לכולם, הפעם נמשיך במסע שלנו ברחבי ה- design patterns.
אחרי שפעם קודמת דיברנו על The open close principle, ושם הזכרתי את ה decorator design pattern, אז הגיע הרגע לצלול פנימה
הבעיה
אז מעבר לזה ש decorator design pattern עוזר לנו לממש את ה- open close principle, הוא קודם כל בא לענות על בעיה אחרת
מה קורה כאשר יש לנו אוסף של מחלקות שהן מאוד דומות אחת לשניה אבל עם הבדלים קטנים?
האופציה הבנאלית היא להוסיף עוד מחלקות בעוד ירושות, מה שיכול לגרום לנו להתפוצצות אוכלוסין בקוד.
ומה הבעיה בהתפוצצות אולוסין אתם שואלים? הרי אנחנו עובדים ב OOP?
ובכן לתחזק קוד כזה, זה עינוי שלא ברא השטן.
האם כל פעם שאנחנו רוצים להוסיף התנהגות חדשה, אנחנו צריכים לקמפל את כל הפרויקט בגלל שהוספנו מחלקה?
לחפש איפה בדיוק צריך להוסיף, ואת מי בדיוק צריך לרשת?
ברוך הבא לבית הקפה
דמיינו את בית הקפה האידאלי, שאתם אוהבים לקנות בו קפה.
זה יכול להיות ארומה, ארקפה או סטארבקס.
בית הקפה הזה צריך מערכת הזמנות. לקוחות אצלנו יכולים להזמין משקאות מכל מיני סוגים. אבל כל משקה אפשר להכין מכמה סוגים.
למשל יש לנו קפוצ'ינו, אפשר עם חלב רגיל, חלב דל שומן, עם קפאין או בלי ועוד.
אז בואו נסתכל רגע על UML, של איך מערכת כזאת אמורה להיות:
המחלקה Beverage היא מחלקה אבסטרקטית, כל המשקאות בבית הקפה שלנו יורשות אותה.
השדה description הוא שדה מסוג String, אשר מכיל תיאור מילולי של המשקה. הוא מאותחל בכל מחלקה
למשל במחלקה DarkRoast הוא יהיה: "Most Excellent Dark Roast"
המתודה cost() היא מתודה אבסטרקטית, כלומר היא לא ממומשת במחלקה Beverage, כל תת מחלקה (sub class) צריכה להגדיר את המימוש.
בתמונה למעלה יש לנו רק את הסוגי משקאות, אבל כמו שכתבתי, אנחנו יכולים לצרף לכל משקה כזה המון תוספות שונות:
- סוגי חלב שונים
- תוספות מלמעלה כמו: סירופ שוקולד, קרמל כו'
אז בגישה הבאנלית, אנחנו ניצור עבור כל שילוב של משקה כזה מחלקה חדשה.
כי לכל שילוב כזה יש לנו מחיר אחר.
למה לא שדות?
בנקודה הזאת אנחנו יכולים לשאול: כל תוספת, וכל סוג חלב יכול להיות שדה בוליאני.
כי להוסיף חלב שקדים הוא אותו מחיר, לא משנה לאיזה משקה אנחנו מוסיפים אותו.
כנ"ל גם לתוספות.
אז המחלקה Beverage, תראה ככה:
כל השדות החדשים שהוספנו, הם שדות בוליאניים
בגרסה הזאת, המחלקה האבסטרקטית שלנו Beverage ממשמת את המתודה cost() בצורה הבאה:
public double cost() {
double condimentCost = 0.0;
if (hasMilk()) {
condimentCost += milkCost;
}
if (hasSoy()) {
condimentCost += soyCost;
}
if (hasMocha()) {
condimentCost += mochaCost;
}
if (hasWhip()) {
condimentCost += whipCost;
}
return condimentCost;
}
לדעתי, זה לא הרבה יותר טוב. כי קל מאוד שהמחלקה Beverage תהיה ענקית, עם עוד ועוד תוספות, ועוד סוגים של חלב.
זה לא סקלבילי, ובוודאי שזה לא יהיה קריא.
ובנוסף אם ניזכר ב open close principle, אז אנחנו רוצים להימנע משינוי של קוד קיים כאשר מתווספת לנו לוגיקה חדשה.
הכירו את Decorator Design Pattern
אז decorator pattern מוסיף אחרויות נוספת לאובייקט בצורה דינאמית, כלומר בזמן ריצה.
Decorator מספק אלטרנטיבה לירושה.
אז בואו נסתכל קודם על הUML הרשמי של Decorator pattern, ואז נראה איך אנחנו יכולים להתאים אותו אל הבית קפה שלנו
כל Component יכול להיות שמיש בזכות עצמו, אבל גם אפשר לעטוף אותו באמצעות Decorator.
ה ConcreteComponent זה האובייקט שאנחנו רוצים להוסיף לו התנהגות חדשה בצורה דינאמית
לכל Decorator, יש קשר של "אחד לאחד" כלומר "עוטף" Component. בעצם יש לו שדה של Component שעליו אנחנו רוצים להוסיף התנהגות חדשה.
ה Decorator צריך לממש את אותו ה interface או מחלקה אבסטרקטית של ה Component
בית קפה מקושט (Decorator)
אז ככה נראה הUML שלנו עבור בית קפה, אחרי שהחלנו עליו את ה Decorator design pattern:
אז במקרה שלנו ה Beverage משמש כ Component שלנו.
יש לנו 4 מימושים של ה Beverage
והוספנו 4 Decorator
אז איך זה עובד בפועל?
אנחנו ניצור אובייקט ממשי של Beverage, נעטוף (Decorate) עם אחד מה Decorator שלנו ובאמצעותו אנחנו נוסיף את המחיר של כל תוספת בצורה דינאמית.
בואו נראה איך זה נראה בקוד:
קודם כל הנה המחלקה Beverage
public abstract class Beverage {
String description = "Unknown Beverage";
public String description() {
return description;
}
public abstract double cost();
}
בסופו של דבר, זו מחלקה די פשוטה.
עכשיו נציג את הCondimentDecorator:
package org.decorator.src.final_solution;
public abstract class CondimentDecorator extends Beverage{
Beverage beverage;
public abstract String getDescription();
}
נשים לב שהמחלקה הזאת מרחיבה את Beverage, ויש לה שדה של אותו מחלקה, כדי שהיא תוכל לעטוף אותו
בנוסף אנחנו מכריחים שכל ה Decorator שלנו יצטרכו לממש את getDescription
יאללה, בואו ניצור כמה משקאות:
public class Espresso extends Beverage {
public Espresso() {
description = "Espresso";
}
@Override
public double cost() {
return 1.99;
}
}
public class Espresso extends Beverage {
public Espresso() {
description = "Espresso";
}
@Override
public double cost() {
return 1.99;
}
}
אז פה יש לנו דוגמה של שני משקאות. כל אחד מהם מגדיר את ה description שלו בבנאי, ובנוסף, מממש את cost
יאללה, אל הdecorator
public class Mocha extends CondimentDecorator {
public Mocha(Beverage beverage) {
this.beverage = beverage;
}
@Override
public double cost() {
return beverage.cost() + 0.2;
}
@Override
public String getDescription() {
return beverage.description + ", Mocha";
}
}
אז נשים לב שהמחלקה Mocha מרחיבה את CondimentDecorator
ובבנאי אנחנו מאתחלים את השדה שאנחנו עוטפים.
והכי חשוב, גם ב cost() וגם ב getDescription() אנחנו משתמשים במה שיש ב beverage על מנת לחשב את התוצאה הרצויה
אז איך כל זה מתחבר? ככה:
ublic class MyCoffeeShop {
public static void main(String[] args) {
Beverage beverage = new Espresso();
System.out.println(beverage.getDescription() + " $" + beverage.cost());
Beverage beverage2 = new DarkRoast();
beverage2 = new Mocha(beverage2);
beverage2 = new Mocha(beverage2);
beverage2 = new Whip(beverage2);
System.out.println(beverage2.getDescription() + " $" + beverage.cost());
Beverage beverage3 = new HouseBlend();
beverage3 = new Soy(beverage3);
beverage3 = new Mocha(beverage3);
beverage3 = new Whip(beverage3);
System.out.println(beverage3.getDescription() + " $" + beverage3.cost());
}
}
והפלט של זה הוא:
Espresso $1.99 Dark Roast Coffee, Mocha, Mocha, Whip $1.99 House Blend Coffee, Soy, Mocha, Whip $1.79
סיכום
היום למדנו בעצם מה זה בכלל Decorator design pattern, למה הוא משמש ואילו בעיות של ירושה הוא פותר לנו.
מקווה שנהניתם.
את כל הקוד אתם יכולים למצוא בgithub הבא
וכמו תמיד, אשמח לקבל כל שאלה, הערה או הארה שיש לכם
פינגבאק: להיות סתגלתנים חלק א' - אדפטר - שורת קוד