نمط التذكرة Memento

من موسوعة حسوب

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

المشكلة

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

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

ضع الصورة.

قبل تنفيذ أي عملية فإن البرنامج يحفظ لقطة من حالة الكائنات يمكن استخدامها لاحقًا لاستعادة الكائنات لحالتها السابقة.

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

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

ضع الصورة. كيف تنسخ حالة الكائن الخاصة (private)

ويجب أن تحتوي اللقطة (snapshot) الفعلية على النص الفعلي وإحداثيات المؤشر (cursor coordinates) وموضع التمرير الحالي (scroll position)، ... . وستحتاج أن تجمع هذه القيم وتضعها في حاوية ما من أجل إنشاء لقطة لحالة المحرر. والغالب أنك ستخزن كثيرًا من كائنات الحاوية تلك داخل قائمة ما تمثل سجل التغييرات (history)، ومن ثم ستكون هذه الحاويات كائنات لفئة واحدة ليس لها أي أساليب لكن في نفس الوقت فيها حقول كثيرة تعكس حالة المحرر.

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

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

الحل

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

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

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

الصورة.

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

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

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

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

البُنية

تطبيق مبني على الفئات المتداخلة

يعتمد التطبيق النموذجي للنمط على دعم الفئات المتداخلة (nested classes) الموجودة في العديد من لغات البرمجة المشهورة مثل ++C و#C وجافا.

الصورة.

  1. تستطيع فئة البادئ (originator) أن تنتج لقطات لحالتها إضافة إلى استرجاع حالتها من لقطات مسجَّلة من قبل عند الحاجة.
  2. التذكِرة (memento) هي كائنُ قيمةٍ يتصرف كلقطة من حالة البادئ، ومن الشائع جعل التذكِرة ثابتة لا تقبل التغيير، وتُمرَّر البيانات إليها مرة واحدة من خلال المنشئ.
  3. لا يدرك النائب (caretaker) متى ولماذا يلتقط حالة البادئ فحسب، بل يعرف كذلك متى يجب أن تُسترجَع الحالة. ويستطيع أن يحتفظ بسجل لتاريخ البادئ عبر تخزين مكدَّس من التذكِرات، وعند حاجة البادئ إلى العودة إلى حالة سابقة فإن النائب يجلب أحدث تذكِرة من المكدَّس ويمررها إلى أسلوب استعادة البادئ.
  4. في هذا التطبيق للنمط فإن فئة التذكرة تكون محتواة بداخل البادئ، ويسمح هذا للبادئ بالوصول إلى الحقول والأساليب الخاصة بالتذكِرة رغم أنها مصرَّحٌ عنها بأنها خاصة (private). ومن الناحية الأخرى فإن النائب ليس لديه إلا وصول محدود جدًا إلى حقول التذكِرة وأساليبها، مما يسمح له بتخزين التذكِرات في مكدَّس دون القدرة على تغيير حالتها أو التعديل فيها.

تطبيق مبني على واجهة وسيطة

هناك تطبيق بديل يناسب لغات البرمجة التي لا تدعم الفئات المتداخلة (مثل PHP).

الصورة.

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

تطبيق آخر بتغليف أكثر تقييدًا

لدينا تطبيق آخر مفيد حين لا تريد ترك أدنى فرصة للفئات الأخرى في الوصول إلى حالة البادئ من خلال التذكِرة.

الصورة.

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

مثال وهمي

يستخدم هذا المثال نمط التذكِرة مع نمط الأمر (command) لتخزين لقطات (snapshots) من الحالة المعقدة للمحرر النصي واستعادة حالة سابقة من تلك اللقطات عند الحاجة.

الصورة.

حفظ لقطات من حالة المحرر النصي.

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

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

// يحمل البادئ بعض البيانات المهمة التي قد تتغير مع الوقت.
// وهو كذلك يحدد أسلوبًا لحفظ حالته داخل تذكِرة وأسلوبًا آخر
// لاستعادة الحالة منها.
class Editor is
    private field text, curX, curY, selectionWidth

    method setText(text) is
        this.text = text

    method setCursor(x, y) is
        this.curX = curX
        this.curY = curY

    method setSelectionWidth(width) is
        this.selectionWidth = width

    // يحفظ الحالة الحالية داخل تذكِرة.
    method createSnapshot():Snapshot is
        // التذكِرة كائن ثابت لا يقبل التغيير، لهذا يمرر البادئ
        // حالته إلى معامِلات منشئ التذكِرة.
        return new Snapshot(this, text, curX, curY, selectionWidth)

