بسم الله الرحمن الرحيم
للـ Command Pattern تطبيقات عديده و فى هذا الدرس سنتكلم بشكل خاص عن كيفية عمل Framework صغيره لإضافة دعم Multi-Level Undo and Redo لبرامجنا.
في هذا الدرس:
- لمن هذا الدرس
- ما هو الـ Command Pattern
- كيف يعمل الـ Command Pattern
- ما هو الـ Undo و الـ Redo
- داخل مكتبتنا
- مشاكل متوقعه و حلولها
- الفئه CommandBase
- الفئه CommandManager
- مشاكل متوقعه و حلولها
- مثال تطبيقى الأله الحاسبه
- الفئه Calculator
- فئات الـ Command
- الفئه AddCommand
- الفئه SubtractCommand
- الفئه MultiplyCommand
- الفئه DivideCommand
- الفئه SaveCommand
- الفئه RestoreCommand
- الفئه AddCommand
- الفئه Calculator مره أخرى
- خاتمه
لمن هذا الدرس
- إذا كنت تريد ان تعرف كيف يتم برمجة الـ Transaction فهذا الدرس هو لك.
- إذا كنت تريد ان تعرف كيف يتم برمجة نظام Undo-Redo عام لبرامجك فهذا الدرس هو لك.
- إذا كنت تريد ان تعرف كيف يتم برمجة انظمة Marco (مثل الموجوده فى الفوتوشوب او الموجوده داخل الفيجوال ستديو) فهذا الدرس هو لك.
- إذا كان لديك فضول لمعرفة عن ماذا يتكلم هذا الدرس فهو لك.
قد يعتقد البعض ممن سيقرأ هذا الموضوع أني على وشك شرح كل هذه الأنواع، للأسف سأخيب ظنك حيث سأركز جهودى كلها على نظام Undo-Redo و ذلك لأنه هو الشائع، لهذا بعد قرائتك لهذا الدرس ستكون لديك فكره قويه عن الكيفيه التى تبرمج بها الأنواع السابق ذكرها و حيث أن اسلوب برمجتهم متماثل و الإختلافات بينهم طفيفه لذا فبمعرفتك احداهم تكون قد قطعت شوطا طويلا من طريق دراسة الـ Command Pattern.
ما هو الـ Command Pattern
الـ Command Pattern هى احد اعضاء عائلة الـ Design Pattern و هى احد الأساليب المتبعه فى حل المشاكل التى تواجه المبرمجين و التى تتطلب وجود ما يشبه الذاكره لتحتوى على كل الخطوات التى تم تنفيذها و التى قامت بتغيير حالة البرنامج إلى أخرى. الهدف من وجود مثل هذه الذاكره هو امكانية التراجع عن احد الخطوات التى تم تنفيذها بالبرنامج، عملية التراجع لها اكثر من مسمى منهم Rollback او Undo.
كيف يعمل الـ Command Pattern
فالنفترض وجود منضده عليها مبلغ معين من النقود و هو 70 جنيه هذا الملبغ موجود على المنضده كـ 5 ورقات من فئة 10 و 5 ورقات من فئة 2 و 10 ورقات من فئة جنيه، سأقوم الأن بتنفيذ أمر لأخذ 10 جنيهات ثم تنفيذ أمر أخر بأخذ 5 جنيهات ثم تنفيذ أمر أخذ 1 جنيه ثم التراجع عن اخر أمر تم تنفيذه و هذا يعنى اعادة 1 جنيه أيضا سأقوم بالتراجع عن اخر أمر قمت به للأن و هو إعادة 5 جنيهات، لذا كما ترى فى النهايه كل ما سيتبقى معى هو 10 جنيهات.
الـ Command Pattern قائم على ما يسمى أمر، هذا الأمر يمكن تمثيله من المثال السابق بشخص، هذا الشخص لديه القدره على إجراء فعل ما و ايضا لديه القدره على التراجع عن هذا الفعل، من المثال السابق قام الشخص بتنفيذ أمر و هو Do Take Cash و ايضا قام بالتراجع عنه UnDo Take Cash.
لتطبيق الـ Command Pattern لابد من توافر بعض العوامل و هى:
- العنصر الرئيسي الذى يحتوى على القيم الذى ستقوم بتغييرها و فى حالتنا هذه المنضده هى العنصر الرئيسى و القيمه التى سنغيرها بها هى كمية النقود بها.
- ان يسمح لك العنصر الرئيسى بإجراء عمليات عليه من شأنها تغيير حالته و ايضا توفير القدره عن التراجع عن هذه العمليات فمثلا فى حالتنا هذه العنصر الرئيسى (المنضده) تسمح لك بأخذ النقود من عليها و ايضا تسمح لك بإرجاعها و لكن لنفترض ان هذه النقود موجوده بداخل خزانه مغلقه جيدا و مثبته على المنضده لذا فى هذه الحاله فأنت لن تستطيع فتح الخزانه و لن تستطيع اخذ النقود.
- لديك الأن المنضده (العنصر الرئيسى) التى تحتوى على النقود (عناصرها) و ايضا المنضده تسمح لك بتغيير محتواياتها لذا ما ينقصك فى هذه الحاله هو الشخص (الأمر) الذى سيقوم بتنفيذ العمليات على النقود الموجوده بالمنضده.
الشكل التالى يوضح لك العلاقه بين العناصر المكونه لنظام الـ Command Pattern
ما هو الـ Undo و الـ Redo
لنقوم بتطبيق مثال المنضده برمجيا على ابسط شكل ممكن لتقريب الفكره.
ذكرت من قبل أن المنضده هى العنصر الرئيسى الذى سيحتوى على نقود نريد تغيير حالتها و ايضا قلنا ان المنضده لابد و أن توفر لنا القدره على اجراء العمليات على النقود (أخذ النقود) و ايضا القدره على التراجع عن هذه العملية (إرجاع النقود) لذا و بناءا على المعطيات التى لدينا سنقوم بتصميم الفئه Table و التى تمثل منضده، هذه الفئه ستحتوى على متغير رقمى يمثل مبلغ النقود الموجود على المنضده، ايضا سنقوم بصنع دالتين هما takeCash و addCash و اللذان يقوما على التوالى بأخذ النقود من المنضده و اضافة النقود إلى المنضده.
هذا هو شكل الفئه:
class Table
{
private:
int _cash;
public:
Table() : _cash(0) {}
void takeCash(int howMuch)
{
if (howMuch > _cash) return;
_cash -= howMuch;
}
void addCash(int howMuch)
{ _cash += howMuch; }
};بوجود الفئه Table و دالتيها takeCash و addCash نكون قد حققنا اول شرطين لتطبيق الـ Command Pattern داخل برنامجنا و ينقصنا الأمر الذى سيقوم بتنفيذ أمر أخذ النقود و ارجاعها، و لكن لنتمهل قليلا قبل القفز و كتابة الكود فنحن حتى الأن لا نعرف ماهية هذا الأمر و ما الذى يجب عليه أن يفعل، لذا لنحلل قليلا.
نحن نعرف أن الأمر سيتعامل مع منضده معينه لذا لابد و ان يوجد مرجع بداخل الأمر لهذه المنضده، أيضا الهدف من هذا الأمر هو تعديل حالة النقود الموجوده بالمنضده لذا لابد أن يحتوى الأمر على قيمة النقود التى أخذها من المنضده أو قيمة النقود التى أرجعها للمنضده و ذلك حتى يستطيع الأمر فيما بعد بإرجاع نفس المبلغ الذى أخذه أو أخذ نفس المبلغ الذى قام بإرجاعه، هذه هى صفات الأمر و لكن توجد صفه أخرى به سنذكرها لاحقا.
لكل منا طبيعه إما فطره الله عليها او هى طبيعه مكتسبه فمثلا عندما ترى شخص نحيف فالفكره الإفتراضيه التى ستجوب بذهنك هى أن هذا الشخص لا يأكل و بالتالى الفعل الإفتراضى له بناءا على هذه المعلومه هو عدم الأكل و التراجع عن هذا الفعل هو أن يأكل، و بالمثل إذا رأيت شخص بدين ستقول ان هذا الشخص يأكل كثيرا و بالتالى الفعل الإفتراضى له هو الأكل و التراجع عن هذا الفعل هو بالتوقف عن الأكل. لذا فى المجمل نستطيع ان نقول ان فعل الأكل و فعل عدم الأكل يتناسبا مع طبيعة الشخص نفسه و التى تستطيع معرفتها بمجرد النظر للشخص.
المنضده تحتوى على فعلين هم أخذ النقود و اضافة النقود لذا الأمر الذى سنقوم بتصميم فئه له لابد و ان يكون له فعل افتراضى و حينها سيكون الأخر هو التراجع بمعنى إذا قمنا بعمل فئه تمثل أخذ النقود سيكون الفعل الإفتراضى لها هو takeCash و التراجع عن هذا الفعل يكون بـ addCash لنفس المبلغ الذى تم أخذه بإستخدام takeCash. أيضا إذا قمنا بعمل فئه تمثل اضافة النقود حينها سيكون الفعل الإفتراضى لها هو addCash و التراجع عن هذا الفعل يكون بـ takeCash لنفس المبلغ الذى تم إضافته بإستخدام addCash. قد تتسأل و كيف سأعرف الفعل الإفتراضى للفئه؟ بالطبع مثلما كنت ستعرف الفعل الإفتراضى للشخص النحيف و البدين، فأنت عندما نظرت للشخص رأيت أحد صفاته و هى النحافه على سبيل المثال و منها عرفت أن الفعل الإفتراضى له هو عدم الأكل بالمثل إسم الفئه سيعطيك أحد صفاتها، هذه الصفه هى الفعل الإفتراضى لها، فمثلا إذا قمنا بصنع الفئه TakeCashCommand ستعرف من اسمها انها تقوم بأخذ مبلغ من النقود و هذا صحيح و لأن هذا هو إسمها سيكون أيضا هو الفعل الإفتراضى لها.
أعتقد أن صفات الأمر أصبحت واضحه لنا و من السابق عرفنا أننا سنحتاج لفئتين و سنطلق عليهم TakeCashCommand و AddCashCommand (أعتقد انك الأن تعرف ما هو الفعل الإفتراضى الذى ستقوم به كل من الفئتين):
class TakeCashCommand
{
private:
Table* _tbl;
int _cash;
public:
TakeCashCommand( Table* pTable, int cash )
: _tbl(pTable), _cash(cash) {}
void Redo() { _tbl->takeCash( _cash ); }
void Undo() { _tbl->addCash( _cash ); }
};
class AddCashCommand
{
private:
Table* _tbl;
int _cash;
public:
AddCashCommand( Table* pTable, int cash )
: _tbl(pTable), _cash(cash) {}
void Redo() { _tbl->addCash( _cash ); }
void Undo() { _tbl->takeCash( _cash ); }
};قبل أن استرسل فى الحديث سأقوم بتلخيص كل ما سبق على شكل سؤال و إجابه بشكل مختصر.
1 - ما الداعى من وجود فئات طالما انه توجد دوال توفرها لنا الفئه Table نستطيع استدعائها بشكل مباشر؟
فالنفترض انك تطلب من المستخدم أدخال المبلغ الذى يريد ان يأخذه من على المنضده، و انت لا تعلم عدد المرات التى سيقوم المستخدم بأخذ هذا المبلغ، لنفترض الأن أن المستخدم طلب سحب المبلغ 2000 جنيه من على المنضده و قام بسحب هذا المبلغ على 30 مره بقيم مختلفه. الأن بعد ان قام المستخدم بسحب المبلغ 2000 جنيه بأكمله قال لك انه يريد التراجع عن اخر 10 عمليات سحب للنقود، فما المبلغ الذى ستقوم بأخذه منه ؟
فى الواقع هذه هى أحد أنواع المشاكل التى أدت لظهور اسلوب الـ Command Pattern و هو وجود كائن يحتفظ بالمعلومات اللازمه لتنفيذ أمر ما و الكائن نفسه يستطيع التراجع عن هذا الأمر، فمثلا فى حالة هذا المستخدم ستقوم بإنشاء عدة كائنات من الفئه TakeCashCommand تساوى نفس عدد المرات التى سحب فيها المستخدم النقود (30 مره) و كل نسخه من TakeCashCommand تعرف المنضده التى سحبت منها النقود بالإضافه إلى المبلغ الذى تم سحبه من على المنضده لذا عندما يطلب منك المستخدم ان تتراجع عن اخر 10 عمليات سحب للنقود كل ما ستفعله هو استدعاء الداله Undo لأخر 10 كائنات موجود لديك و فى النهايه المبلغ الموجود على المنضده سيكون تماما هو المبلغ الذى كان موجود بعد 20 عملية سحب.
2 - لماذا قمنا بصناعة فئتين لتقوما بأمر واحد، ألم يكن يكفى أن نصنع واحده فقط و هى ستستطيع القيام بكلاهما؟
سأعطيك مثال و هو ما سيجيب عليك، المنضده التى عليها النقود الأن فارغه و لا تحتوى على اى نقود، لذا أنت تريد تنفيذ أمر اضافة نقود حتى تستطيع فيما بعد تنفيذ ام أخذ النقود التى قمت بوضعها على المنضده و كما نعلم أن الفئه TakeCashCommand الفعل الإفتراضى لها هو أخذ النقود و بالتالي لن يكون هناك اى معنى لإنشاء كائن من فئه وظيفتها الأساسيه سحب نقود لنستخدمها لإضافة النقود، هذا الفعل يماثل تماما استخدامك لعلامة + لإحضار الجذر التكعيبى لرقم، كلاهما تصرف غير سليم. ما نحتاج إليه هو فئه فعلها الإفتراضى هو إضافة نقود و من هنا كانت الفئه AddCashCommand
3 - كلا الفئتين تحتوى على متغير يمثل النقود و لا يمكن تغيير قيمة هذه النقود بمجرد انشاء كائن من الفئه فلماذا هذا ؟ أيضا ما العمل إذا اردت اخذ مبلغ أخر؟
ذكرت من قبل أن هدف الـ Command Pattern هو الإحتفاظ بنسخه من القيم التى قامت بتغيير حالة البرنامج و ذلك لإتاحة الفرصه لحذف هذه القيم فيما بعد، و لهذا السبب تحديدا الـ Command الواحد لا تتغير قيمته التى يتم انشائه بها أبدا، لذا إن اردت اخذ مبلغ اخر لابد منك من انشاء نسخة جديده من الفئه TakeCashCommand بالمبلغ الذى تريد، قد تقول أن هذا سيسبب تضخم حجم الذاكره بشكل كبير و هذا صحيح لذا تجد اغلب البرامج تطلب من المستخدم تحديد حجم ذاكرة الـ Undo و ذلك حتى لا يتم تخطيها.
داخل مكتبتنا
قطعنا شوطا لا بأس به فى الحديث عن الـ Command Pattern بشكل عام و نظام Undo-Redo بشكل خاص، أيضا عرفنا بعض المشاكل البسيطه و كيفية حلولها.
نظام الـ Undo Provider ثابت لا يتغير فأفكاره واحده و اسلوب تطبيقها واحد، و لكن برامجنا هى المتغيره لذا إما أن نقوم بتطبيق هذا النظام داخل برنامج ثم نسخه داخل أخر أو نفعل ما هو أفضل من ذلك و هو بناء محرك بسيط يكون هو المسئول عن إدارة انشاء و حذف و تنفيذ هذه الـ Command و أيضا نضع داخله امكانية إجراء تعديلات على نظام عمله و ذلك لتتناسب مع أى برنامج نستخدمه داخله و هذا ما سنقوم بفعله تماما.
مشاكل متوقعه و حلولها
لنبدأ فى تحليل متطلبات المحرك الذى سنقوم ببرمجته مع الأخذ فى الإعتبار أن هذا المحرك يمكن استخدامه فى أكثر من مشروع و اننا نريد جعل التغييرات التى سنحدثها بالمحرك عند استخدامه داخل احد المشاريع أقل ما يمكن. بهذه المعلومات الأمور التى سنحتاج أخذها فى الحسبان هنا تتلخص فى الأتى:
- إدارة الذاكره.
- إدارة الـ Commands
- اختلاف الـ Commands
و فيما يلى شرح ملخص لكل نقطه مما سبق
إدارة الذاكره
عملية حجز و تحرير الذاكره تستهلك وقت يسير و لكن فى بعض أنواع البرامج هذا الوقت اليسير قد يسبب عيوب في البرنامج (مثل الألعاب مثلا) لذا يتم استخدام Garbage Collection لتدير عملية حجز و تحرير الذاكره و حيث أن جمبع الـ Command لابد و أن يتم إنشائها داخل الـ Heap لذا لابد من إعادة تعريف new و delete للمحرك إذا كان برنامجك يستخدم GC معين أو يبقى الوضع كما هو عليه إذا كنت لا تستخدم GC.
إدارة الـ Commands
عملية التراجع عن تنفيذ الأوامر لابد و أن تتم عكس ترتيب انشائهم بمعنى ان اخر Command يتم تنفيذه هو اول Command يتم استدعاء Undo له، فمثلا إذا اردنا ان نقوم بالعمليه الحسابيه التاليه سنقوم بالتالى:
7 -> AddCommand(1) 8 -> SubtractCommand(4) 4 -> SubtractCommand(5) -1 -> AddCommand(3) 2 -> AddCommand(2) 4
و إذا اردنا التراجع عن اخر 4 عمليات سنقوم بالتالى:
4 -> AddCommand(2).Undo 2 -> AddCommand(3).Undo -1 -> SubtractCommand(5).Undo 4 -> SubtractCommand(4).Undo 8
كل Command يتم تنفيذه يتم إخراجه من السلسه و ذلك حتى لا يتم تنفيذه مره اخرى. هذا الإسلوب يسمى Last In First Out و أختصارا LIFO و كما نعلم يوجد Data Structure يماثل تنفيذه LIFO و هو الـ Stack لذا و لنجعل الأمور أسهل سنقوم بإنشاء Two Stacks احداهما يحتوى على الـ Commands التى تم تنفيذ الأمر الإفتراضى لها و الأخرى سيتم وضع بها الـ Commands الذى تم تنفيذ الأمر Undo لها.
قد تتسأل و لماذا لم نجعلهم Stack واحده فقط؟ السبب فى ذلك يرجع إلى اننا نريد التفرقه بين الـ Commands التى سيتم استدعاء Undo لهم و الأخرين الذين سيتم تنفيذ Redo لهم.
فى المعادله السابقه قمنا بتنفيذ 5 عمليات و بالتالى الـ Undo Stack يحتوى على 5 اوامر، و بعدها قمنا بالتراجع عن 4 عمليات لذا الـ Redo Stack تحتوى على 4 اوامر و الـUndo Stack أصبحت تحتوى على أمر واحد فقط و القيمه الحاليه للمعادله هى 8، لذا السؤال الذى يتبادر إلى الذهن ماذا يحدث للـ Stacks إذا قمنا بتنفيذ أمر اضافة 1 إلى الرقم 8؟
الإجابه هى سنقوم بتفريغ الـ Redo Stack و ذلك لأن المستخدم إذا كان يريد إعادة تنفيذ امر من الذين قد قام بهم لفعل ذلك و لكنه فضل الغاء ما قام به و تنفيذ أمر جديد، لذا اصبح القديم الموجود داخل Redo Stack لا فائده منه و لهذا تم حذفه.
اختلاف الـ Commands
كما قلنا من قبل سنستخدم الـ Stack لحفظ الـ Commands التى لدينا داخلها و لا يوجد لدينا افضل من الفئه stack الموجوده داخل STL و حيث ان هذه الفئه هى template فلابد و ان نمرر لها النوع الذى ستحتفظ به و هنا تظهر لنا مشكله و هى ما النوع الذى سنحدده لهذه الفئه، إن كل الـ Commands التى لدينا لا تشترك سوى فى ان كل منهم يحتوي على الدالتين Undo و Redo لذا ما الحل؟
أبسط حل لدينا هو ان نقوم بعمل فئه تكون هى الفئه الأساس لكل فئات الـ Command الأخرى و نضع بها الدالتين Undo و Redo و نأمر كل فئه تقوم بوراثتها بأن تقوم بتضمين هاتين الدالتين
و بهذا نستطيع أن نضمن ان كل الفئات لدينا ذات نوع واحد مشترك يحتوى على الدوال التى نحتاجها و ايضا انه يمكن عمل stack بهذا النوع و يتم حفظهم جميعا داخله.
الفئه CommandBase
هذه هى الفئه الرئيسيه لكل فئات الـ Commands و التى ستقوم بوراثتها داخل برامجنا، الفئه شكلها كالتالى: (Engine/CommandBase/CommandBase.h)
class CommandBase
{
public:
CommandBase(void* value, unsigned size);
virtual ~CommandBase();
// redo command
void Redo();
// undo command
void Undo();
// callback to execute after each redo
void setRedoCallback(CommandCallback callback);
// is redo callback exist?
bool IsRedoCallbackPresent();
// callback to execute after each undo
void setUndoCallback(CommandCallback callback);
// is undo callback exist?
bool IsUndoCallbackPresent();
protected:
virtual void RedoInternal() = 0;
virtual void UndoInternal() = 0;
protected:
void* _value;
unsigned _size;
private:
CommandCallback _redoCallback;
CommandCallback _undoCallback;
};ذكرت سابقا ان الفئه ستحتوى على قيمة واحده فقط دائما، هذه القيمه انا لا اعرف نوعها و لا قيمتها و هذه هى مهمة المشيد الخاص بالفئه حيث يطلب منك عنوان يحتوى على القيمه التى ستحتوى عليها الفئه و ايضا المساحه الخاصه بالقيمه التى تم تمريرها بالمعامل الأول، الكود الخاص بالمشيد كالتالى: (Engine/CommandBase/CommandBase.cc)
CommandBase::CommandBase(void* value, unsigned size)
: _size(size), _value(0), _redoCallback(0), _undoCallback(0)
{
if (_size != 0)
{
_value = new char[size];
memcpy(_value, value, size);
}
}فى البدايه قمنا بجعل متغيرات الفئه بصفر بعدها قمنا بالتحقق من انه إذا كان حجم القيمه لا يساوى الصفر فإنه يتم حجز مساحه بهذا الحجم بعد ذلك يتم نسخ القيمه داخل المتغير _value.
المهدم مهمته صغيره و هى تحرير الذاكره المحجوزه للمتغير _value إن وجدت
CommandBase::~CommandBase()
{ if (_value != 0) delete []_value; }احيانا تحتاج بعد تنفيذ عملية Undo او Redo ان تعقب بتنفيذ كود معين لذا قمت بعمل الدالتين setRedoCallback و setUndoCallback و اللتان تطلبا منك داله لها البصمه التاليه:
void func_name(const CommandBase& lpCmd)
هذه الداله سيتم استدعائها دائما بعد تنفيذ عملية الـ Undo او عملية الـ Redo و المعامل الذى سيتم تمريره لها هو الـ Command الذى تم تنفيذه، أيضا توجد الدالتان IsRedoCallbackPresent و IsUndoCallbackPresent و اللتان ترجعان بـ true إذا كان يوجد داله سيتم استدعائها لأى منهم و false إذا لم يكن هناك دوال ليتم استدعائها. كود تلك الدوال كالتالى:
void CommandBase::setRedoCallback(CommandCallback callback)
{ _redoCallback = callback; }
bool CommandBase::IsRedoCallbackPresent()
{ return _redoCallback != 0; }
void CommandBase::setUndoCallback(CommandCallback callback)
{ _undoCallback = callback; }
bool CommandBase::IsUndoCallbackPresent()
{ return _undoCallback != 0; }نأتى الأن لدوال Undo و Redo، يلاحظ انه توجد نسختين لكل منهما احدهما بالإسماء السابقه و اخرى يليها Internal و السبب فى وجود نسختين هو اني اريد بعد كل عملية Undo او Redo ان استدعي الـ Callabck فإين سأكتب الكود الخاص بهذه الإستدعاءات، لهذا قمت بعمل الدالتين Redo و Undo و اللتان تستدعيان الدالتين RedoInternal و UndoInternal على التوالي، يلي هذا الإستدعاء استدعاء الـ Callback لكل منهما إذا كان موجود، كود الدالتين هو التالي:
void CommandBase::Redo()
{
RedoInternal();
if (_redoCallback != 0)
_redoCallback( *this );
}
void CommandBase::Undo()
{
UndoInternal();
if (_undoCallback != 0)
_undoCallback( *this );
}الفئه CommandManager
هذه هى الفئه التى تقوم بإدارة الأوامر من حفظ و استرجاع و تحرير و تنفيذ و شكلها كالتالى: (Engine/Manager/CommandManager.h)
class CommandManager
{
public:
CommandManager();
~CommandManager();
// add a command
bool AddCommand(CommandBase *pcmd);
// redo last command
void Redo() { Redo(1); }
// redo last specified number of commands
void Redo(unsigned count);
// can user Redo?
bool getCanRedo();
// undo last command
void Undo() { Undo(1); }
// undo last specified number of commands
void Undo(unsigned count);
// can user Undo?
bool getCanUndo();
// empty internal commands
void Clear();
private:
// clear redo stack
void ClearRedoStack();
// clear undo stack
void ClearUndoStack();
private:
stack<CommandBase*> _undo;
stack<CommandBase*> _redo;
bool _actionRunning;
};سنبدأ بالدالتين ClearRedoStack و ClearUndoStack و اللتان تقوما بتفريغ الـ stack لكل من _redo و _undo على التوالى: (Engine/Manager/CommandManager.cc)
void CommandManager::ClearRedoStack()
{
while (_redo.size())
{
CommandBase* cmd = _redo.top();
_redo.pop();
delete cmd;
}
}
void CommandManager::ClearUndoStack()
{
while (_undo.size())
{
CommandBase* cmd = _undo.top();
_undo.pop();
delete cmd;
}
}كما ترى يتم اخراج الـ Command من الـ Stack يليه استدعاء الداله delete لتحرير الذاكره الخاصه به.
ايضا يوجد الدالتين getCanRedo و getCanUndo و اللتان يتم استخدامهم للتحقق من امكانية استخدام خاصية redo و undo:
bool CommandManager::getCanRedo()
{ return _redo.size() > 0; }
bool CommandManager::getCanUndo()
{ return _undo.size() > 0; }الداله Clear يتم استخدامها لتفريغ الـ stack الخاص بالـ redo و الـ undo.
void CommandManager::Clear()
{
ClearRedoStack();
ClearUndoStack();
}الداله AddCommand يتم استخدامها لإدخال Command جديد داخل الـ Undo Stack و كما ذكرنا سابقا بعد كل عملية اضافة Command جديد داخل الـ Undo Stack لابد من تفريغ الـ Redo Stack لإن ما تحتوى عليه من Commands أصبح غير صالح، الداله تعود بالقيمه true إذا تم إضافة الـ Command بنجاح، أما إذا عادت بـ false فهذا يعنى ان الـ Command الذى يطلب إضافته تم إنشائه كرد فعل لتنفيذ الداله Undo أو الداله Redo و هذا النوع من الأوامر يمنع إضافته (سيتم شرح هذه النقطه لاحقا):
bool CommandManager::AddCommand(CommandBase *pcmd)
{
if (_actionRunning) return false;
_undo.push(pcmd);
ClearRedoStack();
return true;
}ما بقى لنا الأن هو الدالتين Undo و Redo:
void CommandManager::Redo(unsigned count)
{
if (_actionRunning) return;
for (unsigned i = 0; i < count; i++)
if (getCanRedo())
{
_actionRunning = true;
CommandBase* pcmd = _redo.top();
_redo.pop();
pcmd->Redo();
_undo.push(pcmd);
_actionRunning = false;
}
}
void CommandManager::Undo(unsigned count)
{
if (_actionRunning) return;
for (unsigned i = 0; i < count; i++)
if (getCanUndo())
{
_actionRunning = true;
CommandBase* pcmd = _undo.top();
_undo.pop();
pcmd->Undo();
_redo.push(pcmd);
_actionRunning = false;
}
}كود الدالتين متشابه بقدر كبير لذا سأشرح الفكره بوجه لأحدهم و تستطيع تطبيقها على الأخرى، المستخدم يريد اجراء عملية Undo لعدد معين من الـ Command لذا يتم اجراء حلقة تكرار من صفر و صولا لهذا العدد (و صولا للعدد مطروحا منه 1 لأن الحلقه بدءت من صفر) و فى كل مره يتم التحقق من وجود Commands فإن كان موجود يتم اخراجه من الـ stack ألخاصه به يليها تنفيذ الداله Undo له يليها ادخال الـ Command فى الـ Stack الأخرى.
لنعود مره أخرى للداله AddCommand:
عندما تقوم بإستدعاء أى من الدالتين Redo أو Undo يتم تنفيذ كود خاص بك موجود بالدالتين RedoInternal أو UndoInternal هذا الكود يوجد أحتمال أنه عندما يتم تنفيذه أن يقوم بإنشاء Command و يتم إضافته للـ Manager، المشكله هنا أنك تطلب التراجع (أو إعادة تنفيذ) أمر فإذا حدث أن تم إضافة الأمر الجديد للـ Undo Stack (أو الـ Redo Stack) فسيكون الـ Command الموجود على قمتها ليس صحيح و السبب فى ذلك أن جميع الـ Command الموجوده بكلتا الـ Stacks وجدت بناءا على أمر مباشر من المستخدم و حيث ان هذا الأمر لم يقم المستخدم بإنشائه و انما تم إنشائه كرد فعل لتنفيذ المحرك للدالتين Undo أو Redo لذا هذا الأمر يندرج تحت بند Automatic Command و ليس User Command و حيث أن الـ Stacks تحتوى فقط على الـ User Commands لذا هذا الـ Command هو غير صحيح لذا لن يتم اضافته و سيتم الرجوع بـ false و حينها انت ستعلم ان هذا الـ Command غير صحيح فتقوم بتحرير الذاكره التى تم حجزها له.
بهذا نكون قد انتهينا من تصميم المحرك و الصوره التاليه تحتوى على الـ Class Diagram له:
مثال تطبيقى الأله الحاسبه
فى هذا المثال سنقوم بتصميم أله حاسبه بسيطه تقوم بإدخال لها الأرقام و العمليات خطوه بخطوه و بعدها تسمح لك بالتراجع عن الخطوات التى قمت بها، بالطبع ستجد بعض القصور مما تقوم به الأله و ذلك لأنه ليس الهدف هو صناعة أله حاسبه و انما تطبيق مفهوم الـ Undo-Redo بشكل عملي على مثال بسيط لا يجعلك تفكر بتصميمه اكثر من موضوع الدرس.
الفئه Calculator
الفئه Calculator تمثل الأله الحاسبه و تقوم ببعض العمليات الأساسيه و هى الجمع و الطرح و الضرب و القسمه و كذلك حفظ رقم و استرجاعه (تعمل مثل ذاكره بسيطه)، شكلها كالتالى: (Sample/Calculator.h)
class Calculator
{
public:
Calculator();
~Calculator();
double Add(double n);
double Sub(double n);
double Mul(double n);
double Div(double n);
void Sav(double n);
double Res();
double getValue() { return _num; }
CommandManager* getManager() { return &_manager; }
private:
double _num;
double _saved;
CommandManager _manager;
};الدوال Add و Sub و Mul و Div تقوم بجمع و طرح و ضرب و قسمة الرقم المدخل لهم بالرقم المسجل داخل المتغير _num و يتم العوده منهم بالقيمه الجديده للمتغير _num.
الداله getValue تعود بقيمة المتغير _num.
الداله getManager تعود بنسخة الكائن _manager المسؤل عن الـ Commands.
بالنسبه لأكواد الدوال Add و Sub و Mul و Div سيتم شرحهم لاحقا بعد شرح الفئات الخاصه بالـ Commands.
فئات الـ Command
تسمح الفئه Calculator لمستخدمها أن يقوم بمجموعه من العمليات و هى الجمع و الطرح و الضرب و القسمه و حفظ رقم و استرجاعه و حيث أن هذه العمليات متاح استخدامها يمكن التنبوء انه قد يحتاج المستخدم بالتراجع عنهم و بالتالى نحتاج لصنع فئات خاصه بمتابعة تنفيذ هذه العمليات و ذلك حتى نستطيع التراجع عنهم إن طلب ذلك، لذا و حيث ان الفئه تسمح لنا بإجراء 6 عمليات من خلالها لذا سنحتاج لوجود 6 فئات ليمثلوا هذه العمليات اثناء التراجع او اعادة التنفيذ.
الفئه AddCommand
تمثل الفئه AddCommand عملية الجمع و الكود الخاص بها شكله كالتالى:(Sample/AddCommand.h)
class AddCommand : public CommandBase
{
public:
AddCommand(Calculator* c, double n) : CommandBase(&n, sizeof(double)), _calc(c) {}
~AddCommand() {}
private:
void RedoInternal()
{
double val = *(double*)_value;
printf("\nredoing: adding %g\n", val);
_calc->Add(val);
}
void UndoInternal()
{
double val = *(double*)_value;
printf("\nundoing: subtracting %g\n", val);
_calc->Sub(val);
}
private:
Calculator* _calc;
};حيث ان هذه الفئه مرتبطه بعملية الجمع لذا الفعل الإفتراضى لها هو الجمع و التراجع عنه يكون بالطرح.
الفئه SubtractCommand
تمثل الفئه SubtractCommand عملية الجمع و الكود الخاص بها شكله كالتالى:(Sample/SubtractCommand.h)
class SubtractCommand : public CommandBase
{
public:
SubtractCommand(Calculator* c, double n) : CommandBase(&n, sizeof(double)), _calc(c) {}
~SubtractCommand() {}
private:
void RedoInternal()
{
double val = *(double*)_value;
printf("\nredoing: subtracting %g\n", val);
_calc->Sub(val);
}
void UndoInternal()
{
double val = *(double*)_value;
printf("\nundoing: adding %g\n", val);
_calc->Add(val);
}
private:
Calculator* _calc;
};حيث ان هذه الفئه مرتبطه بعملية الطرح لذا فالفعل الإفتراضى لها هو الطرح و التراجع عنه يكون بالجمع.
الفئه MultiplyCommand
تمثل الفئه MultiplyCommand عملية الجمع و الكود الخاص بها شكله كالتالى:(Sample/MultiplyCommand.h)
class MultiplyCommand : public CommandBase
{
public:
MultiplyCommand(Calculator* c, double n) : CommandBase(&n, sizeof(double)), _calc(c) {}
~MultiplyCommand() {}
private:
void RedoInternal()
{
double val = *(double*)_value;
printf("\nredoing: multiplying %g\n", val);
_calc->Mul(val);
}
void UndoInternal()
{
double val = *(double*)_value;
printf("\nundoing: dividing %g\n", val);
_calc->Div(val);
}
private:
Calculator* _calc;
};حيث ان هذه الفئه مرتبطه بعملية الضرب لذا فالفعل الإفتراضى لها هو الضرب و التراجع عنه يكون بالقسمه.
الفئه DivideCommand
تمثل الفئه DivideCommand عملية الجمع و الكود الخاص بها شكله كالتالى:(Sample/DivideCommand.h)
class DivideCommand : public CommandBase
{
public:
DivideCommand(Calculator* c, double n) : CommandBase(&n, sizeof(double)), _calc(c) {}
~DivideCommand() {}
private:
void RedoInternal()
{
double val = *(double*)_value;
printf("\nredoing: dividing %g\n", val);
_calc->Div(val);
}
void UndoInternal()
{
double val = *(double*)_value;
printf("\nundoing: multiplying %g\n", val);
_calc->Mul(val);
}
private:
Calculator* _calc;
};حيث ان هذه الفئه مرتبطه بعملية القسمه لذا فالفعل الإفتراضى لها هو القسمه و التراجع عنه يكون بالضرب.
الفئه SaveCommand
تمثل الفئه SaveCommand عملية حفظ رقم داخل الأله الحاسبه و الكود الخاص بها شكله كالتالى:(Sample/SaveCommand.h)
class SaveCommand : public CommandBase
{
public:
SaveCommand(Calculator* c, double n) : CommandBase(&n, sizeof(double)), _calc(c) {}
~SaveCommand() {}
private:
void RedoInternal()
{
double val = *(double*)_value;
printf("\nredoing: saving %g\n", val);
_calc->Sav(val);
}
void UndoInternal()
{
printf("\nundoing: restoring %g\n", *(double*)_value);
_calc->Res();
}
private:
Calculator* _calc;
};حيث ان هذه الفئه مرتبطه بعملية حفظ الرقم لذا فالفعل الإفتراضى لها هو الحفظ و التراجع عنه يكون بالإسترجاع.
الفئه RestoreCommand
تمثل الفئه RestoreCommand عملية إسترجاع رقم تم حفظه داخل الأله الحاسبه و الكود الخاص بها شكله كالتالى:(Sample/RestoreCommand.h)
class RestoreCommand : public CommandBase
{
public:
RestoreCommand(Calculator* c, double n) : CommandBase(&n, sizeof(double)), _calc(c) {}
~RestoreCommand() {}
private:
void RedoInternal()
{
printf("\nredoing: restoring %g\n", *(double*)_value);
_calc->Res();
}
void UndoInternal()
{
double val = *(double*)_value;
printf("\nundoing: saving %g\n", val);
_calc->Sav(val);
}
private:
Calculator* _calc;
};حيث ان هذه الفئه مرتبطه بعملية إسترجاع الرقم لذا فالفعل الإفتراضى لها هو الإسترجاع و التراجع عنه يكون بحفظ الرقم.
بهذه الفئه نكون قد انتهينا من فئات الـ Command الخاصه بهذا المثال و الـ Class Diagram لهم شكله كالتالى:
الفئه Calculator مره أخرى
الكود الخاص بالدوال الموجوده بالفئه Calculator بسيط للغايه و لا يوجد به شئ مميز، ها هو:(Sample/Calculator.cc)
Calculator::Calculator() : _num(0), _saved(0) {}
Calculator::~Calculator() {}
double Calculator::Add(double n)
{
printf("Adding %g to %g\n", n, _num);
_num += n;
printf("current value is %g\n", _num);
AddCommand* add = new AddCommand(this, n);
if(!_manager.AddCommand(add)) delete add;
return _num;
}
double Calculator::Sub(double n)
{
printf("Subtracting %g from %g\n", n, _num);
_num -= n;
printf("current value is %g\n", _num);
SubtractCommand* sub = new SubtractCommand(this, n);
if(!_manager.AddCommand(sub)) delete sub;
return _num;
}
double Calculator::Mul(double n)
{
printf("Multiplying %g by %g\n", _num, n);
_num *= n;
printf("current value is %g\n", _num);
MultiplyCommand* mul = new MultiplyCommand(this, n);
if(!_manager.AddCommand(mul)) delete mul;
return _num;
}
double Calculator::Div(double n)
{
printf("Dividing %g by %g\n", _num, n);
_num /= n;
printf("current value is %g\n", _num);
DivideCommand* div = new DivideCommand(this, n);
if(!_manager.AddCommand(div)) delete div;
return _num;
}
void Calculator::Sav(double n)
{
printf("Storing %g\n", n);
_saved = n;
printf("current stored value is %g\n", _saved);
SaveCommand* sav = new SaveCommand(this, n);
if(!_manager.AddCommand(sav)) delete sav;
}
double Calculator::Res()
{
printf("Restoring %g\n", _saved);
RestoreCommand* res = new RestoreCommand(this, _saved);
if(!_manager.AddCommand(res)) delete res;
return _saved;
}كما ترى يتم إجراء العمليه الحسابيه يليها انشاء الـ Command المناسب لها و يتم تمرير له المعاملات المطلوبه ثم يتم إضافته للـ Manager فإن لم تتم إضافته (و ذلك بأن تعود الداله AddCommand بالقيمه false) حينها سيتم تحرير الذاكره الخاصه به.
نأتي الأن للمثال التطبيقي:
int main()
{
Calculator calc;
CommandManager* manager = calc.getManager();
cout << "current value is " << calc.getValue() << endl;
calc.Add(1); // 1
calc.Add(14); // 15
calc.Mul(20.0/3); // 100
calc.Div(-2); // -50
manager->Undo(2); // (1)100 (2)15
calc.Add(12); // 27
calc.Sav(3); // 3
calc.Sub(calc.Res()); // 24
manager->Undo(); // 27
manager->Redo(1); // 24
system("pause");
}الأرقام الموجوده بنهاية كل سطر هى نتيجة العمليه.
خاتمه
هذا الدرس يناقش احد تطبيقات الـ Command Pattern و هى Undo Provider و التى تسمح لبرامجك بتخزين كل التغييرات التى اجريت عليه من قبل مستخدميه و تتيح لهم التراجع عن بعض أو كل العمليات التى قاموا بإجرائها، أيضا قمنا بإنشاء محرك بسيط لإدارة عمليات إنشاء و حفظ و استرجاع و تنفيذ الـ Commands.
فى النهايه، أرجو أن ينفعك الله بمحتوى هذا الدرس فإن اصبت فمن الله وإن أخطأت فمني ومن الشيطان.
و الله ولي التوفيق
