نمط التذكرة Memento

من موسوعة حسوب
اذهب إلى التنقل اذهب إلى البحث

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

المشكلة

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

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

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

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

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

(ش.2) كيف تنسخ حالة الكائن الخاصة (private)

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

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

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

الحل

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

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

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

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

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

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

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

البُنية

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

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

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

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

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

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

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

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

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

مثال توضيحي

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

(ش.7) حفظ لقطات من حالة المحرر النصي.

تتصرف كائنات الأمر كنائبات (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: لقطة صورة

dpmnt.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
{
    // يحمل البادئ حالة مهمة قد تتغير مع الوقت، ويحدد ذلك البادئ أسلوبًا لحفظ 
    // الحالة داخل تذكِرة وأسلوبًا آخر لاستعادة الحالة منها.
    class Originator
    {
        // سنخزِّن حالة البادئ داخل متغير واحد بداعي التبسيط.
        private string _state;

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

        // قد يؤثر منطق عمل البادئ على حالته الداخلية، ولهذا ينبغي أن يأخذ العميل 
        // نسخة احتياطية من الحالة قبل إطلاق أساليب منطق العمل
        // save() عبر أسلوب
        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;
        }

        // يحفظ الحالة الحالية داخل تذكِرة.
        public IMemento Save()
        {
            return new ConcreteMemento(this._state);
        }

        // يسترجع حالة البادئ من كائن تذكِرة.
        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}");
        }
    }

    // توفر واجهة التذكِرة طريقة لاسترجاع البيانات الوصفية للتذكِرة، مثل تاريخ الإنشاء أو الاسم
    // لكنها لا تكشف حالة البادئ.
    public interface IMemento
    {
        string GetName();

        string GetState();

        DateTime GetDate();
    }

    // تحتوي التذكِرة الحقيقية على البنية التحتية لتخزين حالة البادئ.
    class ConcreteMemento : IMemento
    {
        private string _state;

        private DateTime _date;

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

        // يستخدم البادئ هذا الأسلوب عند استرجاع حالته.
        public string GetState()
        {
            return this._state;
        }
        
        // لعرض البيانات الوصفية (caretaker) تُستخدم بقية الأساليب بواسطة النائب.
        public string GetName()
        {
            return $"{this._date} / ({this._state.Substring(0, 9)})...";
        }

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

    // على فئة التذكِرة الحقيقية (caretaker) لا يعتمد النائب 
    // لهذا ليس لديه وصول إلى حالة البادئ المخزنة داخل التذكرة.
    // (basic 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)
        {
            // شيفرة العميل.
            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();
        }
    }
}

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

Originator: My initial state is: Super-duper-super-puper-super.

Caretaker: Saving Originator's state...
Originator: I'm doing something important.
Originator: and my state has changed to: oGyQIIatlDDWNgYYqJATTmdwnnGZQj

Caretaker: Saving Originator's state...
Originator: I'm doing something important.
Originator: and my state has changed to: jBtMDDWogzzRJbTTmEwOOhZrjjBULe

Caretaker: Saving Originator's state...
Originator: I'm doing something important.
Originator: and my state has changed to: exoHyyRkbuuNEXOhhArKccUmexPPHZ

Caretaker: Here's the list of mementos:
12.06.2018 15:52:45 / (Super-dup...)
12.06.2018 15:52:46 / (oGyQIIatl...)
12.06.2018 15:52:46 / (jBtMDDWog...)

Client: Now, let's rollback!

Caretaker: Restoring state to: 12.06.2018 15:52:46 / (jBtMDDWog...)
Originator: My state has changed to: jBtMDDWogzzRJbTTmEwOOhZrjjBULe

Client: Once more!

Caretaker: Restoring state to: 12.06.2018 15:52:46 / (oGyQIIatl...)
Originator: My state has changed to: oGyQIIatlDDWNgYYqJATTmdwnnGZQj

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

المستوى: ★ ★ ★

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

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

مثال تصوري

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

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

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

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

<?php

namespace RefactoringGuru\Memento\Conceptual;

