نمط الأمر
جزء من قسم Object Oriented Programming في رحلة C++ على Coddy — الدرس 96 من 104.
يقوم نمط الأمر (Command pattern) بتغليف الطلب ككائن، مما يسمح لك بتمثيل العملاء بطلبات مختلفة، أو جدولة العمليات، أو دعم خاصية التراجع. فهو يفصل الكائن الذي يستدعي العملية عن الكائن الذي يعرف كيفية تنفيذها.
يتكون النمط من أربعة مكونات رئيسية: واجهة Command تحتوي على طريقة execute()، و Concrete Commands التي تنفذ إجراءات محددة، و Receiver الذي يقوم بالعمل الفعلي، و Invoker الذي يقوم بتشغيل الأوامر:
#include <iostream>
#include <memory>
#include <vector>
// المستقبل (Receiver) - يعرف كيفية تنفيذ العمليات
class Light {
public:
void turnOn() { std::cout << "Light is ON\n"; }
void turnOff() { std::cout << "Light is OFF\n"; }
};
// واجهة الأمر (Command interface)
class Command {
public:
virtual void execute() = 0;
virtual void undo() = 0;
virtual ~Command() = default;
};
// الأوامر الملموسة (Concrete Commands)
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(); }
};
// المستدعي (Invoker)
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();
}
}
};يقوم كل أمر بتخزين مرجع للمستقبل (receiver) الخاص به ويستدعي الطريقة المناسبة عند تنفيذه. لا يعرف المستدعي (invoker) ما هو الإجراء الذي سيحدث - فهو يكتفي باستدعاء execute(). من خلال تخزين الأوامر في سجل التاريخ، يمكنك بسهولة تنفيذ التراجع عن طريق استدعاء طريقة undo() الخاصة بكل أمر.
استخدم نمط Command عندما تحتاج إلى وضع العمليات في طابور، أو تنفيذ التراجع/الإعادة، أو فصل مرسل الطلب عن معالجه.
التحدي
سهللنقم ببناء محرر نصوص (Text Editor) مع خاصية التراجع (undo) باستخدام نمط الأمر (Command pattern). ستقوم بإنشاء نظام يتم فيه تغليف عمليات التحرير (مثل إدراج وحذف النص) ككائنات أوامر، مما يسمح للمستخدمين بتنفيذ الإجراءات ثم التراجع عنها بترتيب عكسي — تماماً مثل محرر النصوص الحقيقي.
ستقوم بتنظيم الكود الخاص بك عبر أربعة ملفات:
TextDocument.h: قم بإنشاء فئة المستقبل (receiver) التي تحتفظ بمحتوى النص الفعلي وتتلاعب به.يجب أن تقوم فئة
TextDocumentبتخزين محتوى المستند كسلسلة نصية (string). قم بتنفيذ هذه الدوال:insertText(const std::string& text)— يضيف النص إلى نهاية المحتوى.deleteText(int count)— يزيل آخرcountمن الأحرف من المحتوى (لا تفعل شيئاً إذا تجاوز count طول المحتوى).getContent()— يعيد المحتوى الحالي كمرجع ثابت (const reference).
Command.h: قم بتعريف واجهة الأمر (command interface) وفئات الأوامر الملموسة (concrete command classes).قم بإنشاء فئة
Commandمجردة مع دوال افتراضية بحتة (pure virtual methods) وهيexecute()وundo()، بالإضافة إلى مدمر افتراضي (virtual destructor).ثم قم بتنفيذ أمرين ملموسين:
InsertCommand— يأخذ مرجعاً لـTextDocumentوسلسلة نصية لإدراجها. عند التنفيذ، يقوم بإدراج النص. وعند التراجع، يقوم بحذف نفس عدد الأحرف التي تم إدراجها.DeleteCommand— يأخذ مرجعاً لـTextDocumentوعدد الأحرف المراد حذفها. يحتاج إلى تخزين النص المحذوف حتى يتمكن من استعادته عند التراجع. عند التنفيذ، يقوم بإزالة الأحرف من النهاية. وعند التراجع، يقوم بإدراج النص المخزن مرة أخرى.
TextEditor.h: قم بإنشاء المستدعي (invoker) الذي يدير تنفيذ الأوامر والسجل (history).يجب أن تحمل فئة
TextEditorمرجعاً لـTextDocumentوتحتفظ بسجل للأوامر المنفذة باستخدام vector منunique_ptr<Command>. قم بتنفيذ:executeCommand(std::unique_ptr<Command> cmd)— ينفذ الأمر ويضيفه إلى السجل.undo()— يتراجع عن أحدث أمر ويزيله من السجل (لا تفعل شيئاً إذا كان السجل فارغاً).showContent()— يطبع محتوى المستند الحالي، أو[empty]إذا كان المحتوى فارغاً.
main.cpp: عرض نمط الأمر مع خاصية التراجع.اقرأ ثلاثة مدخلات:
- النص الأول المراد إدراجه (string)
- النص الثاني المراد إدراجه (string)
- عدد الأحرف المراد حذفها (integer)
قم بإنشاء
TextDocumentوTextEditor. ثم قم بإجراء هذه العمليات بالترتيب، مع عرض المحتوى بعد كل خطوة:- إدراج النص الأول، ثم عرض المحتوى.
- إدراج النص الثاني، ثم عرض المحتوى.
- حذف عدد الأحرف المحدد، ثم عرض المحتوى.
- التراجع مرة واحدة، ثم عرض المحتوى.
- التراجع مرة أخرى، ثم عرض المحتوى.
على سبيل المثال، مع المدخلات Hello و World و 3:
Hello
Hello World
Hello Wo
Hello World
Helloمع المدخلات Code و Editor و 6:
Code
CodeEditor
Code
CodeEditor
Codeلاحظ كيف يعرف كل أمر كيفية عكس نفسه. يتذكر DeleteCommand ما قام بحذفه حتى يتمكن من استعادته، ويعرف InsertCommand بالضبط عدد الأحرف التي يجب إزالتها عند التراجع. هذه هي قوة نمط الأمر — تصبح العمليات كائنات من الدرجة الأولى يمكن تخزينها وتنفيذها وعكسها.
ورقة مرجعية
يقوم نمط الأمر (Command pattern) بتغليف الطلب ككائن، مما يسمح لك بتمثيل العملاء بطلبات مختلفة، أو جدولة العمليات، أو دعم وظيفة التراجع (undo). فهو يفصل الكائن الذي يستدعي العملية عن الكائن الذي يعرف كيفية تنفيذها.
يتكون النمط من أربعة مكونات رئيسية:
- واجهة الأمر (Command interface) مع طريقة
execute() - الأوامر الملموسة (Concrete Commands) التي تنفذ إجراءات محددة
- المستقبل (Receiver) الذي يقوم بالعمل الفعلي
- المستدعي (Invoker) الذي يطلق الأوامر
الهيكل الأساسي:
// Receiver - يعرف كيفية تنفيذ العمليات
class Light {
public:
void turnOn() { std::cout << "Light is ON\n"; }
void turnOff() { std::cout << "Light is OFF\n"; }
};
// Command interface - واجهة الأمر
class Command {
public:
virtual void execute() = 0;
virtual void undo() = 0;
virtual ~Command() = default;
};
// Concrete Command - أمر ملموس
class LightOnCommand : public Command {
Light& light;
public:
LightOnCommand(Light& l) : light(l) {}
void execute() override { light.turnOn(); }
void undo() override { light.turnOff(); }
};
// Invoker - المستدعي
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(). من خلال تخزين الأوامر في سجل (history)، يمكنك تنفيذ خاصية التراجع عن طريق استدعاء طريقة undo() لكل أمر.
استخدم نمط الأمر عندما تحتاج إلى جدولة العمليات، أو تنفيذ التراجع/الإعادة (undo/redo)، أو فصل مرسل الطلب عن معالجه.
جرّب بنفسك
#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;
}
يتضمن هذا الدرس اختبارًا قصيرًا. ابدأ الدرس للإجابة عليه وتتبّع تقدمك.
جميع دروس Object Oriented Programming
1أساسيات الـ OOP
الملفات الخارجيةبناء وتجميع (Compilation) لغة C++ملفات الـ Header وملفات الـ Sourceالـ Namespaces والـ Scopeمقدمة في الـ OOP في C++الـ Classes مقابل الـ Objectsالمؤشر 'this'الـ Methods (الدوال الأعضاء)الـ Attributes (بيانات الأعضاء)أساسيات الـ Ctors والـ Dtorsمراجعة - آلة حاسبة بسيطة4خصائص الـ Class
أعضاء الـ Instance مقابل الـ Staticدوال الـ Getters والـ Settersدوال الأعضاء الثابتة (Const)الكلمة المفتاحية Mutableالدوال والمتغيرات الساكنة (Static)الدوال والفئات الصديقة (Friend)مراجعة - مدير الحساب البنكي7الوراثة
الوراثة الأساسيةمستويات الوصول في الوراثةترتيب استدعاء الـ Ctor والـ Dtorإعادة تعريف الدوال (Method Overriding)الدوال الافتراضية والـ VTableالوراثة المتعددةالوراثة الافتراضيةمراجعة - هيكلية الموظفين10نظرة عامة على STL
نظرة عامة وفلسفة STLحاويات STLالـ Iteratorsخوارزميات STLالـ Functors وتعبيرات Lambdaمراجعة - تكرار الكلمات13أنماط التصميم - الجزء الأول
مقدمة في أنماط التصميمنمط Singletonنمط Factory و Abstract Factoryنمط Builderنمط Observerنمط Strategy2إدارة الذاكرة
ذاكرة Stack مقابل Heapالمؤشرات والمراجعالذاكرة الديناميكية (new/delete)المؤشرات الذكية في C++RAII في C++مراجعة - مدير المصفوفات الديناميكية5التغليف (Encapsulation)
محددات الوصول في C++محددات الوصول بشكل متعمقإخفاء المعلوماتStruct مقابل Classالأصناف المتداخلة والداخليةمراجعة - نظام سجلات الطلاب8تعدد الأشكال (Polymorphism)
تعدد الأشكال: وقت التجميع مقابل وقت التشغيلالتحميل الزائد للدوال (Function Overloading)مراجعة الدوال الافتراضية (Virtual Functions)الدوال الافتراضية البحتة (Pure Virtual Functions)الأصناف المجردة (Abstract Classes)تصميم الواجهات (Interface) في C++التحويل الديناميكي (Dynamic Casting) و RTTIملخص - حاسبة الأشكال11مفاهيم OOP المتقدمة
التركيب مقابل الوراثةالـ Mixins عبر CRTPنمط Pimplمحو النوع (Type Erasure)أصناف Enum والأنواع القويةمعالجة الاستثناءات في OOPتسلسلات الاستثناءات المخصصة14أنماط التصميم - الجزء الثاني
نمط الأمرنمط المحولنمط المزيننمط قالب الطريقةنمط الحالةنمط التركيبRAII كنمط3المنشئات والموادم
المنشئ الافتراضيالمنشئ ذو المعاملاتمنشئ النسخمنشئ النقلقوائم تهيئة المنشئالمنشئات المفوضةتعمق في الموادمقاعدة الثلاثة / الخمسة / الصفرمراجعة - فئة String6تحميل العوامل (Operator Overloading)
مقدمة في تحميل العواملتحميل العوامل الحسابيةتحميل عوامل المقارنةعوامل Streamتحميل عامل التعيينتحميل العوامل [] و ()عوامل تحويل النوعمراجعة - Matrix Class9القوالب
قوالب الدوالقوالب الأصنافتخصيص القوالبالقوالب المتغيرةأساسيات SFINAE و Type Traitsمراجعة - الحاويات العامة