الفرق بين المراجعتين ل"Design Patterns/command"

من موسوعة حسوب
اذهب إلى التنقل اذهب إلى البحث
ط
سطر 47: سطر 47:
 
== مثال توضيحي ==
 
== مثال توضيحي ==
 
في هذا المثال يساعد نمط '''الأمر''' على تتبع تاريخ العمليات المنفَّذة ويجعل عكس أي عملية ممكنًا عند الحاجة إلى ذلك.
 
في هذا المثال يساعد نمط '''الأمر''' على تتبع تاريخ العمليات المنفَّذة ويجعل عكس أي عملية ممكنًا عند الحاجة إلى ذلك.
[[ملف:dpc.example.png|بدون|تصغير|(ش.9) العمليات غير القابلة للعكس -لا يمكن التراجع عنها Undoable- في محرر نصي.]]
+
[[ملف:dpc.example.png|بدون|تصغير|(ش.9) العمليات غير الممكنة (Undoable) في محرر نصي]]
 
تنشئ الأوامر التي تُحدث تغييرات في حالة المحرر (مثل النسخ واللصق) نسخةً احتياطية من حالة المحرر قبل تنفيذ العملية المرتبطة بالأمر، ويوضع في سجل الأوامر بعد تنفيذه (سجل الأوامر Command History: مكدَّس من كائنات الأمر) مع النسخة الاحتياطية من حالة المحرر في تلك النقطة.
 
تنشئ الأوامر التي تُحدث تغييرات في حالة المحرر (مثل النسخ واللصق) نسخةً احتياطية من حالة المحرر قبل تنفيذ العملية المرتبطة بالأمر، ويوضع في سجل الأوامر بعد تنفيذه (سجل الأوامر Command History: مكدَّس من كائنات الأمر) مع النسخة الاحتياطية من حالة المحرر في تلك النقطة.
  

مراجعة 03:59، 21 سبتمبر 2019

نمط الأمر (Command) هو نمط تصميم سلوكي (Behavioral Design Pattern) يحول الطلب إلى كائن مستقل بذاته بداخله كل بيانات الطلب، ويسمح لك هذا التحول بإدخال طلبات مختلفة كمعامِلات (Parameters) داخل الأساليب، وتأخير تنفيذ الطلب أو وضعه في صف انتظار، ودعم العمليات غير الممكنة (Undoable).

المشكلة

(ش.1) كل أزرار التطبيق تنحدر من نفس الفئة.

تخيل أنك تعمل على إنشاء محرر نصي جديد، ومهمتك الحالية هي إنشاء شريط أدوات به بضعة أزرار لعمليات مختلفة داخل المحرر، وقد أنشأتَ فئة Button يمكن استخدامها للأزرار التي في شريط الأدوات وكذلك في الأزرار داخل الصناديق الحوارية المختلفة. (انظر ش.1)

ورغم أن جميع تلك الأزرار تبدو متشابهة إلا أنها يُفترض أن تنفذ مهام مختلفة، ويبدو الحل الأسهل لكتابة شيفرة لمعالِجات النقرات المختلفة لتلك الأزرار هو إنشاء فئات فرعية كثيرة لكل مكان يُستخدم فيه الزر، وستحتوي تلك الفئات الشيفرة التي يجب تنفيذها عند النقر على الزر. (انظر ش.2).

(ش.2) فئات فرعية كثيرة لفئة Button، ما أسوأ ما قد يحدث؟

لكن مع الوقت سترى أن هذا المنظور به الكثير من العيوب، فلديك عدد هائل من الفئات الفرعية، ولم يكن هذا ليمثل مشكلة لو لم تكن تخاطر بتعطيل الشيفرة في هذه الفئات الفرعية في كل مرة تعدِّل فيها فئة Button الأساسية. فكأن شيفرة الواجهة الرسومية للمحرر صارت معتمدة بشكل غريب على شيفرة متطايرة لمنطق العمل (Business Logic). (انظر ش.3)

(ش.3) عدة فئات لها نفس الوظيفة.

والمشكلة هنا أن بعض العمليات مثل النسخ واللصق ستحدث من أماكن متعددة، فمثلًا قد يضغط مستخدم على زر "نسخ" صغير على شريط الأدوات، أو ينسخ شيئًا من خلال القائمة المنسدلة، أو ربما حتى يستخدم اختصار لوحة المفاتيح المشهور Ctrl+C.

ولم يكن ثمة بأس في وضع تطبيقات العمليات المختلفة في فئات الزر الفرعية حين لم يكن في المحرر سوى شريط الأدوات، بمعنى أن وجود شيفرة نسخ النصوص داخل فئة CopyButton فرعية مثلًا كان لا بأس به. لكنك تضطر إلى تكرار شيفرة العملية في عدة فئات أو جعل القوائم معتمدة على الأزرار، وذلك عندما تستخدم القوائم السياقية (Context Menus) والاختصارات وغيرها، وهو خيار أسوأ من الأول.

الحل

يبنى التصميم الجيد للبرمجيات على مبدأ فصل المشاكل والمهام ومن ثم تقسيم البرنامج إلى طبقات، والمثال المشهور في هذا الباب هو أن يكون لديك طبقة لواجهة المستخدم الرسومية وأخرى لمنطق العمل (Business Logic)، وتكون طبقة الواجهة الرسومية مسؤولة عن إخراج الصورة الجميلة على الشاشة والتقاط المدخلات وعرض نتيجة ما يفعله كل من المستخدم والتطبيق. لكن عند تنفيذ شيء مهم مثل حساب مسار القمر مثلًا أو إنشاء تقرير سنوي، فإن طبقة الواجهة الرسومية تفوض العمل إلى الطبقة التي تحتها، وهي طبقة منطق العمل.

وقد تبدو الشيفرة التي تعبر عن مثالنا هذا كالتالي: يستدعي كائن GUI أسلوبًا من كائن منطق عمل، ممررًا إليه بعض الوسائط (Arguments)، وتوصف هذه العملية بأنها كائن يرسل طلبًا إلى كائن آخر. (انظر ش.4)

(ش.4) قد تتواصل كائنات الواجهة الرسومية مع كائنات منطق العمل مباشرة.

ويقترح نمط الأمر (Command) أن كائنات الواجهة الرسومية يجب ألا ترسل تلك الطلبات بشكل مباشر، بل يجب أن تستخرج كل تفاصيل الطلب مثل الكائن المستدعَى واسم الأسلوب وقائمة الوسائط (Arguments)، تستخرجها إلى فئة command مع أسلوب وحيد يشغِّل (trigger) هذا الطلب.

وتتصرف كائنات الأمر (Command Objects) هنا كروابط بين كائنات الواجهة الرسومية وكائنات منطق العمل المختلفة، ومن تلك النقطة فإن كائن الواجهة الرسومية لا يحتاج أن يعرف أي كائن منطق عمل سيستلم الطلب ولا كيف سيعالجه، بل هو -أي كائن الواجهة- يبدأ الأمر الذي يعالج كل تلك التفاصيل. (انظر ش.5)

(ش.5) الوصول إلى طبقة منطق العمل من خلال أمر.

والخطوة التالية هي جعل أوامرك تستخدم نفس الواجهة، وعادة يكون لديها أسلوب تنفيذ وحيد لا يأخذ أي معامِلات (Parameters)، وتسمح لك هذه الواجهة باستخدام أوامر مختلفة مع نفس مرسل الطلب دون ربط ذلك مع الفئات الحقيقية للأوامر. وزيادة على ذلك تستطيع الآن تبديل كائنات الأمر المرتبطة مع المرسل، مغيرًا بهذا سلوك المرسل أثناء وقت التشغيل (runtime). ويجهز الأمر (Command) مسبقًا بتفاصيل الطلب التي ستُرسَل إلى المستقبِل أو يستطيع الحصول عليها بنفسه، ذلك أننا نريد تمرير تفاصيل الطلب إلى المستقبِل لكن أسلوب تنفيذ الأمر ليس فيه أي معامِلات (Parameters). (انظر ش.6)

(ش.6) تفوض كائنات الواجهة الرسومية العمل إلى الأوامر.