// تخزن فئة التذكِرة الحالة السابقة للمحرر.
class Snapshot is
    private field editor: Editor
    private field text, curX, curY, selectionWidth

    constructor Snapshot(editor, text, curX, curY, selectionWidth) is
        this.editor = editor
        this.text = text
        this.curX = curX
        this.curY = curY
        this.selectionWidth = selectionWidth

    // عند مرحلة ما، يمكن استعادة نسخة سابقة من المحرر
    // باستخدام كائن التذكِرة.
    method restore() is
        editor.setText(text)
        editor.setCursor(curX, curY)
        editor.setSelectionWidth(selectionWidth)

// (caretaker) كنائب (command object) يمكن أن يتصرف كائن الأمر
// وفي تلك الحالة فإن الأمر يحصل على تذكِرة قبل أن يغير حالة البادئ
// مباشرة. وعند طلب عملية تراجع فإنه يستعيد حالة البادئ
// من تذكِرة.
class Command is
    private field backup: Snapshot

    method makeBackup() is
        backup = editor.createSnapshot()

    method undo() is
        if (backup != null)
            backup.restore()
    // ...

قابلية التطبيق

  • استخدم نمط التذكِرة حين تريد أن تنتج لقطات من حالة الكائن لتستطيع استعادة نسخة سابقة من حالته.

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

  • استخدم النمط عندما يتسبب الوصول المباشر إلى حقول الكائن أو جالِباتِه (getters) أو محدِّداته (setters) في انتهاك لتغليفه (encapsulation).

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

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

  1. حدد أي الفئات ستلعب دور البادئ. من المهم أن تعرف ما إن كان البرنامج يستخدم كائنًا مركزيًا واحدًا من هذا النوع أو عدة كائنات أصغر.
  2. أنشئ فئة التذكِرة. وصرِّح عن مجموعة من الحقول -واحدًا واحدًا- التي تعكس (mirror) الحقول المصرَّح عنها داخل فئة البادئ.
  3. اجعل فئة التذكِرة غير قابلة للتغيير، ينبغي أن تقبل التذكِرة البيانات مرة واحدة من خلال المنشئ (constructor)، ويجب ألا يكون للفئة أي محدِّدات (setters).
  4. إن كانت لغة البرمجة التي تستخدمها تدعم الفئات المتداخلة، فأدخل التذكِرة داخل البادئ، وإلا فاستخرج واجهة فارغة من فئة التذكِرة واجعل كل الكائنات الأخرى تستخدمها لتشير إلى التذكِرة. قد تضيف بعض العمليات الوصفية إلى الواجهة لكن لا تضيف أي شيء يكشف حالة البادئ.
  5. أضف أسلوبًا إلى فئة البادئ ينتج فئات التذكِرة. ينبغي أن يمرر البادئ حالته إلى التذكِرة من خلال معطى (argument) واحد أو أكثر من معطيات منشئ التذكِرة. ويجب أن يكون نوع الإعادة للأسلوب من الواجهة التي استخرجتها في الخطوة السابقة (على فرض أنك استخرجتها). وينبغي كذلك أن يعمل أسلوب إنتاج التذكِرة مباشرة مع فئة التذكِرة.
  6. أضف أسلوبًا لاستعادة حالة البادئ إلى فئته، وينبغي أن يقبل تذكِرةً كمُعطى (argument). وإن استخرجت واجهة في الخطوة السابقة فاجعل نوع المعامِل(parameter). وفي تلك الحالة فإنك تحتاج إلى تعيين الكائن الجديد إلى فئة الوسيط بما أن البادئ يحتاج وصولًا كاملًا إلى ذلك الكائن.
  7. يجب أن يدرك النائب (caretaker) سواء كان يمثل كائنَ أمرٍ (command object) أو سجلًا أو شيئًا آخر تمامًا، يجب أن يدرك متى يطلب تذكِرات جديدة من البادئ، وكيف يخزنها ومتى يستعيد البادئ مع تذكِرة بعينها.
  8. قد يُنقل الرابط بين النائبات (caretakers) والبادئات إلى فئة التذكِرة، وفي تلك الحالة يجب أن تكون كل تذكِرة متصلةً بالبادئ الذي أنشأها، وقد ينتقل أسلوب الاستعادة كذلك إلى فئة التذكِرة، لكن هذا سيكون منطقيًا فقط في حالة إن كانت فئة التذكِرة محتواة داخل البادئ أو كانت فئة البادئ توفر محدِّدات كافية لتخطي (override) حالته.

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

المزايا

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

