ConstructiCat Logo
CodeBust.
Browse section ▾

الأمر (Command).

الأمر (Command) هو نمط تصميم سلوكي يحوّل الطلب إلى كائن مستقل يحتوي على جميع المعلومات المتعلقة بالطلب. يتيح لك هذا التحويل تمرير الطلبات كوسائط للطرائق، وتأجيل تنفيذ الطلب أو وضعه في طابور، ودعم العمليات القابلة للتراجع.

##Intent

الأمر (Command) هو نمط تصميم سلوكي يحوّل الطلب إلى كائن مستقل يحتوي على جميع المعلومات المتعلقة بالطلب. ويتيح لك هذا التحويل تمرير الطلبات كوسائط للطرائق، وتأجيل تنفيذ الطلب أو وضعه في طابور، ودعم العمليات القابلة للتراجع.

نمط التصميم Command

##Problem

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

مشكلة يحلها نمط الأمر

جميع أزرار التطبيق مشتقة من الصنف نفسه.

ومع أن كل هذه الأزرار تبدو متشابهة، فإن من المفترض أن يؤدي كلٌّ منها شيئاً مختلفاً. فأين ستضع الكود الخاص بمعالِجات النقر المختلفة لهذه الأزرار؟ أبسط حل هو إنشاء عدد هائل من الأصناف الفرعية لكل موضع يُستخدم فيه الزر. وستحتوي هذه الأصناف الفرعية على الكود الذي يجب تنفيذه عند النقر على الزر.

الكثير من الأصناف الفرعية للأزرار

الكثير من الأصناف الفرعية للأزرار. ما الذي قد يسوء؟

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

عدة أصناف تنفّذ الوظيفة نفسها

عدة أصناف تنفّذ الوظيفة نفسها.

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

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

##Solution

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

وفي الكود قد يبدو ذلك هكذا: كائن واجهة رسومية يستدعي طريقة لكائن منطق عمل، مُمرِّراً إليه بعض الوسائط. وتُوصف هذه العملية عادةً بأنها كائن يرسل إلى آخر طلباً.

قد تصل طبقة الواجهة الرسومية إلى طبقة منطق العمل مباشرةً

قد تصل كائنات الواجهة الرسومية إلى كائنات منطق العمل مباشرةً.

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

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

الوصول إلى طبقة منطق العمل عبر أمر.

الوصول إلى طبقة منطق العمل عبر أمر.

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

ربما لاحظت قطعة ناقصة من الأحجية، وهي معاملات الطلب. فقد يكون كائن الواجهة الرسومية قد زوّد كائن طبقة العمل ببعض المعاملات. وبما أن طريقة تنفيذ الأمر لا تأخذ أي معاملات، فكيف نمرّر تفاصيل الطلب إلى المستقبِل؟ يتبيّن أن الأمر ينبغي إما أن يكون مُهيّأً مسبقاً بهذه البيانات، أو قادراً على الحصول عليها بنفسه.

كائنات الواجهة الرسومية تفوّض العمل إلى الأوامر

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

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

ستنفّذ مجموعة من أصناف الأوامر لكل عملية ممكنة وتربطها بأزرار معيّنة، تبعاً للسلوك المقصود من الأزرار.

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

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

##Structure