وبالعودة إلى مثال المحرر النصي، فبعد تطبيق نمط الأمر فإننا لا نكون بحاجة إلى كل فئات الزر الفرعية تلك لتطبيق سلوكيات مختلفة للنقرات، فيكفي وضع حق واحد داخل فئة Button الأساسية تخزن مرجعًا إلى كائن أمر وتجعل الزر ينفذ هذا الأمر عند النقر. ستستخدم مجموعة من فئات الأمر لكل عملية ممكنة وتربطهم جميعًا مع أزرار معينة بناءً على السلوك الذي تريده للأزرار.

أما عناصر الواجهة الرسومية الأخرى مثل القوائم والاختصارات أو الصناديق الحوارية، فيمكن تطبيقها (Implement) بنفس الطريقة، وستكون تلك العناصر مرتبطة بأمر يُنفَّذ عندما يتفاعل مستخدم مع عنصر الواجهة الرسومية، وستكون العناصر المتعلقة بنفس العمليات مرتبطة بنفس الأوامر لمنع تكرار الشيفرة. وكنتيجة لهذا تشكل الأوامر طبقة وسطى تقلل الازدواج أو الربط (Coupling) بين طبقتي الواجهة الرسومية ومنطق العمل، وهذه النتيجة جزء بسيط من منافع نمط الأمر.

مثال واقعي

(ش.7) إعداد طلب في مطعم.

لنفرض أنك دخلت مطعمًا وجلست إلى طاولة فيه وأتاك النادل ليأخذ طلبك ويكتبه على ورقة معه، سيذهب النادل بعدها إلى المطبخ ليلصق الورقة على الحائط، ولا يمر وقت طويل حتى يصل الطلب إلى الطاهي الذي يقرؤه ويطهو الطعام الذي طلبتَه، حتى إذا انتهى من إعداده يضعه على صينية مع طلبك، ويراها النادل فيأتي ليتأكد أن كل ما طلبتَه موجود ثم يحضر إليك الطعام إلى طاولتك.

في هذا المثال السابق، تكون الورقة بمثابة الأمر، وتبقى الورقة في صف انتظار حتى يكون الطاهي جاهزًا لإعدادها، ويحتوي الطلب كل البيانات المرتبطة بالوجبة وتفاصيلها التي يحتاجها الطاهي ليضعها في حسابه أثناء إعدادها، فيشرع في طهيها مباشرة دون إضاعة وقت معك من أجل توضيح تفاصيل الوجبة الذي طلبتَها.

البنية

(ش.8)
  1. تكون فئة المرسِل Sender (أو المستدعي Invoker) مسؤولة عن بدء الطلبات، ويجب أن تحتوي هذه الفئة على حقل لتخزين مرجع إلى كائنِ أمر، ويشغِّل المرسل هذا الأمر بدلًا من إرسال الطلب مباشرة إلى المستقبِل، لاحظ أن المرسل ليس مسؤولًا عن إنشاء كائن الأمر، فعادة ما يحصل على أمر منشأ مسبقًا من العميل من خلال المنشئ (Constructor).
  2. تصرح واجهة الأمر Command عادة عن أسلوب واحد لتنفيذ الأمر.
  3. تستخدم الأوامر الحقيقية Concrete Commands أنواعًا مختلفة من الطلبات، ولا يفترض بالأمر الحقيقي أن ينفذ العمل بنفسه، بل يمرر الاستدعاء إلى أحد كائنات منطق العمل. لكن يمكن دمج تلك الفئات بداعي تبسيط الشيفرة.
  4. تحتوي فئة المستقبِل Receiver على بعض منطق العمل، ويتصرف أي كائن تقريبًا كمستقبِل، وتعالج أغلب الأوامر تفاصيل تمرير الطلب إلى المستقبِل فقط، بينما ينفذ المستقبِل نفسه أغلب العمل الفعلي.
  5. ينشئ العميل 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). أما الثاني فإن النُسخ الاحتياطية للحالة قد تستهلك جزءًا كبيرًا من الذاكرة العشوائية، لذا فإنك تستطيع أحيانًا الرجوع إلى تطبيق بديل: ينفذ نمط الأمر عملية العكس بدلًا من استعادة الحالة السابقة، لكن عملية العكس قد يكون من الصعب تنفيذها أحيانًا، إن لم يكن مستحيلًا.

كيفية الاستخدام

  1. صرح عن واجهة الأمر مع أسلوب تنفيذ واحد.
  2. استخرج الطلبات إلى فئات أمر حقيقية تستخدم واجهة الأمر (Command Interface)، ويجب أن يكون لكل فئة مجموعة حقول لتخزين وسائط الطلب مع مرجع إلى الكائن المستقبِل الحقيقي، ويجب أن تُشغَّل كل تلك القيم من خلال منشئ الأمر (Command Constructor).
  3. عرِّف الفئات التي ستقوم بدور المرسل، وأضف حقول تخزين الأوامر في هذه الفئات. ينبغي للمرسِلات أن تتواصل مع أوامرها من خلال واجهة الأمر فقط، ولا تنشئ المرسلات عادة كائنات أمر بنفسها، بل تحصل عليها من شيفرة العميل.
  4. غيِّر المرسِلات بحيث تنفذ الأمر بدلًا من إرسال طلب إلى المستقبِل مباشرة.
  5. يجب أن يشغِّل العميلُ الكائنات بالترتيب التالي
    • إنشاء المستقبِلات.
    • إنشاء الأوامر وربطها بالمستقبِلات إن دعت الحاجة إلى ذلك.
    • إنشاء المرسِلات وربطها بأوامر محددة.

المزايا والعيوب

المزايا

  • مبدأ المسؤولية الواحدة. تستطيع فصل الفئات التي تستدعي عمليات من فئات تنفذ هذه العمليات.
  • مبدأ المفتوح/المغلق. تستطيع إدخال أوامر جديدة في التطبيق دون تعطيل شيفرة العميل الحالية.
  • تستطيع تطبيق التراجع/إعادة التنفيذ.
  • تستطيع تطبيق التنفيذ المؤجل للعمليات.
  • تستطيع تجميع أوامر بسيطة داخل أمر واحد أكثر تفصيلًا وتعقيدًا.

العيوب

  • قد تصبح الشيفرة أكثر تعقيدًا بما أنك تُدخل طبقة جديدة بين المرسِلات والمستقبِلات.

العلاقات مع الأنماط الأخرى

  1. تختلف أنماط سلسلة المسؤولية والأمر والوسيط والمراقب في طرق توصيل مرسِلات الطلبات ومستقبِلاتها ببعضها، وترى ذلك الاختلاف فيما يلي:
    1. تمرِّر سلسلة المسؤولية الطلب بشكل تسلسلي في سلسلة مرنة من المستقبلين المحتملين إلى أن يعالج أحدهم الطلب.
    2. ينشئ نمط الأمر وصلات أحادية الاتجاه (Unidirectional) بين المرسلين والمستقبلين.
    3. يلغي نمط الوسيط الاتصالات المباشرة بين المرسلين والمستقبلين مجبرًا إياهم على التواصل بشكل غير مباشر من خلال كائن وسيط.
    4. يسمح نمط المراقب للمستقبلين بالاشتراك في استلام الطلبات وكذلك إلغاء الاشتراك بمرونة.
  2. يمكن استخدام المداوِلات في سلسلة المسؤولية كأوامر، وفي تلك الحالة تستطيع تنفيذ عمليات كثيرة مختلفة على نفس الكائن السياقي الممثل في هيئة طلب. لكن هناك طريقة أخرى يكون فيها الطلب نفسه كائن أمر (Command Object)، وفي تلك الحالة تستطيع تنفيذ نفس العملية في تسلسل من السياقات المختلفة المرتبطة في سلسلة.
  3. تستطيع استخدام نمطي الأمر والتذكرة معًا عند إجراء التراجع (Undo)، وفي تلك الحالة تكون الأوامر مسؤولة عن تنفيذ عمليات مختلفة على كائنٍ هدف، في الوقت الذي تحفظ فيه التذكرة حالة ذلك الكائن قبل تنفيذ الأمر.
  4. قد يبدو نمط الأمر مشابهًا لنمط الخطة (Strategy) إذ تستطيع استخدام كليهما لإضافة بعض الإجراءات (Actions) كمعامِلات إلى الكائن، لكن هدف كل منهما يختلف كما يلي:
    • تستطيع استخدام نمط الأمر لتحويل أي عملية إلى كائن، وتصبح معامِلات العملية حقولًا لذلك الكائن. ويسمح لك هذا التحويل بتأجيل تنفيذ العملية أو وضعها في صف أو تخزين سجل الأوامر أو إرسال الأوامر إلى خدمات بعيدة (Remote Services)، إلخ
    • من الناحية الأخرى، يصف نمط الخطة (Strategy) عادة طرقًا مختلفة لتنفيذ نفس الشيء مما يسمح لك بتبديل تلك الخوارزميات داخل فئة سياقية واحدة.
  5. يُفضل استخدام نمط النموذج الأولي حين تريد حفظ نسخ من الأوامر داخل السجل.
  6. تستطيع النظر إلى نمط الزائر (Visitor) على أنه نسخة معززة من نمط الأمر، ذلك أن كائناته تستطيع تنفيذ عمليات على عدة كائنات من فئات مختلفة.