/**
 * يحمل البادئ حالة مهمة قد تتغير مع الوقت، ويحدد ذلك البادئ أسلوبًا لحفظ 
 * الحالة داخل تذكِرة وأسلوبًا آخر لاستعادة الحالة منها.
 */
class Originator
{
    /**
     * @var string سنخزِّن حالة البادئ داخل متغير واحد بداعي التبسيط.
     */
    private $state;

    public function __construct(string $state)
    {
        $this->state = $state;
        echo "Originator: My initial state is: {$this->state}\n";
    }

    /**
     *  قد يؤثر منطق عمل البادئ على حالته الداخلية، ولهذا ينبغي أن يأخذ العميل 
     * نسخة احتياطية من الحالة قبل إطلاق أساليب منطق العمل
     * save() عبر أسلوب
     */
    public function doSomething(): void
    {
        echo "Originator: I'm doing something important.\n";
        $this->state = $this->generateRandomString(30);
        echo "Originator: and my state has changed to: {$this->state}\n";
    }

    private function generateRandomString(int $length = 10): string
    {
        return substr(
            str_shuffle(
                str_repeat(
                    $x = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ',
                    ceil($length / strlen($x))
                )
            ),
            1,
            $length,
        );
    }

    /**
     * يحفظ الحالة الحالية داخل تذكِرة.
     */
    public function save(): Memento
    {
        return new ConcreteMemento($this->state);
    }

    /**
     * يسترجع حالة البادئ من كائن تذكِرة.
     */
    public function restore(Memento $memento): void
    {
        $this->state = $memento->getState();
        echo "Originator: My state has changed to: {$this->state}\n";
    }
}

/**
 * توفر واجهة التذكِرة طريقة لاسترجاع البيانات الوصفية للتذكِرة، مثل تاريخ الإنشاء أو الاسم
 * لكنها لا تكشف حالة البادئ.
 */
interface Memento
{
    public function getName(): string;

    public function getDate(): string;
}

/**
 * تحتوي التذكِرة الحقيقية على البنية التحتية لتخزين حالة البادئ.
 */
class ConcreteMemento implements Memento
{
    private $state;

    private $date;

    public function __construct(string $state)
    {
        $this->state = $state;
        $this->date = date('Y-m-d H:i:s');
    }

    /**
     * يستخدم البادئ هذا الأسلوب عند استرجاع حالته.
     */
    public function getState(): string
    {
        return $this->state;
    }

    /**
     * لعرض البيانات الوصفية (caretaker) تُستخدم بقية الأساليب بواسطة النائب.
     */
    public function getName(): string
    {
        return $this->date . " / (" . substr($this->state, 0, 9) . "...)";
    }

    public function getDate(): string
    {
        return $this->date;
    }
}

/**
 *  على فئة التذكِرة الحقيقية (caretaker) لا يعتمد النائب 
 *  لهذا ليس لديه وصول إلى حالة البادئ المخزنة داخل التذكرة.
 *  (basic memento interface) وهو يعمل مع كل التذكِرات من خلال واجهة التذكِرة الأساسية.
 */
class Caretaker
{
    /**
     * @var Memento[]
     */
    private $mementos = [];

    /**
     * @var Originator
     */
    private $originator;

    public function __construct(Originator $originator)
    {
        $this->originator = $originator;
    }

    public function backup(): void
    {
        echo "\nCaretaker: Saving Originator's state...\n";
        $this->mementos[] = $this->originator->save();
    }

    public function undo(): void
    {
        if (!count($this->mementos)) {
            return;
        }
        $memento = array_pop($this->mementos);

        echo "Caretaker: Restoring state to: " . $memento->getName() . "\n";
        try {
            $this->originator->restore($memento);
        } catch (\Exception $e) {
            $this->undo();
        }
    }

    public function showHistory(): void
    {
        echo "Caretaker: Here's the list of mementos:\n";
        foreach ($this->mementos as $memento) {
            echo $memento->getName() . "\n";
        }
    }
}

/**
 * شيفرة العميل.
 */
$originator = new Originator("Super-duper-super-puper-super.");
$caretaker = new Caretaker($originator);

$caretaker->backup();
$originator->doSomething();

$caretaker->backup();
$originator->doSomething();

$caretaker->backup();
$originator->doSomething();