العيوب

  • قد يستهلك البرنامج كثيرًا من ذاكرة RAM إن كان العملاء يكثرون من إنشاء التذكِرات.
  • لابد أن يتتبع النائب (caretaker) دورة حياة البادئ حتى يستطيع تدمير التذكِرات القديمة.
  • لا تستطيع أغلب لغات البرمجة الديناميكية (مثل PHP - Python - JavaScript) أن تضمن أن الحالة داخل التذكِرة ستبقى دون تعديل.

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

  • تستطيع استخدام نمطي الأمر (Command) والتذكِرة (Memento) معًا عند تطبيق خاصية الإرجاع "undo"، وتكون الأوامر في تلك الحالة مسؤولة عن تنفيذ مختلف العمليت على الكائن الهدف، بينما تحفظ التذكِرات حالة ذلك الكائن قبل تنفيذ الأمر مباشرة.
  • تستطيع استخدام التذكِرة (memento) مع المكرّر (Iterator) لالتقاط حالة التكرار الحالية وإرجاعها (roll back) عند الحاجة.
  • قد يكون نمط النموذج الأولي (Prototype) بديلًا أبسط أحيانًا لنمط التذكِرة (Memento)، وهذا إن كان الكائن -الحالة التي تريد تخزينها في السجل- بسيطًا ومباشرًا وليس به أي روابط إلى مصادر خارجية، أو إن كانت الروابط يمكن إنشاءها مرة أخرى بسهولة.

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

المستوى: ★ ★ ★

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

أمثلة الاستخدام: يمكن تحقيق مبدأ نمط التذكِرة باستخدام التسلسل (serialization) الذي يشيع في لغة جافا، ورغم أنه ليس الطريقة الوحيدة ولا الأفضل لأخذ لقطات snapshot من حالة كائن ما، إلا أنها تسمح بتخزين نسخ احتياطية من الحالة مع حماية هيكل البادئ (originator) من الكائنات الأخرى. إليك بعض الأمثلة من النمط في مكتبات جافا:

محرر الأشكال وخاصية التراجع/الإعادة

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

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

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

المحرر

 editor/Editor.java: شيفرة المحرر
package refactoring_guru.memento.example.editor;

import refactoring_guru.memento.example.commands.Command;
import refactoring_guru.memento.example.history.History;
import refactoring_guru.memento.example.history.Memento;
import refactoring_guru.memento.example.shapes.CompoundShape;
import refactoring_guru.memento.example.shapes.Shape;

import javax.swing.*;
import java.io.*;
import java.util.Base64;

public class Editor extends JComponent {
    private Canvas canvas;
    private CompoundShape allShapes = new CompoundShape();
    private History history;

    public Editor() {
        canvas = new Canvas(this);
        history = new History();
    }

    public void loadShapes(Shape... shapes) {
        allShapes.clear();
        allShapes.add(shapes);
        canvas.refresh();
    }

    public CompoundShape getShapes() {
        return allShapes;
    }

    public void execute(Command c) {
        history.push(c, new Memento(this));
        c.execute();
    }

    public void undo() {
        if (history.undo())
            canvas.repaint();
    }

    public void redo() {
        if (history.redo())
            canvas.repaint();
    }

    public String backup() {
        try {
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            ObjectOutputStream oos = new ObjectOutputStream(baos);
            oos.writeObject(this.allShapes);
            oos.close();
            return Base64.getEncoder().encodeToString(baos.toByteArray());
        } catch (IOException e) {
            return "";
        }
    }

    public void restore(String state) {
        try {
            byte[] data = Base64.getDecoder().decode(state);
            ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(data));
            this.allShapes = (CompoundShape) ois.readObject();
            ois.close();
        } catch (ClassNotFoundException e) {
            System.out.print("ClassNotFoundException occurred.");
        } catch (IOException e) {
            System.out.print("IOException occurred.");
        }
    }
}
 editor/Canvas.java: شيفرة الحاوية (Canvas Code)
package refactoring_guru.memento.example.editor;

import refactoring_guru.memento.example.commands.ColorCommand;
import refactoring_guru.memento.example.commands.MoveCommand;
import refactoring_guru.memento.example.shapes.Shape;

import javax.swing.*;
import javax.swing.border.Border;
import java.awt.*;
import java.awt.event.*;
import java.awt.image.BufferedImage;

class Canvas extends java.awt.Canvas {
    private Editor editor;
    private JFrame frame;
    private static final int PADDING = 10;

    Canvas(Editor editor) {
        this.editor = editor;
        createFrame();
        attachKeyboardListeners();
        attachMouseListeners();
        refresh();
    }

    private void createFrame() {
        frame = new JFrame();
        frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
        frame.setLocationRelativeTo(null);

        JPanel contentPanel = new JPanel();
        Border padding = BorderFactory.createEmptyBorder(PADDING, PADDING, PADDING, PADDING);
        contentPanel.setBorder(padding);
        contentPanel.setLayout(new BoxLayout(contentPanel, BoxLayout.Y_AXIS));
        frame.setContentPane(contentPanel);

        contentPanel.add(new JLabel("Select and drag to move."), BorderLayout.PAGE_END);
        contentPanel.add(new JLabel("Right click to change color."), BorderLayout.PAGE_END);
        contentPanel.add(new JLabel("Undo: Ctrl+Z, Redo: Ctrl+R"), BorderLayout.PAGE_END);
        contentPanel.add(this);
        frame.setVisible(true);
        contentPanel.setBackground(Color.LIGHT_GRAY);
    }