الاستخدم في لغة جافا

المستوى: ★ ☆ ☆

الانتشار:  ★ ★ ★

أمثلة الاستخدام: يكثر استخدام نمط الأمر في لغة جافا، ويستخدم عادة كبديل للاستدعاءات إلى إدخال إجراءاتٍ (Actions) كمعامِلات (Parameters) إلى عناصر الواجهة الرسومية. كما تستخدم أيضًا لصف المهام (Queuing Tasks) وتتبع سجل العمليات، إلخ. إليك بعض الأمثلة من نمط الأمر في مكتبات جافا:

يمكن ملاحظة نمط الأمر من خلال الأساليب السلوكية في نوعٍ مجردٍ أو واجهةٍ (المرسل) الذي يستدعي أسلوبًا في تطبيق لنوعٍ مجردٍ أو واجهةٍ مختلف (المستقبِل) الذي اختُزِل بواسطة تطبيق الأمر أثناء إنشائه، وتكون فئات الأمر عادة محدودة بإجراءات محددة.

أوامر المحرر النصي والتراجع (undo)

ينشئ المحرر النصي في هذا المثال كائنات أمر جديدة في كل مرة يتفاعل المستخدم معه، وبعد أن ينفذ الأمر إجراءاته فإن الأمر يُدفع إلى مكدَّس السجل، ولتنفيذ عملية التراجع فإن التطبيق يأخذ آخر أمر منفَّذ من السجل ويتخذ إجراءً عكسيًا أو يسترجع الحالة السابقة للمحرر المحفوظة بذلك الأمر.

الأوامر (Commands)

 commands/Command.java: أمر أساسي مجرد
package refactoring_guru.command.example.commands;

import refactoring_guru.command.example.editor.Editor;

public abstract class Command {
    public Editor editor;
    private String backup;

    Command(Editor editor) {
        this.editor = editor;
    }

    void backup() {
        backup = editor.textField.getText();
    }

    public void undo() {
        editor.textField.setText(backup);
    }

    public abstract boolean execute();
}
 commands/CopyCommand.java: نسخ النص المحدد إلى الحافظة (Clipboard)
package refactoring_guru.command.example.commands;

import refactoring_guru.command.example.editor.Editor;

public class CopyCommand extends Command {

    public CopyCommand(Editor editor) {
        super(editor);
    }

    @Override
    public boolean execute() {
        editor.clipboard = editor.textField.getSelectedText();
        return false;
    }
}
 commands/PasteCommand.java: لصق النص من الحافظة
package refactoring_guru.command.example.commands;

import refactoring_guru.command.example.editor.Editor;

public class PasteCommand extends Command {

    public PasteCommand(Editor editor) {
        super(editor);
    }

    @Override
    public boolean execute() {
        if (editor.clipboard == null || editor.clipboard.isEmpty()) return false;

        backup();
        editor.textField.insert(editor.clipboard, editor.textField.getCaretPosition());
        return true;
    }
 commands/CutCommand.java: قص النص ووضعه في الحافظة
package refactoring_guru.command.example.commands;

import refactoring_guru.command.example.editor.Editor;

public class CutCommand extends Command {

    public CutCommand(Editor editor) {
        super(editor);
    }

    @Override
    public boolean execute() {
        if (editor.textField.getSelectedText().isEmpty()) return false;

        backup();
        String source = editor.textField.getText();
        editor.clipboard = editor.textField.getSelectedText();
        editor.textField.setText(cutString(source));
        return true;
    }

    private String cutString(String source) {
        String start = source.substring(0, editor.textField.getSelectionStart());
        String end = source.substring(editor.textField.getSelectionEnd());
        return start + end;
    }
}
 commands/CommandHistory.java: سجل الأوامر
package refactoring_guru.command.example.commands;

import java.util.Stack;

public class CommandHistory {
    private Stack<Command> history = new Stack<>();

    public void push(Command c) {
        history.push(c);
    }

    public Command pop() {
        return history.pop();
    }

    public boolean isEmpty() { return history.isEmpty(); }
}

المحرر

 editor/Editor.java: الواجهة الرسومية للمحرر النصي
package refactoring_guru.command.example.editor;

import refactoring_guru.command.example.commands.*;

import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

public class Editor {
    public JTextArea textField;
    public String clipboard;
    private CommandHistory history = new CommandHistory();

    public void init() {
        JFrame frame = new JFrame("Text editor (type & use buttons, Luke!)");
        JPanel content = new JPanel();
        frame.setContentPane(content);
        frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
        content.setLayout(new BoxLayout(content, BoxLayout.Y_AXIS));
        textField = new JTextArea();
        textField.setLineWrap(true);
        content.add(textField);
        JPanel buttons = new JPanel(new FlowLayout(FlowLayout.CENTER));
        JButton ctrlC = new JButton("Ctrl+C");
        JButton ctrlX = new JButton("Ctrl+X");
        JButton ctrlV = new JButton("Ctrl+V");
        JButton ctrlZ = new JButton("Ctrl+Z");
        Editor editor = this;
        ctrlC.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                executeCommand(new CopyCommand(editor));
            }
        });
        ctrlX.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                executeCommand(new CutCommand(editor));
            }
        });
        ctrlV.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                executeCommand(new PasteCommand(editor));
            }
        });
        ctrlZ.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                undo();
            }
        });
        buttons.add(ctrlC);
        buttons.add(ctrlX);
        buttons.add(ctrlV);
        buttons.add(ctrlZ);
        content.add(buttons);
        frame.setSize(450, 200);
        frame.setLocationRelativeTo(null);
        frame.setVisible(true);
    }

    private void executeCommand(Command command) {
        if (command.execute()) {
            history.push(command);
        }
    }

    private void undo() {
        if (history.isEmpty()) return;

        Command command = history.pop();
        if (command != null) {
            command.undo();
        }
    }
}
 Demo.java: شيفرة العميل
package refactoring_guru.command.example;

import refactoring_guru.command.example.editor.Editor;

public class Demo {
    public static void main(String[] args) {
        Editor editor = new Editor();
        editor.init();
    }
}
 OutputDemo.png: نتائج التنفيذ

dpc.OutputDemo.png

الاستخدام في لغة #C

المستوى: ★ ☆ ☆

الانتشار:  ★ ★ ★

أمثلة الاستخدام: يكثر استخدام نمط الأمر في لغة #C، ويستخدم عادة كبديل للاستدعاءات إلى إدخال إجراءاتٍ (Actions) كمعامِلات (Parameters) إلى عناصر الواجهة الرسومية. كما تستخدم أيضًا لصف المهام (Queuing Tasks) وتتبع سجل العمليات، إلخ.

يمكن ملاحظة نمط الأمر من خلال الأساليب السلوكية في نوعٍ مجردٍ أو واجهةٍ (المرسل) الذي يستدعي أسلوبًا في تطبيق لنوعٍ مجردٍ أو واجهةٍ مختلف (المستقبِل) الذي اختُزِل بواسطة تطبيق الأمر أثناء إنشائه، وتكون فئات الأمر عادة محدودة بإجراءات محددة.

مثال تصوري

يوضح هذا المثال بنية نمط الأمر (Command)، ويركز على إجابة الأسئلة التالية:

  • ما الفئات التي يتكون منها؟
  • ما الأدوار التي تلعبها هذه الفئات؟
  • كيف ترتبط عناصر النمط ببعضها؟

 Program.cs: مثال تصوري

using System;

namespace RefactoringGuru.DesignPatterns.Command.Conceptual
{
    // تصرح واجهة الأمر عن أسلوب لتنفيذ أحد الأوامر.
    public interface ICommand
    {
        void Execute();
    }