echo "\n";
$caretaker->showHistory();

echo "\nClient: Now, let's rollback!\n\n";
$caretaker->undo();

echo "\nClient: Once more!\n\n";
$caretaker->undo();

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

Originator: My initial state is: Super-duper-super-puper-super. 

Caretaker: Saving Originator's state...
Originator: I'm doing something important.
Originator: and my state has changed to: srGIngezAEboNPDjBkuvymJKUtMSFX

Caretaker: Saving Originator's state...
Originator: I'm doing something important.
Originator: and my state has changed to: UwCZQaHJOiERLlchyVuMbXNtpqTgWF

Caretaker: Saving Originator's state...
Originator: I'm doing something important.
Originator: and my state has changed to: incqsdoJXkbDUuVOvRFYyKBgfzwZCQ

Caretaker: Here's the list of mementos:
2018-06-04 14:50:39 / (Super-dup...)
2018-06-04 14:50:39 / (srGIngezA...)
2018-06-04 14:50:39 / (UwCZQaHJO...)

Client: Now, let's rollback!

Caretaker: Restoring state to: 2018-06-04 14:50:39 / (UwCZQaHJO...)
Originator: My state has changed to: UwCZQaHJOiERLlchyVuMbXNtpqTgWF

Client: Once more!

Caretaker: Restoring state to: 2018-06-04 14:50:39 / (srGIngezA...)
Originator: My state has changed to: srGIngezAEboNPDjBkuvymJKUtMSFX

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

المستوى: ★ ★ ★

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

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

مثال تصوري

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

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

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

from __future__ import annotations
from abc import ABC, abstractmethod
from datetime import datetime
from random import sample
from string import ascii_letters, digits


class Originator():
    """
    يحمل البادئ حالة مهمة قد تتغير مع الوقت، ويحدد ذلك البادئ أسلوبًا لحفظ 
    الحالة داخل تذكِرة وأسلوبًا آخر لاستعادة الحالة منها.
    """

    _state = None
    """
    سنخزِّن حالة البادئ داخل متغير واحد بداعي التبسيط.
    """

    def __init__(self, state: str) -> None:
        self._state = state
        print(f"Originator: My initial state is: {self._state}")

    def do_something(self) -> None:
        """
        قد يؤثر منطق عمل البادئ على حالته الداخلية، ولهذا ينبغي أن يأخذ العميل 
        نسخة احتياطية من الحالة قبل إطلاق أساليب منطق العمل
        save() عبر أسلوب
        """

        print("Originator: I'm doing something important.")
        self._state = self._generate_random_string(30)
        print(f"Originator: and my state has changed to: {self._state}")

    def _generate_random_string(self, length: int = 10) -> None:
        return "".join(sample(ascii_letters, length))

    def save(self) -> Memento:
        """
        يحفظ الحالة الحالية داخل تذكِرة.
        """

        return ConcreteMemento(self._state)

    def restore(self, memento: Memento) -> None:
        """
        يسترجع حالة البادئ من كائن تذكِرة.
        """

        self._state = memento.get_state()
        print(f"Originator: My state has changed to: {self._state}")


class Memento(ABC):
    """
    توفر واجهة التذكِرة طريقة لاسترجاع البيانات الوصفية للتذكِرة، مثل تاريخ الإنشاء أو الاسم
    لكنها لا تكشف حالة البادئ.
    """

    @abstractmethod
    def get_name(self) -> str:
        pass

    @abstractmethod
    def get_date(self) -> str:
        pass


class ConcreteMemento(Memento):
    def __init__(self, state: str) -> None:
        self._state = state
        self._date = str(datetime.now())[:19]

    def get_state(self) -> str:
        """
        يستخدم البادئ هذا الأسلوب عند استرجاع حالته.
        """
        return self._state

    def get_name(self) -> str:
        """
        لعرض البيانات الوصفية (caretaker) تُستخدم بقية الأساليب بواسطة النائب.
        """

        return f"{self._date} / ({self._state[0:9]}...)"

    def get_date(self) -> str:
        return self._date