Invoker -command+setCommand(command)+executeCommand() «interface» Command +execute() Client Receiver ...+operation(a,b,c) ConcreteCommand1 -receiver-params+Command1(receiver, params)+execute() ConcreteCommand2 +execute() copy = new CopyCommand(editor)button.setCommand(copy) receiver.operation(params)
Invoker -command+setCommand(command)+executeCommand() «interface» Command +execute() Client Receiver ...+operation(a,b,c) ConcreteCommand1 -receiver-params+Command1(receiver, params)+execute() ConcreteCommand2 +execute() copy = new CopyCommand(editor)button.setCommand(copy) receiver.operation(params) 1 2 3 4 5
  1. صنف المُرسِل (Sender) (ويُعرف أيضاً بـ المُستدعي/invoker) مسؤول عن بدء الطلبات. ويجب أن يحتوي هذا الصنف على حقل لتخزين مرجع إلى كائن أمر. ويُطلِق المُرسِل ذلك الأمر بدلاً من إرسال الطلب مباشرةً إلى المستقبِل. لاحظ أن المُرسِل ليس مسؤولاً عن إنشاء كائن الأمر. وعادةً ما يحصل على أمر مُنشأ مسبقاً من العميل عبر الباني.

  2. واجهة الأمر (Command) تُعلن عادةً طريقة واحدة فقط لتنفيذ الأمر.

  3. الأوامر المحددة (Concrete Commands) تنفّذ أنواعاً مختلفة من الطلبات. ولا يُفترض بالأمر المحدد أن ينفّذ العمل بنفسه، بل أن يمرّر الاستدعاء إلى أحد كائنات منطق العمل. غير أنه لأجل تبسيط الكود، يمكن دمج هذه الأصناف.

    يمكن إعلان المعاملات اللازمة لتنفيذ طريقة على كائن مستقبِل كحقول في الأمر المحدد. ويمكنك جعل كائنات الأوامر غير قابلة للتغيير بالسماح فقط بتهيئة هذه الحقول عبر الباني.

  4. صنف المستقبِل (Receiver) يحتوي على بعض منطق العمل. ويمكن لأي كائن تقريباً أن يؤدي دور المستقبِل. ومعظم الأوامر تتولّى فقط تفاصيل كيفية تمرير الطلب إلى المستقبِل، بينما يقوم المستقبِل نفسه بالعمل الفعلي.

  5. العميل (Client) يُنشئ كائنات الأوامر المحددة ويهيّئها. ويجب على العميل تمرير جميع معاملات الطلب، بما في ذلك نسخة من المستقبِل، إلى باني الأمر. وبعد ذلك، يمكن ربط الأمر الناتج بمُرسِل واحد أو عدة مُرسِلات.

##Pseudocode

في هذا المثال، يساعد نمط الأمر (Command) على تتبّع سجلّ العمليات المنفَّذة ويجعل من الممكن التراجع عن عملية عند الحاجة.

بنية مثال نمط الأمر

عمليات قابلة للتراجع في محرر نصوص.

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

كود العميل (عناصر الواجهة الرسومية، وسجلّ الأوامر، وغير ذلك) غير مقترن بأصناف الأوامر المحددة لأنه يتعامل مع الأوامر عبر واجهة الأمر. ويتيح لك هذا النهج إدخال أوامر جديدة إلى التطبيق دون كسر أي كود موجود.

// صنف الأمر الأساسي يُعرّف الواجهة المشتركة لجميع الأوامر المحددة.
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

    // تُعلَن طريقة التنفيذ مجردة لإجبار جميع الأوامر المحددة على تقديم تنفيذها الخاص. ويجب أن تُعيد الطريقة true أو false تبعاً لما إذا كان الأمر يغيّر حالة المحرر.
    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


// سجلّ الأوامر العام ما هو إلا مكدّس.
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
        // إدراج محتويات الحافظة عند الموضع الحالي.


// يُهيّئ صنف التطبيق العلاقات بين الكائنات، وهو يؤدي دور المُرسِل: فعندما يلزم تنفيذ شيء ما، يُنشئ كائن أمر وينفّذه.
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()

##Applicability

استخدم نمط الأمر (Command) عندما تريد تحديد معاملات الكائنات بعمليات.

يمكن لنمط الأمر أن يحوّل استدعاء طريقة معيّن إلى كائن مستقل. ويفتح هذا التغيير الكثير من الاستخدامات المثيرة للاهتمام: إذ يمكنك تمرير الأوامر كوسائط للطرائق، وتخزينها داخل كائنات أخرى، وتبديل الأوامر المرتبطة أثناء التشغيل، وغير ذلك.

إليك مثالاً: أنت تطوّر مكوّن واجهة رسومية مثل قائمة سياقية، وتريد أن يتمكّن مستخدموك من ضبط عناصر القائمة التي تُطلِق عمليات عندما ينقر المستخدم النهائي على عنصر.

استخدم نمط الأمر عندما تريد وضع العمليات في طابور، أو جدولة تنفيذها، أو تنفيذها عن بُعد.

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

استخدم نمط الأمر عندما تريد تنفيذ عمليات قابلة للعكس.

رغم وجود طرق كثيرة لتنفيذ التراجع/الإعادة (undo/redo)، فإن نمط الأمر ربما يكون الأكثر شيوعاً بينها جميعاً.

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

لهذه الطريقة عيبان. أولاً، ليس من السهل حفظ حالة التطبيق لأن بعضها قد يكون خاصاً. ويمكن التخفيف من هذه المشكلة باستخدام نمط Memento.

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