    // تستطيع بعض الأوامر إجراء عمليات بأنفسها.
    class SimpleCommand : ICommand
    {
        private string _payload = string.Empty;

        public SimpleCommand(string payload)
        {
            this._payload = payload;
        }

        public void Execute()
        {
            Console.WriteLine($"SimpleCommand: See, I can do simple things like printing ({this._payload})");
        }
    }

    // (receivers) لكن بعض الأوامر تفوض العمليات المعقدة إلى كائنات أخرى تسمى المستقبِلات.
    class ComplexCommand : ICommand
    {
        private Receiver _receiver;

        // البيانات السياقية مطلوبة لتشغيل أساليب المستقبِل.
        private string _a;

        private string _b;

        // تقبل الأوامر المعقدة كائن مستقبِل واحد أو أكثر إضافة إلى أي بيانات سياقية
        // (Constructor) من خلال المنشئ.
        public ComplexCommand(Receiver receiver, string a, string b)
        {
            this._receiver = receiver;
            this._a = a;
            this._b = b;
        }

        // تستطيع الأوامر أن تفوض إلى أي أسلوب من أساليب المستقبِل.
        public void Execute()
        {
            Console.WriteLine("ComplexCommand: Complex stuff should be done by a receiver object.");
            this._receiver.DoSomething(this._a);
            this._receiver.DoSomethingElse(this._b);
        }
    }

    // على بعض منطق العمل المهم، وتعرف كيف (Receiver) تحتوي فئات المستقبِل
    // تنفذ كل أنواع العمليات المرتبطة بتنفيذ الطلب.
    // يمكن لأي فئة أن تعمل كمستقبِل.
    class Receiver
    {
        public void DoSomething(string a)
        {
            Console.WriteLine($"Receiver: Working on ({a}.)");
        }

        public void DoSomethingElse(string b)
        {
            Console.WriteLine($"Receiver: Also working on ({b}.)");
        }
    }

    // بأمر واحد أو أكثر، ويرسل الطلب إلى الأمر (Invoker) يرتبط المستدعي
    class Invoker
    {
        private ICommand _onStart;

        private ICommand _onFinish;

        // شغِّل الأوامر.
        public void SetOnStart(ICommand command)
        {
            this._onStart = command;
        }

        public void SetOnFinish(ICommand command)
        {
            this._onFinish = command;
        }

        // لا يعتمد المستدعي على فئات الأمر الحقيقي أو على المستقبِل، بل يمرر المستدعي 
        // الطلبَ إلى المستقبِل بطريقة غير مباشرة عبر تنفيذ أحد الأوامر.

        public void DoSomethingImportant()
        {
            Console.WriteLine("Invoker: Does anybody want something done before I begin?");
            if (this._onStart is ICommand)
            {
                this._onStart.Execute();
            }
            
            Console.WriteLine("Invoker: ...doing something really important...");
            
            Console.WriteLine("Invoker: Does anybody want something done after I finish?");
            if (this._onFinish is ICommand)
            {
                this._onFinish.Execute();
            }
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            // (Invoker) تستطيع شيفرة العميل أن تدخل أي أمر كمعامِل إلى المستدعي.
            Invoker invoker = new Invoker();
            invoker.SetOnStart(new SimpleCommand("Say Hi!"));
            Receiver receiver = new Receiver();
            invoker.SetOnFinish(new ComplexCommand(receiver, "Send email", "Save report"));

            invoker.DoSomethingImportant();
        }
    }
}

 Output.txt: نتائج التنفيذ

Invoker: Does anybody want something done before I begin?
SimpleCommand: See, I can do simple things like printing (Say Hi!)
Invoker: ...doing something really important...
Invoker: Does anybody want something done after I finish?
ComplexCommand: Complex stuff should be done by a receiver object.
Receiver: Working on (Send email.)
Receiver: Also working on (Save report.)

الاستخدام في لغة PHP

المستوى: ★ ☆ ☆

الانتشار:  ★ ★ ★

أمثلة الاستخدام: يكثر استخدام نمط الأمر في لغة PHP، ويستخدم عادة كبديل للاستدعاءات إلى إدخال إجراءاتٍ (Actions) كمعامِلات (Parameters) إلى عناصر الواجهة الرسومية. كما تستخدم أيضًا لصف المهام (Queuing Tasks) وتتبع سجل العمليات المُنفَّذة وإجراء عمليات التراجع (undo).

مثال تصوري

يوضح هذا المثال بنية نمط الأمر (Command)، ويركز على إجابة الأسئلة التالية:

  • ما الفئات التي يتكون منها؟
  • ما الأدوار التي تلعبها هذه الفئات؟
  • كيف ترتبط عناصر النمط ببعضها؟

سيكون من السهل عليك استيعاب المثال التالي بعد تعلم بنية النمط، بناء على استخدام واقعي له في لغة PHP.

 index.php: مثال تصوري 

<?php

namespace RefactoringGuru\Command\Conceptual;

/**
 * تصرح واجهة الأمر عن أسلوب لتنفيذ أحد الأوامر.
 */
interface Command
{
    public function execute(): void;
}

/**
 * تستطيع بعض الأوامر إجراء عمليات بأنفسها.
 */
class SimpleCommand implements Command
{
    private $payload;

    public function __construct(string $payload)
    {
        $this->payload = $payload;
    }

    public function execute(): void
    {
        echo "SimpleCommand: See, I can do simple things like printing (" . $this->payload . ")\n";
    }
}

/**
 * (receivers) لكن بعض الأوامر تفوض العمليات المعقدة إلى كائنات أخرى تسمى المستقبِلات.
 */
class ComplexCommand implements Command
{
    /**
     * @var Receiver
     */
    private $receiver;

    /**
     * البيانات السياقية مطلوبة لتشغيل أساليب المستقبِل.
     */
    private $a;

    private $b;

    /**
     * تقبل الأوامر المعقدة كائن مستقبِل واحد أو أكثر إضافة إلى أي بيانات سياقية
     * (Constructor) من خلال المنشئ.
     */
    public function __construct(Receiver $receiver, string $a, string $b)
    {
        $this->receiver = $receiver;
        $this->a = $a;
        $this->b = $b;
    }

    /**
     * تستطيع الأوامر أن تفوض إلى أي أسلوب من أساليب المستقبِل.
     */
    public function execute(): void
    {
        echo "ComplexCommand: Complex stuff should be done by a receiver object.\n";
        $this->receiver->doSomething($this->a);
        $this->receiver->doSomethingElse($this->b);
    }
}

/**
 * على بعض منطق العمل المهم، وتعرف كيف (Receiver) تحتوي فئات المستقبِل
 * تنفذ كل أنواع العمليات المرتبطة بتنفيذ الطلب.
 * يمكن لأي فئة أن تعمل كمستقبِل.
 */
class Receiver
{
    public function doSomething(string $a): void
    {
        echo "Receiver: Working on (" . $a . ".)\n";
    }

    public function doSomethingElse(string $b): void
    {
        echo "Receiver: Also working on (" . $b . ".)\n";
    }
}

/**
 * بأمر واحد أو أكثر، ويرسل الطلب إلى الأمر (Invoker) يرتبط المستدعي
 */
class Invoker
{
    /**
     * @var Command
     */
    private $onStart;

    /**
     * @var Command
     */
    private $onFinish;

    /**
     * شغِّل الأوامر.
     */
    public function setOnStart(Command $command): void
    {
        $this->onStart = $command;
    }

    public function setOnFinish(Command $command): void
    {
        $this->onFinish = $command;
    }

    /**
     * لا يعتمد المستدعي على فئات الأمر الحقيقي أو على المستقبِل، بل يمرر المستدعي 
     * الطلبَ إلى المستقبِل بطريقة غير مباشرة عبر تنفيذ أحد الأوامر.

     */
    public function doSomethingImportant(): void
    {
        echo "Invoker: Does anybody want something done before I begin?\n";
        if ($this->onStart instanceof Command) {
            $this->onStart->execute();
        }

        echo "Invoker: ...doing something really important...\n";

        echo "Invoker: Does anybody want something done after I finish?\n";
        if ($this->onFinish instanceof Command) {
            $this->onFinish->execute();
        }
    }
}