    private void attachKeyboardListeners() {
        addKeyListener(new KeyAdapter() {
            @Override
            public void keyPressed(KeyEvent e) {
                if ((e.getModifiers() & KeyEvent.CTRL_MASK) != 0) {
                    switch (e.getKeyCode()) {
                        case KeyEvent.VK_Z:
                            editor.undo();
                            break;
                        case KeyEvent.VK_R:
                            editor.redo();
                            break;
                    }
                }
            }
        });
    }

    private void attachMouseListeners() {
        MouseAdapter colorizer = new MouseAdapter() {
            @Override
            public void mousePressed(MouseEvent e) {
                if (e.getButton() != MouseEvent.BUTTON3) {
                    return;
                }
                Shape target = editor.getShapes().getChildAt(e.getX(), e.getY());
                if (target != null) {
                    editor.execute(new ColorCommand(editor, new Color((int) (Math.random() * 0x1000000))));
                    repaint();
                }
            }
        };
        addMouseListener(colorizer);

        MouseAdapter selector = new MouseAdapter() {
            @Override
            public void mousePressed(MouseEvent e) {
                if (e.getButton() != MouseEvent.BUTTON1) {
                    return;
                }

                Shape target = editor.getShapes().getChildAt(e.getX(), e.getY());
                boolean ctrl = (e.getModifiers() & ActionEvent.CTRL_MASK) == ActionEvent.CTRL_MASK;

                if (target == null) {
                    if (!ctrl) {
                        editor.getShapes().unSelect();
                    }
                } else {
                    if (ctrl) {
                        if (target.isSelected()) {
                            target.unSelect();
                        } else {
                            target.select();
                        }
                    } else {
                        if (!target.isSelected()) {
                            editor.getShapes().unSelect();
                        }
                        target.select();
                    }
                }
                repaint();
            }
        };
        addMouseListener(selector);


        MouseAdapter dragger = new MouseAdapter() {
            MoveCommand moveCommand;

            @Override
            public void mouseDragged(MouseEvent e) {
                if ((e.getModifiersEx() & MouseEvent.BUTTON1_DOWN_MASK) != MouseEvent.BUTTON1_DOWN_MASK) {
                    return;
                }
                if (moveCommand == null) {
                    moveCommand = new MoveCommand(editor);
                    moveCommand.start(e.getX(), e.getY());
                }
                moveCommand.move(e.getX(), e.getY());
                repaint();
            }

            @Override
            public void mouseReleased(MouseEvent e) {
                if (e.getButton() != MouseEvent.BUTTON1 || moveCommand == null) {
                    return;
                }
                moveCommand.stop(e.getX(), e.getY());
                editor.execute(moveCommand);
                this.moveCommand = null;
                repaint();
            }
        };
        addMouseListener(dragger);
        addMouseMotionListener(dragger);
    }

    public int getWidth() {
        return editor.getShapes().getX() + editor.getShapes().getWidth() + PADDING;
    }

    public int getHeight() {
        return editor.getShapes().getY() + editor.getShapes().getHeight() + PADDING;
    }

    void refresh() {
        this.setSize(getWidth(), getHeight());
        frame.pack();
    }

    public void update(Graphics g) {
        paint(g);
    }

    public void paint(Graphics graphics) {
        BufferedImage buffer = new BufferedImage(this.getWidth(), this.getHeight(), BufferedImage.TYPE_INT_RGB);
        Graphics2D ig2 = buffer.createGraphics();
        ig2.setBackground(Color.WHITE);
        ig2.clearRect(0, 0, this.getWidth(), this.getHeight());

        editor.getShapes().paint(buffer.getGraphics());

        graphics.drawImage(buffer, 0, 0, null);
    }
}

السجل (History)

 history/History.java: السجل يخزن الأوامر (commands) والتذكِرات (mementos)
package refactoring_guru.memento.example.history;

import refactoring_guru.memento.example.commands.Command;

import java.util.ArrayList;
import java.util.List;

public class History {
    private List<Pair> history = new ArrayList<Pair>();
    private int virtualSize = 0;

    private class Pair {
        Command command;
        Memento memento;
        Pair(Command c, Memento m) {
            command = c;
            memento = m;
        }

        private Command getCommand() {
            return command;
        }

        private Memento getMemento() {
            return memento;
        }
    }

    public void push(Command c, Memento m) {
        if (virtualSize != history.size() && virtualSize > 0) {
            history = history.subList(0, virtualSize - 1);
        }
        history.add(new Pair(c, m));
        virtualSize = history.size();
    }

    public boolean undo() {
        Pair pair = getUndo();
        if (pair == null) {
            return false;
        }
        System.out.println("Undoing: " + pair.getCommand().getName());
        pair.getMemento().restore();
        return true;
    }

    public boolean redo() {
        Pair pair = getRedo();
        if (pair == null) {
            return false;
        }
        System.out.println("Redoing: " + pair.getCommand().getName());
        pair.getMemento().restore();
        pair.getCommand().execute();
        return true;
    }

    private Pair getUndo() {
        if (virtualSize == 0) {
            return null;
        }
        virtualSize = Math.max(0, virtualSize - 1);
        return history.get(virtualSize);
    }

    private Pair getRedo() {
        if (virtualSize == history.size()) {
            return null;
        }
        virtualSize = Math.min(history.size(), virtualSize + 1);
        return history.get(virtualSize - 1);
    }
}
 history/Memento.java: فئة التذكِرة
package refactoring_guru.memento.example.history;

import refactoring_guru.memento.example.editor.Editor;

public class Memento {
    private String backup;
    private Editor editor;

    public Memento(Editor editor) {
        this.editor = editor;
        this.backup = editor.backup();
    }

    public void restore() {
        editor.restore(backup);
    }
}

الأوامر Commands

 commands/Command.java: فئة الأمر الأساسية (Base Command Class)
package refactoring_guru.memento.example.commands;

public interface Command {
    String getName();
    void execute();
}
 commands/ColorCommand.java: يغير لون الشكل المختار
package refactoring_guru.memento.example.commands;

import refactoring_guru.memento.example.editor.Editor;
import refactoring_guru.memento.example.shapes.Shape;

import java.awt.*;

public class ColorCommand implements Command {
    private Editor editor;
    private Color color;

    public ColorCommand(Editor editor, Color color) {
        this.editor = editor;
        this.color = color;
    }

    @Override
    public String getName() {
        return "Colorize: " + color.toString();
    }

    @Override
    public void execute() {
        for (Shape child : editor.getShapes().getSelected()) {
            child.setColor(color);
        }
    }
}
 commands/MoveCommand.java: يحرك الشكل المختار
package refactoring_guru.memento.example.commands;

import refactoring_guru.memento.example.editor.Editor;
import refactoring_guru.memento.example.shapes.Shape;

public class MoveCommand implements Command {
    private Editor editor;
    private int startX, startY;
    private int endX, endY;

    public MoveCommand(Editor editor) {
        this.editor = editor;
    }

    @Override
    public String getName() {
        return "Move by X:" + (endX - startX) + " Y:" + (endY - startY);
    }

    public void start(int x, int y) {
        startX = x;
        startY = y;
        for (Shape child : editor.getShapes().getSelected()) {
            child.drag();
        }
    }

    public void move(int x, int y) {
        for (Shape child : editor.getShapes().getSelected()) {
            child.moveTo(x - startX, y - startY);
        }
    }

    public void stop(int x, int y) {
        endX = x;
        endY = y;
        for (Shape child : editor.getShapes().getSelected()) {
            child.drop();
        }
    }

    @Override
    public void execute() {
        for (Shape child : editor.getShapes().getSelected()) {
            child.moveBy(endX - startX, endY - startY);
        }
    }
}

 shapes: أشكال مختلفة

 shapes/Shape.java
package refactoring_guru.memento.example.shapes;

import java.awt.*;
import java.io.Serializable;

public interface Shape extends Serializable {
    int getX();
    int getY();
    int getWidth();
    int getHeight();
    void drag();
    void drop();
    void moveTo(int x, int y);
    void moveBy(int x, int y);
    boolean isInsideBounds(int x, int y);
    Color getColor();
    void setColor(Color color);
    void select();
    void unSelect();
    boolean isSelected();
    void paint(Graphics graphics);
}
 shapes/BaseShape.java
package refactoring_guru.memento.example.shapes;

import java.awt.*;

public abstract class BaseShape implements Shape {
    int x, y;
    private int dx = 0, dy = 0;
    private Color color;
    private boolean selected = false;

    BaseShape(int x, int y, Color color) {
        this.x = x;
        this.y = y;
        this.color = color;
    }

    @Override
    public int getX() {
        return x;
    }

    @Override
    public int getY() {
        return y;
    }

    @Override
    public int getWidth() {
        return 0;
    }

    @Override
    public int getHeight() {
        return 0;
    }

    @Override
    public void drag() {
        dx = x;
        dy = y;
    }

    @Override
    public void moveTo(int x, int y) {
        this.x = dx + x;
        this.y = dy + y;
    }

    @Override
    public void moveBy(int x, int y) {
        this.x += x;
        this.y += y;
    }

    @Override
    public void drop() {
        this.x = dx;
        this.y = dy;
    }

    @Override
    public boolean isInsideBounds(int x, int y) {
        return x > getX() && x < (getX() + getWidth()) &&
                y > getY() && y < (getY() + getHeight());
    }

