Menu
CoddyTech

Mixins באמצעות CRTP

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

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

מבנה ה-CRTP הבסיסי נראה כך:

template <typename Derived>
class Base {
public:
    void interface() {
        static_cast<Derived*>(this)->implementation();
    }
};

class MyClass : public Base<MyClass> {
public:
    void implementation() {
        std::cout << "MyClass implementation\n";
    }
};

CRTP עוצמתי במיוחד ליצירת mixins — פונקציונליות לשימוש חוזר שאפשר "לערבב" בתוך מחלקות. בניגוד לירושה מסורתית, mixins מוסיפים יכולות בלי ליצור היררכיות עמוקות:

template <typename Derived>
class Printable {
public:
    void print() const {
        const Derived& self = static_cast<const Derived&>(*this);
        std::cout << self.toString() << "\n";
    }
};

class Person : public Printable<Person> {
    std::string name;
public:
    Person(const std::string& n) : name(n) {}
    std::string toString() const { return "Person: " + name; }
};

// שימוש:
Person p("Alice");
p.print();  // פלט: Person: Alice

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

challenge icon

אתגר

קל

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

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

  • Mixins.h: הגדירו שתי תבניות של מיקסינים מסוג CRTP שמספקות פונקציונליות לשימוש חוזר.

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

    צרו תבנית מיקסין בשם Describable שמספקת מתודה בשם describe(). המתודה הזאת צריכה להשתמש ב-static_cast כדי לגשת למחלקה היורשת ולקרוא למתודה שלה getDescription(), ואז להדפיס את התוצאה ואחריה שורה חדשה.

    זכרו שמיקסינים מסוג CRTP משתמשים ב-static_cast<Derived*>(this) כדי לגשת לחברים של המחלקה היורשת בזמן הידור.

  • Entities.h: הגדירו שתי מחלקות ישויות שיורשות משני המיקסינים.

    צרו מחלקה בשם Player שיורשת גם מ-Countable<Player> וגם מ-Describable<Player>. עליה לאחסן שם ורמה, ולספק מתודה בשם getDescription() שמחזירה מחרוזת בתבנית: Player: [name] (Level [level])

    צרו מחלקה בשם Enemy שיורשת גם היא משני המיקסינים. עליה לאחסן סוג ונקודות בריאות, ולספק מתודה בשם getDescription() שמחזירה: Enemy: [type] with [health] HP

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

  • main.cpp: קראו ארבעה קלטים (כל אחד בשורה נפרדת):
    1. שם השחקן (מחרוזת)
    2. רמת השחקן (מספר שלם)
    3. סוג האויב (מחרוזת)
    4. נקודות הבריאות של האויב (מספר שלם)

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

    1. הדפיסו Player count: [count] באמצעות המתודה הסטטית getCount()
    2. הדפיסו Enemy count: [count]
    3. קראו ל-describe() עבור השחקן
    4. קראו ל-describe() עבור האויב
    5. צרו שחקן שני עם השם "Guest" ורמה 1
    6. הדפיסו שוב Player count: [count] כדי להציג את המספר המעודכן
    7. קראו ל-describe() עבור השחקן השני

לדוגמה, עם הקלטים Hero, 10, Dragon ו-500:

Player count: 1
Enemy count: 1
Player: Hero (Level 10)
Enemy: Dragon with 500 HP
Player count: 2
Player: Guest (Level 1)

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

נסו בעצמכם

#include <iostream>
#include <string>
#include "Entities.h"

using namespace std;

int main() {
    // קראו את הקלטים
    string playerName;
    int playerLevel;
    string enemyType;
    int enemyHealth;
    
    cin >> playerName;
    cin >> playerLevel;
    cin >> enemyType;
    cin >> enemyHealth;
    
    // TODO: צרו אובייקט Player עם השם והרמה הנתונים
    
    // TODO: צרו אובייקט Enemy עם הסוג והבריאות הנתונים
    
    // TODO: הדפיסו "Player count: [count]" באמצעות Player::getCount()
    
    // TODO: הדפיסו "Enemy count: [count]" באמצעות Enemy::getCount()
    
    // TODO: קראו ל־describe() על השחקן
    
    // TODO: קראו ל־describe() על האויב
    
    // TODO: צרו שחקן שני בשם "Guest" וברמה 1
    
    // TODO: הדפיסו שוב "Player count: [count]"
    
    // TODO: קראו ל־describe() על השחקן השני
    
    return 0;
}
quiz iconבחנו את עצמכם

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

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

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