/**
 * (Invoker) تستطيع شيفرة العميل أن تدخل أي أمر كمعامِل إلى المستدعي.
 */
$invoker = new Invoker;
$invoker->setOnStart(new SimpleCommand("Say Hi!"));
$receiver = new Receiver;
$invoker->setOnFinish(new ComplexCommand($receiver, "Send email", "Save report"));

$invoker->doSomethingImportant();

 Output.txt: نتائج التنفيذ

Invoker: Does anybody want something done before I begin?
SimpleCommand: See, I can do simple things like printing (Say Hi!)
Invoker: ...doing something really important...
Invoker: Does anybody want something done after I finish?
ComplexCommand: Complex stuff should be done by a receiver object.
Receiver: Working on (Send email.)
Receiver: Also working on (Save report.)

مثال واقعي

يُستخدم نمط الأمر Command في هذا المثال لصفّ استدعاءات الاستخلاص (Web scraping calls) إلى موقع IMDB ومن ثم تنفيذها واحدًا تلو الآخر، ويُحفظ الصف نفسه في قاعدة بيانات تساهم في الاحتفاظ بالأوامر بين إطلاقات الشيفرة النصية (Script Launches).

index.php: مثال واقعي

<?php

namespace RefactoringGuru\Command\RealWorld;

/**
 * تصرح واجهة الأمر عن أسلوب التنفيذ الأساسي إضافة إلى عدة أساليب مساعدة لاسترجاع
 * البيانات الوصفية للأمر.
 */
interface Command
{
    public function execute(): void;

    public function getId(): int;

    public function getStatus(): int;
}

/**
 * البنيةَ التحتيةَ الأساسية للتحميل، والمشتركة (Web Scraping) يعرِّف أمرُ استخلاص الويب الأساسي
 * بين كل أوامر استخلاص الويب الحقيقية.
 */
abstract class WebScrapingCommand implements Command
{
    public $id;

    public $status = 0;

    /**
     * @var الرابط النصي للاستخلاص.
     */
    public $url;

    public function __construct(string $url)
    {
        $this->url = $url;
    }

    public function getId(): int
    {
        return $this->id;
    }

    public function getStatus(): int
    {
        return $this->status;
    }

    public function getURL(): string
    {
        return $this->url;
    }

    /**
     * بما أن أساليب التنفيذ لكل أوامر استخلاص الويب متشابهة، فيمكننا توفير 
     * استخدام افتراضي ونعطي للفئات الفرعية صلاحية تخطيها إن دعت الحاجة.
     *
     * قد تلاحظ نمطًا سلوكيًا آخر هنا إن دققت النظر!
     */
    public function execute(): void
    {
        $html = $this->download();
        $this->parse($html);
        $this->complete();
    }

    public function download(): string
    {
        $html = file_get_contents($this->getURL());
        echo "WebScrapingCommand: Downloaded {$this->url}\n";

        return $html;
    }

    abstract public function parse(string $html): void;

    public function complete(): void
    {
        $this->status = 1;
        Queue::get()->completeCommand($this);
    }
}

/**
 * الأمر الحقيقي لاستخلاص قائمة من تصانيف الأفلام.
 */
class IMDBGenresScrapingCommand extends WebScrapingCommand
{
    public function __construct()
    {
        $this->url = "https://www.imdb.com/feature/genre/";
    }

    /**
     * استخرج كل التصانيف وروابط بحثها من الصفحة التالية:
     * https://www.imdb.com/feature/genre/
     */
    public function parse($html): void
    {
        preg_match_all("|href=\"(https://www.imdb.com/search/title\?genres=.*?)\"|", $html, $matches);
        echo "IMDBGenresScrapingCommand: Discovered " . count($matches[1]) . " genres.\n";

        foreach ($matches[1] as $genre) {
            Queue::get()->add(new IMDBGenrePageScrapingCommand($genre));
        }
    }
}

/**
 * الأمر الحقيقي لاستخلاص قائمة من الأفلام في تصنيف معين.
 */
class IMDBGenrePageScrapingCommand extends WebScrapingCommand
{
    private $page;

    public function __construct(string $url, int $page = 1)
    {
        parent::__construct($url);
        $this->page = $page;
    }

    public function getURL(): string
    {
        return $this->url . '?page=' . $this->page;
    }

    /**
     * استخرج كل الأفلام من صفحة كهذه:
     * https://www.imdb.com/search/title?genres=sci-fi&explore=title_type,genres
     */
    public function parse(string $html): void
    {
        preg_match_all("|href=\"(/title/.*?/)\?ref_=adv_li_tt\"|", $html, $matches);
        echo "IMDBGenrePageScrapingCommand: Discovered " . count($matches[1]) . " movies.\n";

        foreach ($matches[1] as $moviePath) {
            $url = "https://www.imdb.com" . $moviePath;
            Queue::get()->add(new IMDBMovieScrapingCommand($url));
        }

        // رابط الصفحة التالية (Parse) حلل..
        if (preg_match("|Next &#187;</a>|", $html)) {
            Queue::get()->add(new IMDBGenrePageScrapingCommand($this->url, $this->page + 1));
        }
    }
}

/**
 * الأمر الحقيقي لاستخلاص تفاصيل الفيلم.
 */
class IMDBMovieScrapingCommand extends WebScrapingCommand
{
    /**
     * احصل على معلومات الفيلم من صفحة كهذه:
     * https://www.imdb.com/title/tt4154756/
     */
    public function parse(string $html): void
    {
        if (preg_match("|<h1 itemprop=\"name\" class=\"\">(.*?)</h1>|", $html, $matches)) {
            $title = $matches[1];
        }
        echo "IMDBMovieScrapingCommand: Parsed movie $title.\n";
    }
}

/**
 * فتكدِّس كائنات الأمر وتنفذها واحدًا تلو الآخر ،(Invoker) كمستدعي Queue تتصرف فئة
 * فإن أوُقِف تنفيذُ الشيفرة النصية فجأة فإن الصف وكل أوامره يمكن استرجاعهم جميعًا بسهولة.
 * ولن تضطر إلى تكرار تنفيذ كل الأوامر التي سبق تنفيذها.
 * 
 * الذي يخزن الأوامر في قاعدة (Command Queue) لاحظ أن هذا تطبيق بدائي جدًا لصف الأوامر
 * محلية، فهناك عشرات الحلول المجدية المتاحة للاستخدام في التطبيقات الحقيقية SQLite بيانات.
 */
class Queue
{
    private $db;

    public function __construct()
    {
        $this->db = new \SQLite3(__DIR__ . '/commands.sqlite',
            SQLITE3_OPEN_CREATE | SQLITE3_OPEN_READWRITE);

        $this->db->query('CREATE TABLE IF NOT EXISTS "commands" (
            "id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
            "command" TEXT,
            "status" INTEGER
        )');
    }

    public function isEmpty(): bool
    {
        $query = 'SELECT COUNT("id") FROM "commands" WHERE status = 0';

        return $this->db->querySingle($query) === 0;
    }

    public function add(Command $command): void
    {
        $query = 'INSERT INTO commands (command, status) VALUES (:command, :status)';
        $statement = $this->db->prepare($query);
        $statement->bindValue(':command', base64_encode(serialize($command)));
        $statement->bindValue(':status', $command->getStatus());
        $statement->execute();
    }

    public function getCommand(): Command
    {
        $query = 'SELECT * FROM "commands" WHERE "status" = 0 LIMIT 1';
        $record = $this->db->querySingle($query, true);
        $command = unserialize(base64_decode($record["command"]));
        $command->id = $record['id'];

        return $command;
    }

    public function completeCommand(Command $command): void
    {
        $query = 'UPDATE commands SET status = :status WHERE id = :id';
        $statement = $this->db->prepare($query);
        $statement->bindValue(':status', $command->getStatus());
        $statement->bindValue(':id', $command->getId());
        $statement->execute();
    }

    public function work(): void
    {
        while (!$this->isEmpty()) {
            $command = $this->getCommand();
            $command->execute();
        }
    }

    /**
     * من باب تسهيل المثال (Singleton) سنجعل كائن الصفِّ هنا مفردةً.
     */
    public static function get(): Queue
    {
        static $instance;
        if (!$instance) {
            $instance = new Queue;
        }

        return $instance;
    }
}

/**
 * شيفرة العميل.
 */

$queue = Queue::get();