    @Override
    public Color getColor() {
        return color;
    }

    @Override
    public void setColor(Color color) {
        this.color = color;
    }

    @Override
    public void select() {
        selected = true;
    }

    @Override
    public void unSelect() {
        selected = false;
    }

    @Override
    public boolean isSelected() {
        return selected;
    }

    void enableSelectionStyle(Graphics graphics) {
        graphics.setColor(Color.LIGHT_GRAY);

        Graphics2D g2 = (Graphics2D) graphics;
        float dash1[] = {2.0f};
        g2.setStroke(new BasicStroke(1.0f,
                BasicStroke.CAP_BUTT,
                BasicStroke.JOIN_MITER,
                2.0f, dash1, 0.0f));
    }

    void disableSelectionStyle(Graphics graphics) {
        graphics.setColor(color);
        Graphics2D g2 = (Graphics2D) graphics;
        g2.setStroke(new BasicStroke());
    }

    @Override
    public void paint(Graphics graphics) {
        if (isSelected()) {
            enableSelectionStyle(graphics);
        }
        else {
            disableSelectionStyle(graphics);
        }

        // ...
    }
}
 shapes/Circle.java
package refactoring_guru.memento.example.shapes;

import java.awt.*;

public class Circle extends BaseShape {
    private int radius;

    public Circle(int x, int y, int radius, Color color) {
        super(x, y, color);
        this.radius = radius;
    }

    @Override
    public int getWidth() {
        return radius * 2;
    }

    @Override
    public int getHeight() {
        return radius * 2;
    }

    @Override
    public void paint(Graphics graphics) {
        super.paint(graphics);
        graphics.drawOval(x, y, getWidth() - 1, getHeight() - 1);
    }
}
 shapes/Dot.java
package refactoring_guru.memento.example.shapes;

import java.awt.*;

public class Dot extends BaseShape {
    private final int DOT_SIZE = 3;

    public Dot(int x, int y, Color color) {
        super(x, y, color);
    }

    @Override
    public int getWidth() {
        return DOT_SIZE;
    }

    @Override
    public int getHeight() {
        return DOT_SIZE;
    }

    @Override
    public void paint(Graphics graphics) {
        super.paint(graphics);
        graphics.fillRect(x - 1, y - 1, getWidth(), getHeight());
    }
}
 shapes/Rectangle.java
package refactoring_guru.memento.example.shapes;

import java.awt.*;

public class Rectangle extends BaseShape {
    private int width;
    private int height;

    public Rectangle(int x, int y, int width, int height, Color color) {
        super(x, y, color);
        this.width = width;
        this.height = height;
    }

    @Override
    public int getWidth() {
        return width;
    }

    @Override
    public int getHeight() {
        return height;
    }

    @Override
    public void paint(Graphics graphics) {
        super.paint(graphics);
        graphics.drawRect(x, y, getWidth() - 1, getHeight() - 1);
    }
}
 shapes/CompoundShape.java
package refactoring_guru.memento.example.shapes;

import java.awt.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public class CompoundShape extends BaseShape {
    private List<Shape> children = new ArrayList<>();

    public CompoundShape(Shape... components) {
        super(0, 0, Color.BLACK);
        add(components);
    }

    public void add(Shape component) {
        children.add(component);
    }

    public void add(Shape... components) {
        children.addAll(Arrays.asList(components));
    }

    public void remove(Shape child) {
        children.remove(child);
    }

    public void remove(Shape... components) {
        children.removeAll(Arrays.asList(components));
    }

    public void clear() {
        children.clear();
    }

    @Override
    public int getX() {
        if (children.size() == 0) {
            return 0;
        }
        int x = children.get(0).getX();
        for (Shape child : children) {
            if (child.getX() < x) {
                x = child.getX();
            }
        }
        return x;
    }

    @Override
    public int getY() {
        if (children.size() == 0) {
            return 0;
        }
        int y = children.get(0).getY();
        for (Shape child : children) {
            if (child.getY() < y) {
                y = child.getY();
            }
        }
        return y;
    }

    @Override
    public int getWidth() {
        int maxWidth = 0;
        int x = getX();
        for (Shape child : children) {
            int childsRelativeX = child.getX() - x;
            int childWidth = childsRelativeX + child.getWidth();
            if (childWidth > maxWidth) {
                maxWidth = childWidth;
            }
        }
        return maxWidth;
    }

    @Override
    public int getHeight() {
        int maxHeight = 0;
        int y = getY();
        for (Shape child : children) {
            int childsRelativeY = child.getY() - y;
            int childHeight = childsRelativeY + child.getHeight();
            if (childHeight > maxHeight) {
                maxHeight = childHeight;
            }
        }
        return maxHeight;
    }

    @Override
    public void drag() {
        for (Shape child : children) {
            child.drag();
        }
    }

    @Override
    public void drop() {
        for (Shape child : children) {
            child.drop();
        }
    }

    @Override
    public void moveTo(int x, int y) {
        for (Shape child : children) {
            child.moveTo(x, y);
        }
    }

    @Override
    public void moveBy(int x, int y) {
        for (Shape child : children) {
            child.moveBy(x, y);
        }
    }

    @Override
    public boolean isInsideBounds(int x, int y) {
        for (Shape child : children) {
            if (child.isInsideBounds(x, y)) {
                return true;
            }
        }
        return false;
    }

    @Override
    public void setColor(Color color) {
        super.setColor(color);
        for (Shape child : children) {
            child.setColor(color);
        }
    }

    @Override
    public void unSelect() {
        super.unSelect();
        for (Shape child : children) {
            child.unSelect();
        }
    }

    public Shape getChildAt(int x, int y) {
        for (Shape child : children) {
            if (child.isInsideBounds(x, y)) {
                return child;
            }
        }
        return null;
    }

    public boolean selectChildAt(int x, int y) {
        Shape child = getChildAt(x,y);
        if (child != null) {
            child.select();
            return true;
        }
        return false;
    }

    public List<Shape> getSelected() {
        List<Shape> selected = new ArrayList<>();
        for (Shape child : children) {
            if (child.isSelected()) {
                selected.add(child);
            }
        }
        return selected;
    }

    @Override
    public void paint(Graphics graphics) {
        if (isSelected()) {
            enableSelectionStyle(graphics);
            graphics.drawRect(getX() - 1, getY() - 1, getWidth() + 1, getHeight() + 1);
            disableSelectionStyle(graphics);
        }

        for (Shape child : children) {
            child.paint(graphics);
        }
    }
}
 Demo.java: شيفرة البدء (Initialization Code)
