Menu
Coddy logo textTech

תבנית העיצוב

חלק מהיחידה תכנות מונחה עצמים במסלול ה-C++ של Coddy. שיעור 98 מתוך 104.

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

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

#include <iostream>
#include <memory>

// ממשק רכיב
class Coffee {
public:
    virtual std::string getDescription() const = 0;
    virtual double getCost() const = 0;
    virtual ~Coffee() = default;
};

// רכיב קונקרטי
class SimpleCoffee : public Coffee {
public:
    std::string getDescription() const override { return "Coffee"; }
    double getCost() const override { return 2.0; }
};

// דקורטור בסיסי
class CoffeeDecorator : public Coffee {
protected:
    std::unique_ptr<Coffee> coffee;
public:
    CoffeeDecorator(std::unique_ptr<Coffee> c) : coffee(std::move(c)) {}
};

// דקורטורים קונקרטיים
class MilkDecorator : public CoffeeDecorator {
public:
    MilkDecorator(std::unique_ptr<Coffee> c) : CoffeeDecorator(std::move(c)) {}
    std::string getDescription() const override {
        return coffee->getDescription() + ", Milk";
    }
    double getCost() const override { return coffee->getCost() + 0.5; }
};

class SugarDecorator : public CoffeeDecorator {
public:
    SugarDecorator(std::unique_ptr<Coffee> c) : CoffeeDecorator(std::move(c)) {}
    std::string getDescription() const override {
        return coffee->getDescription() + ", Sugar";
    }
    double getCost() const override { return coffee->getCost() + 0.2; }
};

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

int main() {
    std::unique_ptr<Coffee> order = std::make_unique<SimpleCoffee>();
    order = std::make_unique<MilkDecorator>(std::move(order));
    order = std::make_unique<SugarDecorator>(std::move(order));
    
    std::cout << order->getDescription() << ": $" << order->getCost();
    // פלט: Coffee, Milk, Sugar: $2.7
}

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

challenge icon

אתגר

קל

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

תארגנו את הקוד בשלושה קבצים:

  • Pizza.h: הגדירו את ממשק הרכיב ואת מחלקת הפיצה הבסיסית.

    צרו מחלקה מופשטת Pizza עם המתודות הווירטואליות הטהורות getDescription() (מחזירה מחרוזת) ו-getCost() (מחזירה מספר מסוג double), וכן מפרק וירטואלי.

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

  • ToppingDecorator.h: בנו את מחלקת הבסיס של העוטף ואת העוטפים הקונקרטיים של התוספות.

    צרו מחלקה בשם ToppingDecorator שיורשת מ-Pizza ומחזיקה std::unique_ptr<Pizza> לרכיב העטוף. מחלקה זו משמשת בסיס לכל העוטפים של התוספות.

    ממשו שלושה עוטפים קונקרטיים של תוספות:

    • CheeseTopping: מוסיפה ", Cheese" לתיאור ו-1.5 לעלות
    • PepperoniTopping: מוסיפה ", Pepperoni" לתיאור ו-2.0 לעלות
    • MushroomTopping: מוסיפה ", Mushrooms" לתיאור ו-1.0 לעלות

    כל עוטף צריך להעביר את הקריאה לפיצה העטופה ואז להוסיף את התרומה שלו.

  • main.cpp: צרו הזמנת פיצה מותאמת אישית על סמך קלט המשתמש.

    קראו שלושה קלטים (כל אחד מהם יהיה yes או no):

    1. להוסיף גבינה?
    2. להוסיף פפרוני?
    3. להוסיף פטריות?

    התחילו עם PlainPizza, ואז עטפו אותה בעוטפי התוספות המתאימים בהתאם לקלטים (בסדר הזה: גבינה, פפרוני, פטריות). לבסוף, הדפיסו את פרטי ההזמנה בפורמט הבא:

    Order: [description]
    Total: $[cost]

לדוגמה, עם הקלטים yes, yes, no:

Order: Pizza, Cheese, Pepperoni
Total: $11.5

עם הקלטים yes, no, yes:

Order: Pizza, Cheese, Mushrooms
Total: $10.5

עם הקלטים no, no, no:

Order: Pizza
Total: $8

עם הקלטים yes, yes, yes:

Order: Pizza, Cheese, Pepperoni, Mushrooms
Total: $12.5

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

נסו בעצמכם

#include <iostream>
#include <string>
#include <memory>
#include "Pizza.h"
#include "ToppingDecorator.h"

using namespace std;

int main() {
    // קראו שלושה קלטים (כן או לא)
    string addCheese, addPepperoni, addMushrooms;
    cin >> addCheese;
    cin >> addPepperoni;
    cin >> addMushrooms;
    
    // TODO: התחילו עם PlainPizza באמצעות std::unique_ptr
    
    // TODO: אם addCheese הוא "yes", עטפו את הפיצה ב-CheeseTopping
    
    // TODO: אם addPepperoni הוא "yes", עטפו את הפיצה ב-PepperoniTopping
    
    // TODO: אם addMushrooms הוא "yes", עטפו את הפיצה ב-MushroomTopping
    
    // TODO: הדפיסו את ההזמנה בפורמט:
    // הזמנה: [תיאור]
    // סה״כ: $[עלות]
    
    return 0;
}
quiz iconבחנו את עצמכם

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

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

תרגלו בעצמכם: קומפיילר C++ אונליין