if ($queue->isEmpty()) {
    $queue->add(new IMDBGenresScrapingCommand);
}

$queue->work();

 Output.txt: نتائج التنفيذ

WebScrapingCommand: Downloaded https://www.imdb.com/feature/genre/
IMDBGenresScrapingCommand: Discovered 14 genres.
WebScrapingCommand: Downloaded https://www.imdb.com/search/title?genres=comedy
IMDBGenrePageScrapingCommand: Discovered 50 movies.
WebScrapingCommand: Downloaded https://www.imdb.com/search/title?genres=sci-fi
IMDBGenrePageScrapingCommand: Discovered 50 movies.
WebScrapingCommand: Downloaded https://www.imdb.com/search/title?genres=horror
IMDBGenrePageScrapingCommand: Discovered 50 movies.
WebScrapingCommand: Downloaded https://www.imdb.com/search/title?genres=romance
IMDBGenrePageScrapingCommand: Discovered 50 movies.
...

الاستخدام في لغة بايثون

المستوى: ★ ☆ ☆

الانتشار:  ★ ★ ★

أمثلة الاستخدام: يكثر استخدام نمط الأمر في لغة بايثون، ويستخدم عادة كبديل للاستدعاءات إلى إدخال إجراءاتٍ (Actions) كمعامِلات (Parameters) إلى عناصر الواجهة الرسومية. كما تستخدم أيضًا لصف المهام (Queuing Tasks) وتتبع سجل العمليات، إلخ.

يمكن ملاحظة نمط الأمر من خلال الأساليب السلوكية في نوعٍ مجردٍ أو واجهةٍ (المرسل) الذي يستدعي أسلوبًا في تطبيق لنوعٍ مجردٍ أو واجهةٍ مختلف (المستقبِل) الذي اختُزِل بواسطة تطبيق الأمر أثناء إنشائه، وتكون فئات الأمر عادة محدودة بإجراءات محددة.

مثال تصوري

يوضح هذا المثال بنية نمط الأمر (Command)، ويركز على إجابة الأسئلة التالية:

  • ما الفئات التي يتكون منها؟
  • ما الأدوار التي تلعبها هذه الفئات؟
  • كيف ترتبط عناصر النمط ببعضها؟

 main.py: مثال تصوري

from __future__ import annotations
from abc import ABC, abstractmethod


class Command(ABC):
    """
    تصرح واجهة الأمر عن أسلوب لتنفيذ أحد الأوامر.
    """

    @abstractmethod
    def execute(self) -> None:
        pass


class SimpleCommand(Command):
    """
    تستطيع بعض الأوامر إجراء عمليات بأنفسها.
    """

    def __init__(self, payload: str) -> None:
        self._payload = payload

    def execute(self) -> None:
        print(f"SimpleCommand: See, I can do simple things like printing"
              f"({self._payload})")


class ComplexCommand(Command):
    """
     (receivers) لكن بعض الأوامر تفوض العمليات المعقدة إلى كائنات أخرى تسمى المستقبِلات.
    """

    def __init__(self, receiver: Receiver, a: str, b: str) -> None:
        """
        تقبل الأوامر المعقدة كائن مستقبِل واحد أو أكثر إضافة إلى أي
        (Constructor) بيانات سياقية من خلال المنشئ.
        """

        self._receiver = receiver
        self._a = a
        self._b = b

    def execute(self) -> None:
        """
        تستطيع الأوامر أن تفوض إلى أي أسلوب من أساليب المستقبِل.
        """

        print("ComplexCommand: Complex stuff should be done by a receiver object", end="")
        self._receiver.do_something(self._a)
        self._receiver.do_something_else(self._b)


class Receiver:
    """
    على بعض منطق العمل المهم، وتعرف كيف (Receiver) تحتوي فئات المستقبِل
    تنفذ كل أنواع العمليات المرتبطة بتنفيذ الطلب. بل يمكن لأي فئة أن تعمل كمستقبِل.
    """

    def do_something(self, a: str) -> None:
        print(f"\nReceiver: Working on ({a}.)", end="")

    def do_something_else(self, b: str) -> None:
        print(f"\nReceiver: Also working on ({b}.)", end="")


class Invoker:
    """
    بأمر واحد أو أكثر، ويرسل الطلب إلى الأمر (Invoker) يرتبط المستدعي.
    """

    _on_start = None
    _on_finish = None

    """
    شغِّل الأوامر.
    """

    def set_on_start(self, command: Command):
        self._on_start = command

    def set_on_finish(self, command: Command):
        self._on_finish = command

    def do_something_important(self) -> None:
        """
        لا يعتمد المستدعي على فئات الأمر الحقيقي أو على المستقبِل، بل يمرر المستدعي الطلبَ
        إلى المستقبِل بطريقة غير مباشرة عبر تنفيذ أحد الأوامر.
        """

        print("Invoker: Does anybody want something done before I begin?")
        if isinstance(self._on_start, Command):
            self._on_start.execute()

        print("Invoker: ...doing something really important...")

        print("Invoker: Does anybody want something done after I finish?")
        if isinstance(self._on_finish, Command):
            self._on_finish.execute()


if __name__ == "__main__":
    """
    (Invoker) تستطيع شيفرة العميل أن تدخل أي أمر كمعامِل إلى المستدعي.
    """

    invoker = Invoker()
    invoker.set_on_start(SimpleCommand("Say Hi!"))
    receiver = Receiver()
    invoker.set_on_finish(ComplexCommand(
        receiver, "Send email", "Save report"))

    invoker.do_something_important()

 Output.txt: نتائج التنفيذ

Invoker: Does anybody want something done before I begin?
SimpleCommand: See, I can do simple things like printing (Say Hi!)
Invoker: ...doing something really important...
Invoker: Does anybody want something done after I finish?
ComplexCommand: Complex stuff should be done by a receiver object
Receiver: Working on (Send email.)
Receiver: Also working on (Save report.)

الاستخدام في لغة روبي

المستوى: ★ ☆ ☆

الانتشار:  ★ ★ ★

أمثلة الاستخدام: يكثر استخدام نمط الأمر في لغة روبي، ويستخدم عادة كبديل للاستدعاءات إلى إدخال إجراءاتٍ (Actions) كمعامِلات (Parameters) إلى عناصر الواجهة الرسومية. كما تستخدم أيضًا لصف المهام (Queuing Tasks) وتتبع سجل العمليات، إلخ.

يمكن ملاحظة نمط الأمر من خلال الأساليب السلوكية في نوعٍ مجردٍ أو واجهةٍ (المرسل) الذي يستدعي أسلوبًا في تطبيق لنوعٍ مجردٍ أو واجهةٍ مختلف (المستقبِل) الذي اختُزِل بواسطة تطبيق الأمر أثناء إنشائه، وتكون فئات الأمر عادة محدودة بإجراءات محددة.

مثال تصوري

يوضح هذا المثال بنية نمط الأمر (Command)، ويركز على إجابة الأسئلة التالية:

  • ما الفئات التي يتكون منها؟
  • ما الأدوار التي تلعبها هذه الفئات؟
  • كيف ترتبط عناصر النمط ببعضها؟

main.rb: مثال تصوري

# تصرح واجهة الأمر عن أسلوب لتنفيذ أحد الأوامر.
class Command
  # @abstract
  def execute
    raise NotImplementedError, "#{self.class} has not implemented method '#{__method__}'"
  end
end

# تستطيع بعض الأوامر إجراء عمليات بأنفسها.
class SimpleCommand < Command
  # @param [String] payload
  def initialize(payload)
    @payload = payload
  end

  def execute
    puts "SimpleCommand: See, I can do simple things like printing (#{@payload})"
  end
end

# (receivers) لكن بعض الأوامر تفوض العمليات المعقدة إلى كائنات أخرى تسمى المستقبِلات.
class ComplexCommand < Command
  # تقبل الأوامر المعقدة كائن مستقبِل واحد أو أكثر إضافة إلى أي بيانات سياقية
  # (Constructor) من خلال المنشئ.
  def initialize(receiver, a, b)
    @receiver = receiver
    @a = a
    @b = b
  end

  # تستطيع الأوامر أن تفوض إلى أي أسلوب من أساليب المستقبِل.
  def execute
    print 'ComplexCommand: Complex stuff should be done by a receiver object'
    @receiver.do_something(@a)
    @receiver.do_something_else(@b)
  end