package refactoring_guru.memento.example;

import refactoring_guru.memento.example.editor.Editor;
import refactoring_guru.memento.example.shapes.Circle;
import refactoring_guru.memento.example.shapes.CompoundShape;
import refactoring_guru.memento.example.shapes.Dot;
import refactoring_guru.memento.example.shapes.Rectangle;

import java.awt.*;

public class Demo {
    public static void main(String[] args) {
        Editor editor = new Editor();
        editor.loadShapes(
                new Circle(10, 10, 10, Color.BLUE),

                new CompoundShape(
                        new Circle(110, 110, 50, Color.RED),
                        new Dot(160, 160, Color.RED)
                ),

                new CompoundShape(
                        new Rectangle(250, 250, 100, 100, Color.GREEN),
                        new Dot(240, 240, Color.GREEN),
                        new Dot(240, 360, Color.GREEN),
                        new Dot(360, 360, Color.GREEN),
                        new Dot(360, 240, Color.GREEN)
                )
        );
    }
}
 OutputDemo.png: لقطة صورة

الصورة.

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

المستوى: ★ ★ ★

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

أمثلة الاستخدام: يمكن تحقيق مبدأ نمط التذكِرة باستخدام التسلسل (serialization) الذي يشيع في لغة #C، ورغم أنه ليس الطريقة الوحيدة ولا الأفضل لأخذ لقطات snapshot من حالة كائن ما، إلا أنها تسمح بتخزين نسخ احتياطية من الحالة مع حماية هيكل البادئ (originator) من الكائنات الأخرى.

مثال تصوري

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

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

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

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;

namespace RefactoringGuru.DesignPatterns.Memento.Conceptual
{
    // The Originator holds some important state that may change over time. It
    // also defines a method for saving the state inside a memento and another
    // method for restoring the state from it.
    class Originator
    {
        // For the sake of simplicity, the originator's state is stored inside a
        // single variable.
        private string _state;

        public Originator(string state)
        {
            this._state = state;
            Console.WriteLine("Originator: My initial state is: " + state);
        }

        // The Originator's business logic may affect its internal state.
        // Therefore, the client should backup the state before launching
        // methods of the business logic via the save() method.
        public void DoSomething()
        {
            Console.WriteLine("Originator: I'm doing something important.");
            this._state = this.GenerateRandomString(30);
            Console.WriteLine($"Originator: and my state has changed to: {_state}");
        }

        private string GenerateRandomString(int length = 10)
        {
            string allowedSymbols = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
            string result = string.Empty;

            while (length > 0)
            {
                result += allowedSymbols[new Random().Next(0, allowedSymbols.Length)];

                Thread.Sleep(12);

                length--;
            }

            return result;
        }

        // Saves the current state inside a memento.
        public IMemento Save()
        {
            return new ConcreteMemento(this._state);
        }

        // Restores the Originator's state from a memento object.
        public void Restore(IMemento memento)
        {
            if (!(memento is ConcreteMemento))
            {
                throw new Exception("Unknown memento class " + memento.ToString());
            }

            this._state = memento.GetState();
            Console.Write($"Originator: My state has changed to: {_state}");
        }
    }

