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

من موسوعة حسوب
اذهب إلى التنقل اذهب إلى البحث
(2.0 محتوى)
(2.1 محتوى)
سطر 65: سطر 65:
  
 
ثم إذا احتاج المستخدم أن يعكس عملية ما فإن التطبيق يستطيع أخذ آخر أمر من السجل ويقرأ النسخة الاحتياطية المرتبطة به من حالة المحرر ويخزنها. ولا تكون شيفرة العميل (عناصر الواجهة الرسومية، سجل الأوامر، ... إلخ) مرتبطة بفئات أمر حقيقية لأنها تعمل مع الأوامر من خلال واجهة الأمر، ويتيح لك هذا المنظور أن تدخل أوامر جديدة في التطبيق دون تعطيل الشيفرة الحالية.<syntaxhighlight lang="java">
 
ثم إذا احتاج المستخدم أن يعكس عملية ما فإن التطبيق يستطيع أخذ آخر أمر من السجل ويقرأ النسخة الاحتياطية المرتبطة به من حالة المحرر ويخزنها. ولا تكون شيفرة العميل (عناصر الواجهة الرسومية، سجل الأوامر، ... إلخ) مرتبطة بفئات أمر حقيقية لأنها تعمل مع الأوامر من خلال واجهة الأمر، ويتيح لك هذا المنظور أن تدخل أوامر جديدة في التطبيق دون تعطيل الشيفرة الحالية.<syntaxhighlight lang="java">
// The base command class defines the common interface for all
+
// تصرح فئة الأمر الأساسية عن واجهة مشتركة لكل
// concrete commands.
+
// (Concrete Commands) الأوامر الحقيقية.
 
abstract class Command is
 
abstract class Command is
 
     protected field app: Application
 
     protected field app: Application
سطر 76: سطر 76:
 
         this.editor = editor
 
         this.editor = editor
  
     // Make a backup of the editor's state.
+
     // أنشئ نسخة احتياطية من حالة المحرِّر.
 
     method saveBackup() is
 
     method saveBackup() is
 
         backup = editor.text
 
         backup = editor.text
  
     // Restore the editor's state.
+
     // استعِدْ حالة المحرِّر.
 
     method undo() is
 
     method undo() is
 
         editor.text = backup
 
         editor.text = backup
  
     // The execution method is declared abstract to force all
+
     // يُصرح عن أسلوب  التنفيذ على أنه أسلوم مجرد، من أجل إجبار كل الأوامر
     // concrete commands to provide their own implementations.
+
     // بنفسها (Implementations) الحقيقية على توفير تطبيقاتها.
     // The method must return true or false depending on whether
+
     // وفقًا لتغيير الأمر  false أو true ويجب أن يعيد الأسلوب إما
     // the command changes the editor's state.
+
     // لحالة المحرر من عدمه.
 
     abstract method execute()
 
     abstract method execute()
  
  
// The concrete commands go here.
+
// الأوامر الحقيقية تدخل هنا.
 
class CopyCommand extends Command is
 
class CopyCommand extends Command is
     // The copy command isn't saved to the history since it
+
     // لا يحفظ أمر النسخ في السجل نظرًا لأنه لا يغير حالة المحرر
    // doesn't change the editor's state.
 
 
     method execute() is
 
     method execute() is
 
         app.clipboard = editor.getSelection()
 
         app.clipboard = editor.getSelection()
سطر 100: سطر 99:
  
 
class CutCommand extends Command is
 
class CutCommand extends Command is
     // The cut command does change the editor's state, therefore
+
     // يغير أمر القص حالة المحرر لذا يجب أن يُحفظ في السجل، وسيظل
     // it must be saved to the history. And it'll be saved as
+
     // true محفوظًا طالما أن الأسلوب يعيد
    // long as the method returns true.
 
 
     method execute() is
 
     method execute() is
 
         saveBackup()
 
         saveBackup()
سطر 115: سطر 113:
 
         return true
 
         return true
  
// The undo operation is also a command.
+
// تُعد عملية التراجع أمرًا كذلك.
 
class UndoCommand extends Command is
 
class UndoCommand extends Command is
 
     method execute() is
 
     method execute() is
سطر 122: سطر 120:
  
  
// The global command history is just a stack.
+
// (Stack) السجل العام للأوامر ما هو إلا مكدَّس.
 
class CommandHistory is
 
class CommandHistory is
 
     private field history: array of Command
 
     private field history: array of Command
  
     // Last in...
+
     // الأخير دخولًا...
 
     method push(c: Command) is
 
     method push(c: Command) is
         // Push the command to the end of the history array.
+
         // أزح الأمر إلى نهاية مصفوفة السجل.
  
     // ...first out
+
     // ...الأول خروجًا
 
     method pop():Command is
 
     method pop():Command is
         // Get the most recent command from the history.
+
         // اجلب أحدث أمر من السجل
  
  
// The editor class has actual text editing operations. It plays
+
// فئة المحرر بها عمليات تعديل فعلية على النصوص، وتلعب دور المستقبِل:
// the role of a receiver: all commands end up delegating
+
// كل الأوامر في النهاية تفوض التنفيذ إلى أساليب المحرر
// execution to the editor's methods.
 
 
class Editor is
 
class Editor is
 
     field text: string
 
     field text: string
  
 
     method getSelection() is
 
     method getSelection() is
         // Return selected text.
+
         // أعد النص المحدَّد.
  
 
     method deleteSelection() is
 
     method deleteSelection() is
         // Delete selected text.
+
         // احذف النص المحدد.
  
 
     method replaceSelection(text) is
 
     method replaceSelection(text) is
         // Insert the clipboard's contents at the current
+
         // في الموضع الحالي (Clipboard) أدخل محتويات الحافظة.
        // position.
 
  
  

مراجعة 15:23، 18 مايو 2019

نمط الأمر (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)

  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) أدخل محتويات الحافظة.


// The application class sets up object relations. It acts as a
// sender: when something needs to be done, it creates a command
// object and executes it.
class Application is
    field clipboard: string
    field editors: array of Editors
    field activeEditor: Editor
    field history: CommandHistory

    // The code which assigns commands to UI objects may look
    // like this.
    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)

    // Execute a command and check whether it has to be added to
    // the history.
    method executeCommand(command) is
        if (command.execute)
            history.push(command)

    // Take the most recent command from the history and run its
    // undo method. Note that we don't know the class of that
    // command. But we don't have to, since the command knows
    // how to undo its own action.
    method undo() is
        command = history.pop()
        if (command != null)
            command.undo()