end

# على بعض منطق العمل المهم، وتعرف كيف (Receiver) تحتوي فئات المستقبِل
# تنفذ كل أنواع العمليات المرتبطة بتنفيذ الطلب، بل يمكن لأي فئة أن تعمل كمستقبِل.
class Receiver
  # @param [String] a
  def do_something(a)
    print "\nReceiver: Working on (#{a}.)"
  end

  # @param [String] b
  def do_something_else(b)
    print "\nReceiver: Also working on (#{b}.)"
  end
end

# The Invoker is associated with one or several commands. It sends a request to
# the command.
class Invoker
  # Initialize commands.

  # @param [Command] command
  def on_start=(command)
    @on_start = command
  end

  # @param [Command] command
  def on_finish=(command)
    @on_finish = command
  end

  # لا يعتمد المستدعي على فئات الأمر الحقيقي أو على المستقبِل، بل يمرر المستدعي الطلبَ إلى
  # المستقبِل بطريقة غير مباشرة عبر تنفيذ أحد الأوامر.
  def do_something_important
    puts 'Invoker: Does anybody want something done before I begin?'
    @on_start.execute if @on_start.is_a? Command

    puts 'Invoker: ...doing something really important...'

    puts 'Invoker: Does anybody want something done after I finish?'
    @on_finish.execute if @on_finish.is_a? Command
  end
end

# (Invoker) تستطيع شيفرة العميل أن تدخل أي أمر كمعامِل إلى المستدعي.
invoker = Invoker.new
invoker.on_start = SimpleCommand.new('Say Hi!')
receiver = Receiver.new
invoker.on_finish = ComplexCommand.new(receiver, 'Send email', 'Save report')

invoker.do_something_important

 output.txt: نتائج التنفيذ

Invoker: Does anybody want something done before I begin?
SimpleCommand: See, I can do simple things like printing (Say Hi!)
Invoker: ...doing something really important...
Invoker: Does anybody want something done after I finish?
ComplexCommand: Complex stuff should be done by a receiver object
Receiver: Working on (Send email.)
Receiver: Also working on (Save report.)

الاستخدام في لغة Swift

المستوى: ★ ☆ ☆

الانتشار:  ★ ★ ★

أمثلة الاستخدام: يكثر استخدام نمط الأمر في لغة روبي، ويستخدم عادة كبديل للاستدعاءات إلى إدخال إجراءاتٍ (Actions) كمعامِلات (Parameters) إلى عناصر الواجهة الرسومية. كما تستخدم أيضًا لصف المهام (Queuing Tasks) وتتبع سجل العمليات، إلخ.

يمكن ملاحظة نمط الأمر من خلال الأساليب السلوكية في نوعٍ مجردٍ أو واجهةٍ (المرسل) الذي يستدعي أسلوبًا في تطبيق لنوعٍ مجردٍ أو واجهةٍ مختلف (المستقبِل) الذي اختُزِل بواسطة تطبيق الأمر أثناء إنشائه، وتكون فئات الأمر عادة محدودة بإجراءات محددة.

مثال تصوري

يوضح هذا المثال بنية نمط الأمر (Command)، ويركز على إجابة الأسئلة التالية:

  • ما الفئات التي يتكون منها؟
  • ما الأدوار التي تلعبها هذه الفئات؟
  • كيف ترتبط عناصر النمط ببعضها؟

Example.swift: مثال تصوري

import XCTest

/// تصرح واجهة الأمر عن أسلوب لتنفيذ أحد الأوامر.
protocol Command {

    func execute()
}

/// تستطيع بعض الأوامر إجراء عمليات بأنفسها.
class SimpleCommand: Command {

    private var payload: String

    init(_ payload: String) {
        self.payload = payload
    }

    func execute() {
        print("SimpleCommand: See, I can do simple things like printing (" + payload + ")")
    }
}

///  (receivers) لكن بعض الأوامر تفوض العمليات المعقدة إلى كائنات أخرى تسمى المستقبِلات.
class ComplexCommand: Command {

    private var receiver: Receiver

    /// البيانات السياقية مطلوبة لتشغيل أساليب المستقبِل.
    private var a: String
    private var b: String

    /// تقبل الأوامر المعقدة كائن مستقبِل واحد أو أكثر إضافة إلى أي بيانات سياقية
    /// (Constructor) من خلال المنشئ.
    init(_ receiver: Receiver, _ a: String, _ b: String) {
        self.receiver = receiver
        self.a = a
        self.b = b
    }

    /// تستطيع الأوامر أن تفوض إلى أي أسلوب من أساليب المستقبِل.
    func execute() {
        print("ComplexCommand: Complex stuff should be done by a receiver object.\n")
        receiver.doSomething(a)
        receiver.doSomethingElse(b)
    }
}

/// على بعض منطق العمل المهم، وتعرف كيف (Receiver) تحتوي فئات المستقبِل
/// تنفذ كل أنواع العمليات المرتبطة بتنفيذ الطلب.
/// يمكن لأي فئة أن تعمل كمستقبِل.
class Receiver {

    func doSomething(_ a: String) {
        print("Receiver: Working on (" + a + ")\n")
    }

    func doSomethingElse(_ b: String) {
        print("Receiver: Also working on (" + b + ")\n")
    }
}

/// بأمر واحد أو أكثر، ويرسل الطلب إلى الأمر (Invoker) يرتبط المستدعي
class Invoker {

    private var onStart: Command?

    private var onFinish: Command?

    /// شغِّل الأوامر.

    func setOnStart(_ command: Command) {
        onStart = command
    }

    func setOnFinish(_ command: Command) {
        onFinish = command
    }

    /// لا يعتمد المستدعي على فئات الأمر الحقيقي أو على المستقبِل، بل يمرر 
    /// المستدعي الطلبَ إلى المستقبِل بطريقة غير مباشرة عبر تنفيذ أحد الأوامر.

    func doSomethingImportant() {

        print("Invoker: Does anybody want something done before I begin?")

        onStart?.execute()

        print("Invoker: ...doing something really important...")
        print("Invoker: Does anybody want something done after I finish?")

        onFinish?.execute()
    }
}

/// لنرى الآن كيف ستعمل تلك الأجزاء مع بعضها.
class CommandConceptual: XCTestCase {

    func test() {
        /// (Invoker) تستطيع شيفرة العميل أن تدخل أي أمر كمعامِل إلى المستدعي.

        let invoker = Invoker()
        invoker.setOnStart(SimpleCommand("Say Hi!"))

        let receiver = Receiver()
        invoker.setOnFinish(ComplexCommand(receiver, "Send email", "Save report"))
        invoker.doSomethingImportant()
    }
}

 Output.txt: نتائج التنفيذ

Invoker: Does anybody want something done before I begin?
SimpleCommand: See, I can do simple things like printing (Say Hi!)
Invoker: ...doing something really important...
Invoker: Does anybody want something done after I finish?
ComplexCommand: Complex stuff should be done by a receiver object.

Receiver: Working on (Send email)

Receiver: Also working on (Save report)

مثال واقعي

 Example.swift: شيفرة العميل

import Foundation
import XCTest


class DelayedOperation: Operation {

    private var delay: TimeInterval

    init(_ delay: TimeInterval = 0) {
        self.delay = delay
    }

    override var isExecuting : Bool {
        get { return _executing }
        set {
            willChangeValue(forKey: "isExecuting")
            _executing = newValue
            didChangeValue(forKey: "isExecuting")
        }
    }
    private var _executing : Bool = false

    override var isFinished : Bool {
        get { return _finished }
        set {
            willChangeValue(forKey: "isFinished")
            _finished = newValue
            didChangeValue(forKey: "isFinished")
        }
    }
    private var _finished : Bool = false

    override func start() {

        guard delay > 0 else {
            _start()
            return
        }

        let deadline = DispatchTime.now() + delay
        DispatchQueue(label: "").asyncAfter(deadline: deadline) {
            self._start()
        }
    }

    private func _start() {

        guard !self.isCancelled else {
            print("\(self): operation is canceled")
            self.isFinished = true
            return
        }

        self.isExecuting = true
        self.main()
        self.isExecuting = false
        self.isFinished = true
    }
}

class WindowOperation: DelayedOperation {

    override func main() {
        print("\(self): Windows are closed via HomeKit.")
    }

    override var description: String { return "WindowOperation" }
}