    // The Memento interface provides a way to retrieve the memento's metadata,
    // such as creation date or name. However, it doesn't expose the
    // Originator's state.
    public interface IMemento
    {
        string GetName();

        string GetState();

        DateTime GetDate();
    }

    // The Concrete Memento contains the infrastructure for storing the
    // Originator's state.
    class ConcreteMemento : IMemento
    {
        private string _state;

        private DateTime _date;

        public ConcreteMemento(string state)
        {
            this._state = state;
            this._date = DateTime.Now;
        }

        // The Originator uses this method when restoring its state.
        public string GetState()
        {
            return this._state;
        }
        
        // The rest of the methods are used by the Caretaker to display
        // metadata.
        public string GetName()
        {
            return $"{this._date} / ({this._state.Substring(0, 9)})...";
        }

        public DateTime GetDate()
        {
            return this._date;
        }
    }

    // The Caretaker doesn't depend on the Concrete Memento class. Therefore, it
    // doesn't have access to the originator's state, stored inside the memento.
    // It works with all mementos via the base Memento interface.
    class Caretaker
    {
        private List<IMemento> _mementos = new List<IMemento>();

        private Originator _originator = null;

        public Caretaker(Originator originator)
        {
            this._originator = originator;
        }

        public void Backup()
        {
            Console.WriteLine("\nCaretaker: Saving Originator's state...");
            this._mementos.Add(this._originator.Save());
        }

        public void Undo()
        {
            if (this._mementos.Count == 0)
            {
                return;
            }

            var memento = this._mementos.Last();
            this._mementos.Remove(memento);

            Console.WriteLine("Caretaker: Restoring state to: " + memento.GetName());

            try
            {
                this._originator.Restore(memento);
            }
            catch (Exception)
            {
                this.Undo();
            }
        }

        public void ShowHistory()
        {
            Console.WriteLine("Caretaker: Here's the list of mementos:");

            foreach (var memento in this._mementos)
            {
                Console.WriteLine(memento.GetName());
            }
        }
    }
    
    class Program
    {
        static void Main(string[] args)
        {
            // Client code.
            Originator originator = new Originator("Super-duper-super-puper-super.");
            Caretaker caretaker = new Caretaker(originator);

            caretaker.Backup();
            originator.DoSomething();

            caretaker.Backup();
            originator.DoSomething();

            caretaker.Backup();
            originator.DoSomething();

            Console.WriteLine();
            caretaker.ShowHistory();

            Console.WriteLine("\nClient: Now, let's rollback!\n");
            caretaker.Undo();

            Console.WriteLine("\n\nClient: Once more!\n");
            caretaker.Undo();

            Console.WriteLine();
        }
    }
}

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

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

المستوى: ★ ★ ★

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

أمثلة الاستخدام: يمكن تحقيق مبدأ نمط التذكِرة باستخدام التسلسل (serialization) الذي يشيع في لغة بايثون، ورغم أنه ليس الطريقة الوحيدة ولا الأفضل لأخذ لقطات snapshot من حالة كائن ما، إلا أنها تسمح بتخزين نسخ احتياطية من الحالة مع حماية هيكل البادئ (originator) من الكائنات الأخرى.

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

المستوى: ★ ★ ★

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

أمثلة الاستخدام: يمكن تحقيق مبدأ نمط التذكِرة باستخدام التسلسل (serialization) الذي يشيع في لغة روبي، ورغم أنه ليس الطريقة الوحيدة ولا الأفضل لأخذ لقطات snapshot من حالة كائن ما، إلا أنها تسمح بتخزين نسخ احتياطية من الحالة مع حماية هيكل البادئ (originator) من الكائنات الأخرى.

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

المستوى: ★ ★ ★

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

أمثلة الاستخدام: يمكن تحقيق مبدأ نمط التذكِرة باستخدام التسلسل (serialization) الذي يشيع في لغة Swift، ورغم أنه ليس الطريقة الوحيدة ولا الأفضل لأخذ لقطات snapshot من حالة كائن ما، إلا أنها تسمح بتخزين نسخ احتياطية من الحالة مع حماية هيكل البادئ (originator) من الكائنات الأخرى.

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

المستوى: ★ ★ ★

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

أمثلة الاستخدام: يمكن تحقيق مبدأ نمط التذكِرة باستخدام التسلسل (serialization) الذي يشيع في لغة TypeScript، ورغم أنه ليس الطريقة الوحيدة ولا الأفضل لأخذ لقطات snapshot من حالة كائن ما، إلا أنها تسمح بتخزين نسخ احتياطية من الحالة مع حماية هيكل البادئ (originator) من الكائنات الأخرى.

انظر أيضًا

مصادر