##How to Implement

  1. أعلِن واجهة الأمر بطريقة تنفيذ واحدة.

  2. ابدأ باستخراج الطلبات إلى أصناف أوامر محددة تُنفّذ واجهة الأمر. ويجب أن يحتوي كل صنف على مجموعة من الحقول لتخزين وسائط الطلب إلى جانب مرجع إلى كائن المستقبِل الفعلي. ويجب تهيئة كل هذه القيم عبر باني الأمر.

  3. حدّد الأصناف التي ستؤدي دور المُرسِلات. وأضِف إلى هذه الأصناف الحقول اللازمة لتخزين الأوامر. وينبغي أن تتواصل المُرسِلات مع أوامرها عبر واجهة الأمر فقط. وعادةً لا تُنشئ المُرسِلات كائنات الأوامر بنفسها، بل تحصل عليها من كود العميل.

  4. غيّر المُرسِلات بحيث تنفّذ الأمر بدلاً من إرسال طلب إلى المستقبِل مباشرةً.

  5. ينبغي للعميل تهيئة الكائنات بالترتيب التالي:

    • إنشاء المستقبِلات.
    • إنشاء الأوامر، وربطها بالمستقبِلات عند الحاجة.
    • إنشاء المُرسِلات، وربطها بأوامر محددة.

##Pros & Cons

Pros
  • مبدأ المسؤولية الواحدة. يمكنك فصل الأصناف التي تستدعي العمليات عن الأصناف التي تنفّذ هذه العمليات.
  • مبدأ الانفتاح/الانغلاق. يمكنك إدخال أوامر جديدة إلى التطبيق دون كسر كود العميل الموجود.
  • يمكنك تنفيذ التراجع/الإعادة (undo/redo).
  • يمكنك تنفيذ التأجيل لتنفيذ العمليات.
  • يمكنك تجميع مجموعة من الأوامر البسيطة في أمر معقّد.
Cons
  • قد يصبح الكود أكثر تعقيداً لأنك تُدخل طبقة جديدة كاملة بين المُرسِلات والمستقبِلات.

##Relations with Other Patterns

  • تتناول أنماط Chain of Responsibility وCommand وMediator وObserver طرقاً مختلفة لربط مُرسِلات الطلبات بمستقبِليها:

    • Chain of Responsibility يمرّر الطلب تتابعياً على طول سلسلة ديناميكية من المستقبِلات المحتملة حتى يعالجه أحدها.
    • Command يُنشئ روابط أحادية الاتجاه بين المُرسِلات والمستقبِلات.
    • Mediator يُزيل الروابط المباشرة بين المُرسِلات والمستقبِلات، مُجبراً إياها على التواصل بشكل غير مباشر عبر كائن وسيط.
    • Observer يتيح للمستقبِلات الاشتراك في تلقّي الطلبات وإلغاء الاشتراك ديناميكياً.
  • يمكن تنفيذ المعالِجات في Chain of Responsibility على هيئة Commands. وفي هذه الحالة، يمكنك تنفيذ الكثير من العمليات المختلفة على كائن السياق نفسه، الذي يُمثَّل بطلب.

    غير أن هناك نهجاً آخر، يكون فيه الطلب نفسه كائن Command. وفي هذه الحالة، يمكنك تنفيذ العملية نفسها في سلسلة من السياقات المختلفة المرتبطة في سلسلة.

  • يمكنك استخدام Command وMemento معاً عند تنفيذ «التراجع». وفي هذه الحالة، تكون الأوامر مسؤولة عن تنفيذ عمليات مختلفة على كائن هدف، بينما تحفظ الـ mementos حالة ذلك الكائن قُبيل تنفيذ الأمر مباشرةً.

  • قد يبدو Command وStrategy متشابهين لأنه يمكنك استخدام كليهما لتحديد كائن بفعل ما كمعامل. غير أن لهما مقاصد مختلفة جداً.

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

    • من ناحية أخرى، يصف Strategy عادةً طرقاً مختلفة لفعل الشيء نفسه، ما يتيح لك تبديل هذه الخوارزميات ضمن صنف سياق واحد.

  • يمكن أن يساعد Prototype عندما تحتاج إلى حفظ نسخ من Commands في السجلّ.

  • يمكنك اعتبار Visitor نسخةً قوية من نمط Command. إذ يمكن لكائناته تنفيذ عمليات على كائنات شتى من أصناف مختلفة.