class DoorOperation: DelayedOperation {

    override func main() {
        print("\(self): Doors are closed via HomeKit.")
    }

    override var description: String { return "DoorOperation" }
}

class TaxiOperation: DelayedOperation {

    override func main() {
        print("\(self): Taxi is ordered via Uber")
    }

    override var description: String { return "TaxiOperation" }
}



class CommandRealWorld: XCTestCase {

    func testCommandRealWorld() {
        prepareTestEnvironment {

            let siri = SiriShortcuts.shared

            print("User: Hey Siri, I am leaving my home")
            siri.perform(.leaveHome)

            print("User: Hey Siri, I am leaving my work in 3 minutes")
            siri.perform(.leaveWork, delay: 3) /// for simplicity, we use seconds

            print("User: Hey Siri, I am still working")
            siri.cancel(.leaveWork)
        }
    }
}

extension CommandRealWorld {

    struct ExecutionTime {
        static let max: TimeInterval = 5
        static let waiting: TimeInterval = 4
    }

    func prepareTestEnvironment(_ execution: () -> ()) {

        /// أن ينتظر العمليات غير المتزامنة Xcode هذا الأسلوب يخبر
        /// وإلا يُنفَّذ الاختبار الأساسي مباشرة.

        let expectation = self.expectation(description: "Expectation for async operations")

        let deadline = DispatchTime.now() + ExecutionTime.waiting
        DispatchQueue.main.asyncAfter(deadline: deadline) { expectation.fulfill() }

        execution()

        wait(for: [expectation], timeout: ExecutionTime.max)
    }
}

class SiriShortcuts {

    static let shared = SiriShortcuts()
    private lazy var queue = OperationQueue()

    private init() {}

    enum Action: String {
        case leaveHome
        case leaveWork
    }

    func perform(_ action: Action, delay: TimeInterval = 0) {
        print("Siri: performing \(action)-action\n")
        switch action {
        case .leaveHome:
            add(operation: WindowOperation(delay))
            add(operation: DoorOperation(delay))
        case .leaveWork:
            add(operation: TaxiOperation(delay))
        }
    }

    func cancel(_ action: Action) {
        print("Siri: canceling \(action)-action\n")
        switch action {
        case .leaveHome:
            cancelOperation(with: WindowOperation.self)
            cancelOperation(with: DoorOperation.self)
        case .leaveWork:
            cancelOperation(with: TaxiOperation.self)
        }
    }

    private func cancelOperation(with operationType: Operation.Type) {
        queue.operations.filter { operation in
            return type(of: operation) == operationType
        }.forEach({ $0.cancel() })
    }

    private func add(operation: Operation) {
        queue.addOperation(operation)
    }
}

 Output.txt: نتائج التنفيذ

User: Hey Siri, I am leaving my home
Siri: performing leaveHome-action

User: Hey Siri, I am leaving my work in 3 minutes
Siri: performing leaveWork-action

User: Hey Siri, I am still working
Siri: canceling leaveWork-action

DoorOperation: Doors are closed via HomeKit.
WindowOperation: Windows are closed via HomeKit.
TaxiOperation: operation is canceled

الاستخدام في لغة TypeScript

المستوى: ★ ☆ ☆

الانتشار:  ★ ★ ★

أمثلة الاستخدام: يكثر استخدام نمط الأمر في لغة روبي، ويستخدم عادة كبديل للاستدعاءات إلى إدخال إجراءاتٍ (Actions) كمعامِلات (Parameters) إلى عناصر الواجهة الرسومية. كما تستخدم أيضًا لصف المهام (Queuing Tasks) وتتبع سجل العمليات، إلخ.

يمكن ملاحظة نمط الأمر من خلال الأساليب السلوكية في نوعٍ مجردٍ أو واجهةٍ (المرسل) الذي يستدعي أسلوبًا في تطبيق لنوعٍ مجردٍ أو واجهةٍ مختلف (المستقبِل) الذي اختُزِل بواسطة تطبيق الأمر أثناء إنشائه، وتكون فئات الأمر عادة محدودة بإجراءات محددة.

مثال تصوري

يوضح هذا المثال بنية نمط الأمر (Command)، ويركز على إجابة الأسئلة التالية:

  • ما الفئات التي يتكون منها؟
  • ما الأدوار التي تلعبها هذه الفئات؟
  • كيف ترتبط عناصر النمط ببعضها؟

index.ts: مثال تصوري

/**
 * تصرح واجهة الأمر عن أسلوب لتنفيذ أحد الأوامر.
 */
interface Command {
    execute(): void;
}

/**
 * تستطيع بعض الأوامر إجراء عمليات بأنفسها.
 */
class SimpleCommand implements Command {
    private payload: string;

    constructor(payload: string) {
        this.payload = payload;
    }

    public execute(): void {
        console.log(`SimpleCommand: See, I can do simple things like printing (${this.payload})`);
    }
}

/**
 * (receivers) لكن بعض الأوامر تفوض العمليات المعقدة إلى كائنات أخرى تسمى المستقبِلات.
 */
class ComplexCommand implements Command {
    private receiver: Receiver;

    /**
     * البيانات السياقية مطلوبة لتشغيل أساليب المستقبِل.
     */
    private a: string;

    private b: string;

    /**
     * تقبل الأوامر المعقدة كائن مستقبِل واحد أو أكثر إضافة إلى أي بيانات سياقية
     * (Constructor) من خلال المنشئ.
     */
    constructor(receiver: Receiver, a: string, b: string) {
        this.receiver = receiver;
        this.a = a;
        this.b = b;
    }

    /**
     * تستطيع الأوامر أن تفوض إلى أي أسلوب من أساليب المستقبِل.
     */
    public execute(): void {
        console.log('ComplexCommand: Complex stuff should be done by a receiver object.');
        this.receiver.doSomething(this.a);
        this.receiver.doSomethingElse(this.b);
    }
}

/**
 * على بعض منطق العمل المهم، وتعرف كيف (Receiver) تحتوي فئات المستقبِل
 * تنفذ كل أنواع العمليات المرتبطة بتنفيذ الطلب.
 * يمكن لأي فئة أن تعمل كمستقبِل.
 */
class Receiver {
    public doSomething(a: string): void {
        console.log(`Receiver: Working on (${a}.)`);
    }

    public doSomethingElse(b: string): void {
        console.log(`Receiver: Also working on (${b}.)`);
    }
}

/**
 * بأمر واحد أو أكثر، ويرسل الطلب إلى الأمر (Invoker) يرتبط المستدعي
 */
class Invoker {
    private onStart: Command;

    private onFinish: Command;

    /**
     * شغِّل الأوامر.
     */
    public setOnStart(command: Command): void {
        this.onStart = command;
    }

    public setOnFinish(command: Command): void {
        this.onFinish = command;
    }

    /**
     * لا يعتمد المستدعي على فئات الأمر الحقيقي أو على المستقبِل، بل يمرر المستدعي الطلبَ 
     * إلى المستقبِل بطريقة غير مباشرة عبر تنفيذ أحد الأوامر.

     */
    public doSomethingImportant(): void {
        console.log('Invoker: Does anybody want something done before I begin?');
        if (this.isCommand(this.onStart)) {
            this.onStart.execute();
        }

        console.log('Invoker: ...doing something really important...');

        console.log('Invoker: Does anybody want something done after I finish?');
        if (this.isCommand(this.onFinish)) {
            this.onFinish.execute();
        }
    }

    private isCommand(object): object is Command {
        return object.execute !== undefined;
    }
}

/**
 * (Invoker) تستطيع شيفرة العميل أن تدخل أي أمر كمعامِل إلى المستدعي.
 */
const invoker = new Invoker();
invoker.setOnStart(new SimpleCommand('Say Hi!'));
const receiver = new Receiver();
invoker.setOnFinish(new ComplexCommand(receiver, 'Send email', 'Save report'));

invoker.doSomethingImportant();

 Output.txt: نتائج التنفيذ

Invoker: Does anybody want something done before I begin?
SimpleCommand: See, I can do simple things like printing (Say Hi!)
Invoker: ...doing something really important...
Invoker: Does anybody want something done after I finish?
ComplexCommand: Complex stuff should be done by a receiver object.
Receiver: Working on (Send email.)
Receiver: Also working on (Save report.)

انظر أيضًا

مصادر