class Caretaker():
    """
    على فئة التذكِرة الحقيقية (caretaker) لا يعتمد النائب
    لهذا ليس لديه وصول إلى حالة البادئ المخزنة داخل التذكرة.
    (basic memento interface) وهو يعمل مع كل التذكِرات من خلال واجهة التذكِرة الأساسية.
    """

    def __init__(self, originator: Originator) -> None:
        self._mementos = []
        self._originator = originator

    def backup(self) -> None:
        print("\nCaretaker: Saving Originator's state...")
        self._mementos.append(self._originator.save())

    def undo(self) -> None:
        if not len(self._mementos):
            return

        memento = self._mementos.pop()
        print(f"Caretaker: Restoring state to: {memento.get_name()}")
        try:
            self._originator.restore(memento)
        except Exception:
            self.undo()

    def show_history(self) -> None:
        print("Caretaker: Here's the list of mementos:")
        for memento in self._mementos:
            print(memento.get_name())


if __name__ == "__main__":
    originator = Originator("Super-duper-super-puper-super.")
    caretaker = Caretaker(originator)

    caretaker.backup()
    originator.do_something()

    caretaker.backup()
    originator.do_something()

    caretaker.backup()
    originator.do_something()

    print()
    caretaker.show_history()

    print("\nClient: Now, let's rollback!\n")
    caretaker.undo()

    print("\nClient: Once more!\n")
    caretaker.undo()

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

Originator: My initial state is: Super-duper-super-puper-super.

Caretaker: Saving Originator's state...
Originator: I'm doing something important.
Originator: and my state has changed to: wQAehHYOqVSlpEXjyIcgobrxsZUnat

Caretaker: Saving Originator's state...
Originator: I'm doing something important.
Originator: and my state has changed to: lHxNORKcsgMWYnJqoXjVCbQLEIeiSp

Caretaker: Saving Originator's state...
Originator: I'm doing something important.
Originator: and my state has changed to: cvIYsRilNOtwynaKdEZpDCQkFAXVMf

Caretaker: Here's the list of mementos:
2019-01-26 21:11:24 / (Super-dup...)
2019-01-26 21:11:24 / (wQAehHYOq...)
2019-01-26 21:11:24 / (lHxNORKcs...)

Client: Now, let's rollback!

Caretaker: Restoring state to: 2019-01-26 21:11:24 / (lHxNORKcs...)
Originator: My state has changed to: lHxNORKcsgMWYnJqoXjVCbQLEIeiSp

Client: Once more!

Caretaker: Restoring state to: 2019-01-26 21:11:24 / (wQAehHYOq...)
Originator: My state has changed to: wQAehHYOqVSlpEXjyIcgobrxsZUnat

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

المستوى: ★ ★ ★

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

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

مثال تصوري

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

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

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

# يحمل البادئ حالة مهمة قد تتغير مع الوقت، ويحدد ذلك البادئ أسلوبًا لحفظ 
# الحالة داخل تذكِرة وأسلوبًا آخر لاستعادة الحالة منها.
class Originator
  # سنخزِّن حالة البادئ داخل متغير واحد بداعي التبسيط.
  attr_accessor :state
  private :state

  # @param [String] state
  def initialize(state)
    @state = state
    puts "Originator: My initial state is: #{@state}"
  end

  # قد يؤثر منطق عمل البادئ على حالته الداخلية، ولهذا ينبغي أن يأخذ العميل
  # نسخة احتياطية من الحالة قبل إطلاق أساليب منطق العمل
  # save() عبر أسلوب
  def do_something
    puts 'Originator: I\'m doing something important.'
    @state = generate_random_string(30)
    puts "Originator: and my state has changed to: #{@state}"
  end

  private def generate_random_string(length = 10)
    ascii_letters = [*'a'..'z', *'A'..'Z']
    (0...length).map { ascii_letters.sample }.join
  end

  # يحفظ الحالة الحالية داخل تذكِرة.
  def save
    ConcreteMemento.new(@state)
  end

  # يسترجع حالة البادئ من كائن تذكِرة.
  def restore(memento)
    @state = memento.state
    puts "Originator: My state has changed to: #{@state}"
  end
end

# توفر واجهة التذكِرة طريقة لاسترجاع البيانات الوصفية للتذكِرة، مثل تاريخ الإنشاء أو الاسم
# لكنها لا تكشف حالة البادئ.
class Memento
  # @abstract
  #
  # @return [String]
  def name
    raise NotImplementedError, "#{self.class} has not implemented method '#{__method__}'"
  end

  # @abstract
  #
  # @return [String]
  def date
    raise NotImplementedError, "#{self.class} has not implemented method '#{__method__}'"
  end
end

class ConcreteMemento < Memento
  # @param [String] state
  def initialize(state)
    @state = state
    @date = Time.now.strftime('%F %T')
  end

  # يستخدم البادئ هذا الأسلوب عند استرجاع حالته.
  attr_reader :state

  # لعرض البيانات الوصفية (caretaker) تُستخدم بقية الأساليب بواسطة النائب.
  def name
    "#{@date} / (#{@state[0, 9]}...)"
  end

  # @return [String]
  attr_reader :date
end

# على فئة التذكِرة الحقيقية (caretaker) لا يعتمد النائب
# لهذا ليس لديه وصول إلى حالة البادئ المخزنة داخل التذكرة.
# (basic memento interface) وهو يعمل مع كل التذكِرات من خلال واجهة التذكِرة الأساسية.
works with all mementos via the base Memento interface.
class Caretaker
  # @param [Originator] originator
  def initialize(originator)
    @mementos = []
    @originator = originator
  end

  def backup
    puts "\nCaretaker: Saving Originator's state..."
    @mementos << @originator.save
  end

  def undo
    return if @mementos.empty?

    memento = @mementos.pop
    puts "Caretaker: Restoring state to: #{memento.name}"

    begin
      @originator.restore(memento)
    rescue StandardError
      undo
    end
  end

  def show_history
    puts 'Caretaker: Here\'s the list of mementos:'

    @mementos.each { |memento| puts memento.name }
  end
end

originator = Originator.new('Super-duper-super-puper-super.')
caretaker = Caretaker.new(originator)

caretaker.backup
originator.do_something

caretaker.backup
originator.do_something

caretaker.backup
originator.do_something

puts "\n"
caretaker.show_history

puts "\nClient: Now, let's rollback!\n"
caretaker.undo

puts "\nClient: Once more!\n"
caretaker.undo

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

Originator: My initial state is: Super-duper-super-puper-super.

Caretaker: Saving Originator's state...
Originator: I'm doing something important.
Originator: and my state has changed to: BFDECxFORrlPIMDCDfQuRwHcuvpbTv

Caretaker: Saving Originator's state...
Originator: I'm doing something important.
Originator: and my state has changed to: xjyrOYzoBYIPYHhGKlvbQrsRvKSRzY

Caretaker: Saving Originator's state...
Originator: I'm doing something important.
Originator: and my state has changed to: xHfPozLWyhsamFVUUPfhIaGBhaBBvK

Caretaker: Here's the list of mementos:
2019-03-06 23:04:13 / (Super-dup...)
2019-03-06 23:04:13 / (BFDECxFOR...)
2019-03-06 23:04:13 / (xjyrOYzoB...)

Client: Now, let's rollback!
Caretaker: Restoring state to: 2019-03-06 23:04:13 / (xjyrOYzoB...)
Originator: My state has changed to: xjyrOYzoBYIPYHhGKlvbQrsRvKSRzY

Client: Once more!
Caretaker: Restoring state to: 2019-03-06 23:04:13 / (BFDECxFOR...)
Originator: My state has changed to: BFDECxFORrlPIMDCDfQuRwHcuvpbTv

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

المستوى: ★ ★ ★

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

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

مثال تصوري

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

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

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

import XCTest

/// يحمل البادئ حالة مهمة قد تتغير مع الوقت، ويحدد ذلك البادئ أسلوبًا لحفظ 
/// الحالة داخل تذكِرة وأسلوبًا آخر لاستعادة الحالة منها.
class Originator {

    /// سنخزِّن حالة البادئ داخل متغير واحد بداعي التبسيط.
    private var state: String

    init(state: String) {
        self.state = state
        print("Originator: My initial state is: \(state)")
    }

    /// قد يؤثر منطق عمل البادئ على حالته الداخلية، ولهذا ينبغي أن يأخذ 
    /// العميل نسخة احتياطية من الحالة قبل إطلاق أساليب منطق العمل.
    /// save() عبر أسلوب
    func doSomething() {
        print("Originator: I'm doing something important.")
        state = generateRandomString()
        print("Originator: and my state has changed to: \(state)")
    }

    private func generateRandomString() -> String {
        return String(UUID().uuidString.suffix(4))
    }

    /// يحفظ الحالة الحالية داخل تذكِرة.
    func save() -> Memento {
        return ConcreteMemento(state: state)
    }

    /// يسترجع حالة البادئ من كائن تذكِرة.
    func restore(memento: Memento) {
        guard let memento = memento as? ConcreteMemento else { return }
        self.state = memento.state
        print("Originator: My state has changed to: \(state)")
    }
}

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

    var name: String { get }
    var date: Date { get }
}

/// تحتوي التذكِرة الحقيقية على البنية التحتية لتخزين حالة البادئ.
class ConcreteMemento: Memento {

    /// يستخدم البادئ هذا الأسلوب عند استرجاع حالته.
    private(set) var state: String
    private(set) var date: Date

    init(state: String) {
        self.state = state
        self.date = Date()
    }

    /// لعرض البيانات الوصفية (caretaker) تُستخدم بقية الأساليب بواسطة النائب.
    var name: String { return state + " " + date.description.suffix(14).prefix(8) }
}

/// على فئة التذكِرة الحقيقية (caretaker) لا يعتمد النائب
/// لهذا ليس لديه وصول إلى حالة البادئ المخزنة داخل التذكرة.
/// وهو يعمل مع كل التذكِرات من خلال
/// (basic memento interface) واجهة التذكِرة الأساسية
class Caretaker {

    private lazy var mementos = [Memento]()
    private var originator: Originator

    init(originator: Originator) {
        self.originator = originator
    }

    func backup() {
        print("\nCaretaker: Saving Originator's state...\n")
        mementos.append(originator.save())
    }

    func undo() {

        guard !mementos.isEmpty else { return }
        let removedMemento = mementos.removeLast()

        print("Caretaker: Restoring state to: " + removedMemento.name)
        originator.restore(memento: removedMemento)
    }

    func showHistory() {
        print("Caretaker: Here's the list of mementos:\n")
        mementos.forEach({ print($0.name) })
    }
}

/// لنرى الآن كيف سيعمل كل ذلك.
class MementoConceptual: XCTestCase {

    func testMementoConceptual() {

        let originator = Originator(state: "Super-duper-super-puper-super.")
        let caretaker = Caretaker(originator: originator)

        caretaker.backup()
        originator.doSomething()

        caretaker.backup()
        originator.doSomething()

        caretaker.backup()
        originator.doSomething()

        print("\n")
        caretaker.showHistory()

        print("\nClient: Now, let's rollback!\n\n")
        caretaker.undo()

        print("\nClient: Once more!\n\n")
        caretaker.undo()
    }
}

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

Originator: My initial state is: Super-duper-super-puper-super.

Caretaker: Saving Originator's state...

Originator: I'm doing something important.
Originator: and my state has changed to: 1923

Caretaker: Saving Originator's state...

Originator: I'm doing something important.
Originator: and my state has changed to: 74FB

Caretaker: Saving Originator's state...

Originator: I'm doing something important.
Originator: and my state has changed to: 3681


Caretaker: Here's the list of mementos:

Super-duper-super-puper-super. 11:45:44
1923 11:45:44
74FB 11:45:44

Client: Now, let's rollback!


Caretaker: Restoring state to: 74FB 11:45:44
Originator: My state has changed to: 74FB

Client: Once more!


Caretaker: Restoring state to: 1923 11:45:44
Originator: My state has changed to: 1923

مثال واقعي

 Example.swift: مثال واقعي

import XCTest

class MementoRealWorld: XCTestCase {

    /// يُستخدم كلًا من نمط الحالة والأمر معًا في الغالب في حالة وجوب
    /// استعادة الحالة السابقة للكائن، بسبب فشل عملية ما.
    ///
    /// كبديل UndoManager لاحظ أنه يمكن استخدام.

    func test() {

        let textView = UITextView()
        let undoStack = UndoStack(textView)

        textView.text = "First Change"
        undoStack.save()

        textView.text = "Second Change"
        undoStack.save()

        textView.text = (textView.text ?? "") + " & Third Change"
        textView.textColor = .red
        undoStack.save()

        print(undoStack)

        print("Client: Perform Undo operation 2 times\n")
        undoStack.undo()
        undoStack.undo()

        print(undoStack)
    }
}

class UndoStack: CustomStringConvertible {

    private lazy var mementos = [Memento]()
    private let textView: UITextView

    init(_ textView: UITextView) {
        self.textView = textView
    }

    func save() {
        mementos.append(textView.memento)
    }

    func undo() {
        guard !mementos.isEmpty else { return }
        textView.restore(with: mementos.removeLast())
    }

    var description: String {
        return mementos.reduce("", { $0 + $1.description })
    }
}

protocol Memento: CustomStringConvertible {

    var text: String { get }
    var date: Date { get }
}

extension UITextView {

    var memento: Memento {
        return TextViewMemento(text: text,
                               textColor: textColor,
                               selectedRange: selectedRange)
    }

    func restore(with memento: Memento) {
        guard let textViewMemento = memento as? TextViewMemento else { return }

        text = textViewMemento.text
        textColor = textViewMemento.textColor
        selectedRange = textViewMemento.selectedRange
    }

    struct TextViewMemento: Memento {

        let text: String
        let date = Date()

        let textColor: UIColor?
        let selectedRange: NSRange

        var description: String {
            let time = Calendar.current.dateComponents([.hour, .minute, .second, .nanosecond],
                                                       from: date)
            let color = String(describing: textColor)
            return "Text: \(text)\n" + "Date: \(time.description)\n"
                + "Color: \(color)\n" + "Range: \(selectedRange)\n\n"
        }
    }
}

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

Text: First Change
Date: hour: 12 minute: 21 second: 50 nanosecond: 821737051 isLeapMonth: false
Color: nil
Range: {12, 0}

Text: Second Change
Date: hour: 12 minute: 21 second: 50 nanosecond: 826483011 isLeapMonth: false
Color: nil
Range: {13, 0}

Text: Second Change & Third Change
Date: hour: 12 minute: 21 second: 50 nanosecond: 829187035 isLeapMonth: false
Color: Optional(UIExtendedSRGBColorSpace 1 0 0 1)
Range: {28, 0}


Client: Perform Undo operation 2 times

Text: First Change
Date: hour: 12 minute: 21 second: 50 nanosecond: 821737051 isLeapMonth: false
Color: nil
Range: {12, 0}

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

المستوى: ★ ★ ★

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

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

مثال تصوري

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

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

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

/**
 * يحمل البادئ حالة مهمة قد تتغير مع الوقت، ويحدد ذلك البادئ أسلوبًا لحفظ 
 * الحالة داخل تذكِرة وأسلوبًا آخر لاستعادة الحالة منها.
 */
class Originator {
    /**
     * سنخزِّن حالة البادئ داخل متغير واحد بداعي التبسيط.
     */
    private state: string;

    constructor(state: string) {
        this.state = state;
        console.log(`Originator: My initial state is: ${state}`);
    }

    /**
     * قد يؤثر منطق عمل البادئ على حالته الداخلية، ولهذا ينبغي أن يأخذ العميل 
     * نسخة احتياطية من الحالة قبل إطلاق أساليب منطق العمل.
     * save() عبر أسلوب
     */
    public doSomething(): void {
        console.log('Originator: I\'m doing something important.');
        this.state = this.generateRandomString(30);
        console.log(`Originator: and my state has changed to: ${this.state}`);
    }

    private generateRandomString(length: number = 10): string {
        const charSet = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';

        return Array
            .apply(null, { length })
            .map(() => charSet.charAt(Math.floor(Math.random() * charSet.length)))
            .join('');
    }

    /**
     * يحفظ الحالة الحالية داخل تذكِرة.
     */
    public save(): Memento {
        return new ConcreteMemento(this.state);
    }

    /**
     * يسترجع حالة البادئ من كائن تذكِرة.
     */
    public restore(memento: Memento): void {
        this.state = memento.getState();
        console.log(`Originator: My state has changed to: ${this.state}`);
    }
}

/**
 * توفر واجهة التذكِرة طريقة لاسترجاع البيانات الوصفية للتذكِرة، مثل تاريخ 
 * الإنشاء أو الاسم، لكنها لا تكشف حالة البادئ.
 */
interface Memento {
    getState(): string;

    getName(): string;

    getDate(): string;
}

/**
 * تحتوي التذكِرة الحقيقية على البنية التحتية لتخزين حالة البادئ.
 */
class ConcreteMemento implements Memento {
    private state: string;

    private date: string;

    constructor(state: string) {
        this.state = state;
        this.date = new Date().toISOString().slice(0, 19).replace('T', ' ');
    }

    /**
     * يستخدم البادئ هذا الأسلوب عند استرجاع حالته.
     */
    public getState(): string {
        return this.state;
    }

    /**
     * لعرض البيانات الوصفية (caretaker) تُستخدم بقية الأساليب بواسطة النائب.
     */
    public getName(): string {
        return `${this.date} / (${this.state.substr(0, 9)}...)`;
    }

    public getDate(): string {
        return this.date;
    }
}

/**
 * على فئة التذكِرة الحقيقية (caretaker) لا يعتمد النائب 
 * لهذا ليس لديه وصول إلى حالة البادئ المخزنة داخل التذكرة.
 * وهو يعمل مع كل التذكِرات من خلال 
 * (basic memento interface) واجهة التذكِرة الأساسية.
 */
class Caretaker {
    private mementos: Memento[] = [];

    private originator: Originator;

    constructor(originator: Originator) {
        this.originator = originator;
    }

    public backup(): void {
        console.log('\nCaretaker: Saving Originator\'s state...');
        this.mementos.push(this.originator.save());
    }

    public undo(): void {
        if (!this.mementos.length) {
            return;
        }
        const memento = this.mementos.pop();

        console.log(`Caretaker: Restoring state to: ${memento.getName()}`);
        this.originator.restore(memento);
    }

    public showHistory(): void {
        console.log('Caretaker: Here\'s the list of mementos:');
        for (const memento of this.mementos) {
            console.log(memento.getName());
        }
    }
}

/**
 * شيفرة العميل.
 */
const originator = new Originator('Super-duper-super-puper-super.');
const caretaker = new Caretaker(originator);

caretaker.backup();
originator.doSomething();

caretaker.backup();
originator.doSomething();

caretaker.backup();
originator.doSomething();

console.log('');
caretaker.showHistory();

console.log('\nClient: Now, let\'s rollback!\n');
caretaker.undo();

console.log('\nClient: Once more!\n');
caretaker.undo();

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

Originator: My initial state is: Super-duper-super-puper-super.

Caretaker: Saving Originator's state...
Originator: I'm doing something important.
Originator: and my state has changed to: qXqxgTcLSCeLYdcgElOghOFhPGfMxo

Caretaker: Saving Originator's state...
Originator: I'm doing something important.
Originator: and my state has changed to: iaVCJVryJwWwbipieensfodeMSWvUY

Caretaker: Saving Originator's state...
Originator: I'm doing something important.
Originator: and my state has changed to: oSUxsOCiZEnohBMQEjwnPWJLGnwGmy

Caretaker: Here's the list of mementos:
2019-02-17 15:14:05 / (Super-dup...)
2019-02-17 15:14:05 / (qXqxgTcLS...)
2019-02-17 15:14:05 / (iaVCJVryJ...)

Client: Now, let's rollback!

Caretaker: Restoring state to: 2019-02-17 15:14:05 / (iaVCJVryJ...)
Originator: My state has changed to: iaVCJVryJwWwbipieensfodeMSWvUY

Client: Once more!

Caretaker: Restoring state to: 2019-02-17 15:14:05 / (qXqxgTcLS...)
Originator: My state has changed to: qXqxgTcLSCeLYdcgElOghOFhPGfMxo

انظر أيضًا

مصادر