Menu
Coddy logo textTech

תבנית הפקודה

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

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

Command עם מתודה execute(), פקודות קונקרטיות שמממשות פעולות מסוימות, מקבל שמבצע את העבודה בפועל, ומפעיל שמפעיל פקודות:

#include <iostream>
#include <memory>
#include <vector>

// המקבל - יודע כיצד לבצע פעולות
class Light {
public:
    void turnOn() { std::cout << "Light is ON\n"; }
    void turnOff() { std::cout << "Light is OFF\n"; }
};

// ממשק Command
class Command {
public:
    virtual void execute() = 0;
    virtual void undo() = 0;
    virtual ~Command() = default;
};

// פקודות קונקרטיות
class LightOnCommand : public Command {
    Light& light;
public:
    LightOnCommand(Light& l) : light(l) {}
    void execute() override { light.turnOn(); }
    void undo() override { light.turnOff(); }
};

class LightOffCommand : public Command {
    Light& light;
public:
    LightOffCommand(Light& l) : light(l) {}
    void execute() override { light.turnOff(); }
    void undo() override { light.turnOn(); }
};

// המפעיל
class RemoteControl {
    std::vector<std::unique_ptr<Command>> history;
public:
    void pressButton(std::unique_ptr<Command> cmd) {
        cmd->execute();
        history.push_back(std::move(cmd));
    }
    
    void pressUndo() {
        if (!history.empty()) {
            history.back()->undo();
            history.pop_back();
        }
    }
};

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

השתמשו ב-Command כשצריך להכניס פעולות לתור, לממש ביטול וביצוע מחדש, או לנתק בין שולח הבקשה למטפל בה.

challenge icon

אתגר

קל

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

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

  • TextDocument.h: צרו את מחלקת המקבל שמחזיקה את תוכן הטקסט עצמו ומבצעת בו שינויים.

    מחלקת TextDocument שלכם צריכה לשמור את תוכן המסמך כמחרוזת. ממשו את המתודות הבאות:

    • insertText(const std::string& text): מוסיפה טקסט לסוף התוכן
    • deleteText(int count): מסירה את count התווים האחרונים מהתוכן (לא עושה דבר אם count גדול מאורך התוכן)
    • getContent(): מחזירה את התוכן הנוכחי כייחוס קבוע
  • Command.h: הגדירו את ממשק הפקודה ואת מחלקות הפקודות המוחשיות.

    צרו מחלקת Command מופשטת עם מתודות וירטואליות טהורות execute() ו-undo(), וגם מפרק וירטואלי.

    לאחר מכן ממשו שתי פקודות מוחשיות:

    • InsertCommand: מקבלת ייחוס ל-TextDocument ומחרוזת להוספה. בעת ביצועה, היא מוסיפה את הטקסט. בעת ביטולה, היא מוחקת את אותו מספר תווים שהוכנסו.
    • DeleteCommand: מקבלת ייחוס ל-TextDocument ומספר תווים למחיקה. עליה לשמור את הטקסט שנמחק כדי שתוכל לשחזר אותו בעת ביטול הפעולה. בעת ביצועה, היא מסירה תווים מהסוף. בעת ביטולה, היא מוסיפה מחדש את הטקסט השמור.
  • TextEditor.h: צרו את המחלקה המזמנת שמנהלת את ביצוע הפקודות ואת ההיסטוריה.

    מחלקת TextEditor שלכם צריכה להחזיק ייחוס ל-TextDocument ולנהל היסטוריה של פקודות שבוצעו, באמצעות וקטור של unique_ptr<Command>. ממשו:

    • executeCommand(std::unique_ptr<Command> cmd): מבצעת את הפקודה ומוסיפה אותה להיסטוריה
    • undo(): מבטלת את הפקודה האחרונה ומסירה אותה מההיסטוריה (לא עושה דבר אם ההיסטוריה ריקה)
    • showContent(): מדפיסה את תוכן המסמך הנוכחי, או [empty] אם התוכן ריק
  • main.cpp: הדגימו את תבנית הפקודה עם אפשרות לבטל פעולות.

    קראו שלושה קלטים:

    1. הטקסט הראשון להוספה (מחרוזת)
    2. הטקסט השני להוספה (מחרוזת)
    3. מספר התווים למחיקה (מספר שלם)

    צרו TextDocument ו-TextEditor. לאחר מכן בצעו את הפעולות הבאות לפי הסדר, והציגו את התוכן לאחר כל שלב:

    1. הוסיפו את הטקסט הראשון, ולאחר מכן הציגו את התוכן
    2. הוסיפו את הטקסט השני, ולאחר מכן הציגו את התוכן
    3. מחקו את מספר התווים שצוין, ולאחר מכן הציגו את התוכן
    4. בטלו פעולה אחת, ולאחר מכן הציגו את התוכן
    5. בטלו פעולה נוספת, ולאחר מכן הציגו את התוכן

לדוגמה, עם הקלטים Hello, World ו-3:

Hello
Hello World
Hello Wo
Hello World
Hello

עם הקלטים Code, Editor ו-6:

Code
CodeEditor
Code
CodeEditor
Code

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

נסו בעצמכם

#include <iostream>
#include <string>
#include <memory>
#include "TextDocument.h"
#include "Command.h"
#include "TextEditor.h"

int main() {
    // קראו את הקלטים
    std::string firstText;
    std::string secondText;
    int deleteCount;
    
    std::getline(std::cin, firstText);
    std::getline(std::cin, secondText);
    std::cin >> deleteCount;

    // TODO: צרו TextDocument ו-TextEditor
    
    // TODO: בצעו את הפעולות לפי הסדר, והציגו את התוכן אחרי כל פעולה:
    // 1. הכניסו את הטקסט הראשון, ואז הציגו את התוכן
    // 2. הכניסו את הטקסט השני, ואז הציגו את התוכן
    // 3. מחקו את מספר התווים שצוין, ואז הציגו את התוכן
    // 4. בטלו את הפעולה האחרונה פעם אחת, ואז הציגו את התוכן
    // 5. בטלו פעולה נוספת, ואז הציגו את התוכן
    
    // רמז: השתמשו ב-std::make_unique<InsertCommand>(...) וב- 
    //       std::make_unique<DeleteCommand>(...) כדי ליצור פקודות

    return 0;
}
quiz iconבחנו את עצמכם

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

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

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