نمط الأمر (Command)
نمط الأمر (Command) هو نمط تصميم سلوكي (Behavioral Design Pattern) يحول الطلب إلى كائن مستقل بذاته بداخله كل بيانات الطلب، ويسمح لك هذا التحول بإدخال طلبات مختلفة كمعامِلات (Parameters) داخل الأساليب، وتأخير تنفيذ الطلب أو وضعه في صف انتظار، ودعم العمليات غير الممكنة (Undoable).
المشكلة
تخيل أنك تعمل على إنشاء محرر نصي جديد، ومهمتك الحالية هي إنشاء شريط أدوات به بضعة أزرار لعمليات مختلفة داخل المحرر، وقد أنشأتَ فئة Button يمكن استخدامها للأزرار التي في شريط الأدوات وكذلك في الأزرار داخل الصناديق الحوارية المختلفة.
الصورة. (ش.1) كل أزرار التطبيق تنحدر من نفس الفئة.
ورغم أن جميع تلك الأزرار تبدو متشابهة إلا أنها يُفترض أن تنفذ مهام مختلفة، ويبدو الحل الأسهل لكتابة شيفرة لمعالِجات النقرات المختلفة لتلك الأزرار هو إنشاء فئات فرعية كثيرة لكل مكان يُستخدم فيه الزر، وستحتوي تلك الفئات الشيفرة التي يجب تنفيذها عند النقر على الزر.
الصورة. (ش.2) فئات فرعية كثيرة لفئة Button، ما أسوأ ما قد يحدث؟
لكن مع الوقت سترى أن هذا المنظور به الكثير من العيوب، فلديك عدد هائل من الفئات الفرعية، ولم يكن هذا ليمثل مشكلة لو لم تكن تخاطر بتعطيل الشيفرة في هذه الفئات الفرعية في كل مرة تعدِّل فيها فئة Button الأساسية. فكأن شيفرة الواجهة الرسومية للمحرر صارت معتمدة بشكل غريب على شيفرة متطايرة لمنطق العمل (Business Logic).
الصورة. (ش.3) عدة فئات لها نفس الوظيفة.
والمشكلة هنا أن بعض العمليات مثل النسخ واللصق ستحدث من أماكن متعددة، فمثلًا قد يضغط مستخدم على زر "نسخ" صغير على شريط الأدوات، أو ينسخ شيئًا من خلال القائمة المنسدلة، أو ربما حتى يستخدم اختصار لوحة المفاتيح المشهور Ctrl+C.
ولم يكن ثمة بأس في وضع تطبيقات العمليات المختلفة في فئات الزر الفرعية حين لم يكن في المحرر سوى شريط الأدوات، بمعنى أن وجود شيفرة نسخ النصوص داخل فئة CopyButton فرعية مثلًا كان لا بأس به. لكن عندما تستخدم القوائم السياقية (Context Menues) والاختصارات وغيرها، تكون مضطرًا إلى تكرار شيفرة العملية في عدة فئات أو جعل القوائم معتمدة على الأزرار، وذاك خيار أسوأ من الأول.
الحل
يبنى التصميم الجيد للبرمجيات على مبدأ فصل المشاكل والمهام ومن ثم تقسيم البرنامج إلى طبقات، والمثال المشهور في هذا الباب هو أن يكون لديك طبقة لواجهة المستخدم الرسومية وأخرى لمنطق العمل (Business Logic)، وتكون طبقة الواجهة الرسومية مسؤولة عن إخراج الصورة الجميلة على الشاشة والتقاط المدخلات وعرض نتيجة ما يفعله كل من المستخدم والتطبيق. لكن عند تنفيذ شيء مهم مثل حساب مسار القمر مثلًا أو إنشاء تقرير سنوي، فإن طبقة الواجهة الرسومية تفوض العمل إلى الطبقة التي تحتها، وهي طبقة منطق العمل.
وقد تبدو الشيفرة التي تعبر عن مثالنا هذا كالتالي: يستدعي كائن GUI أسلوبًا من كائن منطق عمل، ممررًا إليه بعض الوسائط (Arguments)، وتوصف هذه العملية بأنها كائن يرسل طلبًا إلى كائن آخر.
الصورة. (ش.4) قد تتواصل كائنات الواجهة الرسومية مع كائنات منطق العمل مباشرة.
ويقترح نمط الأمر (Command) أن كائنات الواجهة الرسومية يجب ألا ترسل تلك الطلبات بشكل مباشر، بل يجب أن تستخرج كل تفاصيل الطلب مثل الكائن المستدعَى واسم الأسلوب وقائمة الوسائط (Arguments)، تستخرجها إلى فئة command مع أسلوب وحيد يشغِّل (trigger) هذا الطلب.
وتتصرف كائنات الأمر (Command Objects) هنا كروابط بين كائنات الواجهة الرسومية وكائنات منطق العمل المختلفة، ومن تلك النقطة فإن كائن الواجهة الرسومية لا يحتاج أن يعرف أي كائن منطق عمل سيستلم الطلب ولا كيف سيعالجه، بل هو -أي كائن الواجهة- يبدأ الأمر الذي يعالج كل تلك التفاصيل.
الصورة. (ش.5) الوصول إلى طبقة منطق العمل من خلال أمر.
والخطوة التالية هي جعل أوامرك تستخدم نفس الواجهة، وعادة يكون لديها أسلوب تنفيذ وحيد لا يأخذ أي معامِلات (Parameters)، وتسمح لك هذه الواجهة باستخدام أوامر مختلفة مع نفس مرسل الطلب دون ربط ذلك مع الفئات الحقيقية للأوامر. وزيادة على ذلك تستطيع الآن تبديل كائنات الأمر المرتبطة مع المرسل، مغيرًا بهذا سلوك المرسل أثناء وقت التشغيل (runtime).
ويكون الأمر (Command) مجهز مسبقًا بتفاصيل الطلب التي ستُرسَل إلى المستقبِل أو يستطيع الحصول عليها بنفسه، ذلك أننا نريد تمرير تفاصيل الطلب إلى المستقبِل لكن أسلوب تنفيذ الأمر ليس فيه أي معامِلات (Parameters).
الصورة. (ش.6) تفوض كائنات الواجهة الرسومية العمل إلى الأوامر.
وبالعودة إلى مثال المحرر النصي، فبعد تطبيق نمط الأمر فإننا لا نكون بحاجة إلى كل فئات الزر الفرعية تلك لتطبيق سلوكيات مختلفة للنقرات، فيكفي وضع حق واحد داخل فئة Button الأساسية تخزن مرجعًا إلى كائن أمر وتجعل الزر ينفذ هذا الأمر عند النقر. ستستخدم مجموعة من فئات الأمر لكل عملية ممكنة وتربطهم جميعًا مع أزرار معينة بناءً على السلوك الذي تريده للأزرار.
أما عناصر الواجهة الرسومية الأخرى مثل القوائم والاختصارات أو الصناديق الحوارية، فيمكن تطبيقها (Implement) بنفس الطريقة، وستكون تلك العناصر مرتبطة بأمر يُنفَّذ عندما يتفاعل مستخدم مع عنصر الواجهة الرسومية، وستكون العناصر المتعلقة بنفس العمليات مرتبطة بنفس الأوامر لمنع تكرار الشيفرة. وكنتيجة لهذا تشكل الأوامر طبقة وسطى تقلل الازدواج أو الربط (Coupling) بين طبقتي الواجهة الرسومية ومنطق العمل، وهذه النتيجة جزء بسيط من منافع نمط الأمر.
مثال واقعي
الصورة. (ش.7) إعداد طلب في مطعم.
لنفرض أنك دخلت مطعمًا وجلست إلى طاولة فيه وأتاك النادل ليأخذ طلبك ويكتبه على ورقة معه، سيذهب النادل بعدها إلى المطبخ ليلصق الورقة على الحائط، ولا يمر وقت طويل حتى يصل الطلب إلى الطاهي الذي يقرؤه ويطهو الطعام الذي طلبتَه، حتى إذا انتهى من إعداده يضعه على صينية مع طلبك، ويراها النادل فيأتي ليتأكد أن كل ما طلبتَه موجود ثم يحضر إليك الطعام إلى طاولتك.
في هذا المثال السابق، تكون الورقة بمثابة الأمر، وتبقى الورقة في صف انتظار حتى يكون الطاهي جاهزًا لإعدادها، ويحتوي الطلب كل البيانات المرتبطة بالوجبة وتفاصيلها التي يحتاجها الطاهي ليضعها في حسابه أثناء إعدادها، فيشرع في طهيها مباشرة دون إضاعة وقت معك من أجل توضيح تفاصيل الوجبة الذي طلبتَها.
البنية
الصورة. (ش.8)
- تكون فئة المرسِل Sender (أو المستدعي Invoker) مسؤولة عن بدء الطلبات، ويجب أن تحتوي هذه الفئة على حقل لتخزين مرجع إلى كائنِ أمر، ويشغِّل المرسل هذا الأمر بدلًا من إرسال الطلب مباشرة إلى المستقبِل، لاحظ أن المرسل ليس مسؤولًا عن إنشاء كائن الأمر، فعادة ما يحصل على أمر منشأ مسبقًا من العميل من خلال المنشئ (Constructor).
- تصرح واجهة الأمر Command عادة عن أسلوب واحد لتنفيذ الأمر.
- تستخدم الأوامر الحقيقية Concrete Commands أنواعًا مختلفة من الطلبات، ولا يفترض بالأمر الحقيقي أن ينفذ العمل بنفسه، بل يمرر الاستدعاء إلى أحد كائنات منطق العمل. لكن يمكن دمج تلك الفئات بداعي تبسيط الشيفرة.
- تحتوي فئة المستقبِل Receiver على بعض منطق العمل، ويتصرف أي كائن تقريبًا كمستقبِل، وتعالج أغلب الأوامر تفاصيل تمرير الطلب إلى المستقبِل فقط، بينما ينفذ المستقبِل نفسه أغلب العمل الفعلي.
- ينشئ العميل Client كائنات الأمر الحقيقي ويهيؤها، ويجب أن يمرر العميل كل معامِلات الطلب بما فيها نسخة المستقبِل (Receiver Instance) إلى منشئ الأمر، ثم يكون الأمر الناتج بعدها مرتبطًا مع مرسِل واحد أو أكثر.
مثال توضيحي
في هذا المثال يساعد نمط الأمر على تتبع تاريخ العمليات المنفَّذة ويجعل عكس أي عملية ممكنًا عند الحاجة إلى ذلك.
الصورة. (ش.9) العمليات غير القابلة للعكس -لا يمكن التراجع عنها Undoable- في محرر نصي.
تنشئ الأوامر التي تُحدث تغييرات في حالة المحرر (مثل النسخ واللصق) نسخةً احتياطية من حالة المحرر قبل تنفيذ العملية المرتبطة بالأمر، ويوضع في سجل الأوامر بعد تنفيذه (سجل الأوامر Command History: مكدَّس من كائنات الأمر) مع النسخة الاحتياطية من حالة المحرر في تلك النقطة.
ثم إذا احتاج المستخدم أن يعكس عملية ما فإن التطبيق يستطيع أخذ آخر أمر من السجل ويقرأ النسخة الاحتياطية المرتبطة به من حالة المحرر ويخزنها. ولا تكون شيفرة العميل (عناصر الواجهة الرسومية، سجل الأوامر، ... إلخ) مرتبطة بفئات أمر حقيقية لأنها تعمل مع الأوامر من خلال واجهة الأمر، ويتيح لك هذا المنظور أن تدخل أوامر جديدة في التطبيق دون تعطيل الشيفرة الحالية.
// تصرح فئة الأمر الأساسية عن واجهة مشتركة لكل
// (Concrete Commands) الأوامر الحقيقية.
abstract class Command is
protected field app: Application
protected field editor: Editor
protected field backup: text
constructor Command(app: Application, editor: Editor) is
this.app = app
this.editor = editor
// أنشئ نسخة احتياطية من حالة المحرِّر.
method saveBackup() is
backup = editor.text
// استعِدْ حالة المحرِّر.
method undo() is
editor.text = backup
// يُصرح عن أسلوب التنفيذ على أنه أسلوم مجرد، من أجل إجبار كل الأوامر
// بنفسها (Implementations) الحقيقية على توفير تطبيقاتها.
// وفقًا لتغيير الأمر false أو true ويجب أن يعيد الأسلوب إما
// لحالة المحرر من عدمه.
abstract method execute()
// الأوامر الحقيقية تدخل هنا.
class CopyCommand extends Command is
// لا يحفظ أمر النسخ في السجل نظرًا لأنه لا يغير حالة المحرر
method execute() is
app.clipboard = editor.getSelection()
return false
class CutCommand extends Command is
// يغير أمر القص حالة المحرر لذا يجب أن يُحفظ في السجل، وسيظل
// true محفوظًا طالما أن الأسلوب يعيد
method execute() is
saveBackup()
app.clipboard = editor.getSelection()
editor.deleteSelection()
return true
class PasteCommand extends Command is
method execute() is
saveBackup()
editor.replaceSelection(app.clipboard)
return true
// تُعد عملية التراجع أمرًا كذلك.
class UndoCommand extends Command is
method execute() is
app.undo()
return false
// (Stack) السجل العام للأوامر ما هو إلا مكدَّس.
class CommandHistory is
private field history: array of Command
// الأخير دخولًا...
method push(c: Command) is
// أزح الأمر إلى نهاية مصفوفة السجل.
// ...الأول خروجًا
method pop():Command is
// اجلب أحدث أمر من السجل
// فئة المحرر بها عمليات تعديل فعلية على النصوص، وتلعب دور المستقبِل:
// كل الأوامر في النهاية تفوض التنفيذ إلى أساليب المحرر
class Editor is
field text: string
method getSelection() is
// أعد النص المحدَّد.
method deleteSelection() is
// احذف النص المحدد.
method replaceSelection(text) is
// في الموضع الحالي (Clipboard) أدخل محتويات الحافظة.
// تُعِدُّ فئةُ التطبيق علاقات الكائن، وتتصرف كمرسِل: حين نحتاج إلى تنفيذ شيء ما
// فإنها تنشئ كائن أمر وتنفذه.
class Application is
field clipboard: string
field editors: array of Editors
field activeEditor: Editor
field history: CommandHistory
// قد تبدو الشيفرة التي تعيِّن الأوامر إلى كائنات الواجهة كالتالي:
method createUI() is
// ...
copy = function() { executeCommand(
new CopyCommand(this, activeEditor)) }
copyButton.setCommand(copy)
shortcuts.onKeyPress("Ctrl+C", copy)
cut = function() { executeCommand(
new CutCommand(this, activeEditor)) }
cutButton.setCommand(cut)
shortcuts.onKeyPress("Ctrl+X", cut)
paste = function() { executeCommand(
new PasteCommand(this, activeEditor)) }
pasteButton.setCommand(paste)
shortcuts.onKeyPress("Ctrl+V", paste)
undo = function() { executeCommand(
new UndoCommand(this, activeEditor)) }
undoButton.setCommand(undo)
shortcuts.onKeyPress("Ctrl+Z", undo)
// نفِّذ أمرًا وتفقد إن كان يجب أن يضاف إلى السجل.
method executeCommand(command) is
if (command.execute)
history.push(command)
// خذ أحدث أمر من السجل وشغِّل أسلوب التراجع الخاص به، لاحظ أننا لا نعرف فئة
// ذلك الأمر، لكننا لا نحتاج إلى ذلك بأي حال بما أن الأمر يعرف كيف
// يتراجع عن أفعاله الخاصة.
method undo() is
command = history.pop()
if (command != null)
command.undo()
قابلية التطبيق
- استخدم نمط الأمر حين تريد أن تدخل العمليات كمعامِلات إلى الكائنات.
يستطيع نمط الأمر أن يحول أسلوب استدعاء بعينه إلى كائن مستقل، وهذا التحويل يفتح الباب لاستخدامات كثيرة، فتستطيع تمرير الأوامر كوسائط للأساليب (Arguments)، وتخزنها داخل كائنات أخرى، وتبدل الأوامر المرتبطة أثناء وقت التشغيل، إلخ. كأن تطور أحد مكونات الواجهة الرسومية مثلًا كالقائمة المنسدلة، وتريد لمستخدميك أن يستطيعوا إعداد عناصر القائمة التي تشغِّل عمليات حين ينقر المستخدم النهائي على أحد العناصر.
- استخدم نمط الأمر حين تريد أن تضع العمليات في صف أو تجدول تنفيذها أو تنفذها عن بعد.
يمكن وضع الأمر في تسلسل -كأي كائن آخر-، ما يعني تحويله إلى نص (String) يمكن كتابته بسهولة إلى ملف أو قاعدة بيانات، ويمكن استعادة النص لاحقًا على أنه كائن الأمر الأولي، ومن ثم يمكنك تأخير وجدولة تنفيذ الأمر. وبنفس الطريقة تستطيع إرسال الأوامر عبر الشبكة أو وضعها في صف (Queue) أو في سجل (Log).
- استخدم نمط الأمر حين تريد استخدام العمليات القابلة للعكس (Reversible)
برغم وجود عدة طرق لتطبيق عمليات التراجع وإعادة التنفيذ (Undo/Redo) إلا أن نمط الأمر هو أكثر تلك الطرق شهرة تقريبًا. ولكي تستطيع عكس عملية ستحتاج إلى استخدام سجل العمليات المنفَّذة، وسجل الأوامر هو مكدَّس يحتوي كل كائنات الأمر المنفَّذة مع نسخ احتياطية لحالة التطبيق مرتبطة بها.
لكن هذا الأسلوب له عيبان، أولهما أنْ ليس من السهل حفظ حالة التطبيق بسبب أن جزءًا منه قد يكون خاصًا (Private)، وهذه المشكلة يمكن حلها باستخدام نمط التذكرة (Memento). أما الثاني فإن النُسخ الاحتياطية للحالة قد تستهلك جزءًا كبيرًا من الذاكرة العشوائية، لذا فإنك تستطيع أحيانًا الرجوع إلى تطبيق بديل: ينفذ نمط الأمر عملية العكس بدلًا من استعادة الحالة السابقة، لكن عملية العكس قد يكون من الصعب تنفيذها أحيانًا، إن لم يكن مستحيلًا.
كيفية الاستخدام
- صرح عن واجهة الأمر مع أسلوب تنفيذ واحد.
- استخرج الطلبات إلى فئات أمر حقيقية تستخدم واجهة الأمر (Command Interface)، ويجب أن يكون لكل فئة مجموعة حقول لتخزين وسائط الطلب مع مرجع إلى الكائن المستقبِل الحقيقي، ويجب أن تُشغَّل كل تلك القيم من خلال منشئ الأمر (Command Constructor).
- عرِّف الفئات التي ستقوم بدور المرسل، وأضف حقول تخزين الأوامر في هذه الفئات. ينبغي للمرسِلات أن تتواصل مع أوامرها من خلال واجهة الأمر فقط، ولا تنشئ المرسلات عادة كائنات أمر بنفسها، بل تحصل عليها من شيفرة العميل.
- غيِّر المرسِلات بحيث تنفذ الأمر بدلًا من إرسال طلب إلى المستقبِل مباشرة.
- يجب أن يشغِّل العميلُ الكائنات بالترتيب التالي
- إنشاء المستقبِلات.
- إنشاء الأوامر وربطها بالمستقبِلات إن دعت الحاجة إلى ذلك.
- إنشاء المرسِلات وربطها بأوامر محددة.
المزايا والعيوب
المزايا
- مبدأ المسؤولية الواحدة. تستطيع فصل الفئات التي تستدعي عمليات من فئات تنفذ هذه العمليات.
- مبدأ المفتوح/المغلق. تستطيع إدخال أوامر جديدة في التطبيق دون تعطيل شيفرة العميل الحالية.
- تستطيع تطبيق التراجع/إعادة التنفيذ.
- تستطيع تطبيق التنفيذ المؤجل للعمليات.
- تستطيع تجميع أوامر بسيطة داخل أمر واحد أكثر تفصيلًا وتعقيدًا.
العيوب
- قد تصبح الشيفرة أكثر تعقيدًا بما أنك تُدخل طبقة جديدة بين المرسِلات والمستقبِلات.
العلاقات مع الأنماط الأخرى
- تختلف أنماط سلسلة المسؤولية والأمر والوسيط والمراقب في طرق توصيل مرسِلات الطلبات ومستقبِلاتها ببعضها، وترى ذلك الاختلاف فيما يلي:
- تمرِّر سلسلة المسؤولية الطلب بشكل تسلسلي في سلسلة مرنة من المستقبلين المحتملين إلى أن يعالج أحدهم الطلب.
- ينشئ نمط الأمر وصلات أحادية الاتجاه (Unidirectional) بين المرسلين والمستقبلين.
- يلغي نمط الوسيط الاتصالات المباشرة بين المرسلين والمستقبلين مجبرًا إياهم على التواصل بشكل غير مباشر من خلال كائن وسيط.
- يسمح نمط المراقب للمستقبلين بالاشتراك في استلام الطلبات وكذلك إلغاء الاشتراك بمرونة.
- يمكن استخدام المداوِلات في سلسلة المسؤولية كأوامر، وفي تلك الحالة تستطيع تنفيذ عمليات كثيرة مختلفة على نفس الكائن السياقي الممثل في هيئة طلب. لكن هناك طريقة أخرى يكون فيها الطلب نفسه كائن أمر (Command Object)، وفي تلك الحالة تستطيع تنفيذ نفس العملية في تسلسل من السياقات المختلفة المرتبطة في سلسلة.
- تستطيع استخدام نمطي الأمر والتذكرة معًا عند إجراء التراجع (Undo)، وفي تلك الحالة تكون الأوامر مسؤولة عن تنفيذ عمليات مختلفة على كائنٍ هدف، في الوقت الذي تحفظ فيه التذكرة حالة ذلك الكائن قبل تنفيذ الأمر.
- قد يبدو نمط الأمر مشابهًا لنمط الخطة إذ تستطيع استخدام كليهما لإضافة بعض الأحداث كمعامِلات إلى الكائن، لكن هدف كل منهما يختلف كما يلي: