نمط التذكرة 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 وجافا.
الصورة.
- تستطيع فئة البادئ (originator) أن تنتج لقطات لحالتها إضافة إلى استرجاع حالتها من لقطات مسجَّلة من قبل عند الحاجة.
- التذكِرة (memento) هي كائنُ قيمةٍ يتصرف كلقطة من حالة البادئ، ومن الشائع جعل التذكِرة ثابتة لا تقبل التغيير، وتُمرَّر البيانات إليها مرة واحدة من خلال المنشئ.
- لا يدرك النائب (caretaker) متى ولماذا يلتقط حالة البادئ فحسب، بل يعرف كذلك متى يجب أن تُسترجَع الحالة. ويستطيع أن يحتفظ بسجل لتاريخ البادئ عبر تخزين مكدَّس من التذكِرات، وعند حاجة البادئ إلى العودة إلى حالة سابقة فإن النائب يجلب أحدث تذكِرة من المكدَّس ويمررها إلى أسلوب استعادة البادئ.
- في هذا التطبيق للنمط فإن فئة التذكرة تكون محتواة بداخل البادئ، ويسمح هذا للبادئ بالوصول إلى الحقول والأساليب الخاصة بالتذكِرة رغم أنها مصرَّحٌ عنها بأنها خاصة (private). ومن الناحية الأخرى فإن النائب ليس لديه إلا وصول محدود جدًا إلى حقول التذكِرة وأساليبها، مما يسمح له بتخزين التذكِرات في مكدَّس دون القدرة على تغيير حالتها أو التعديل فيها.
تطبيق مبني على واجهة وسيطة
هناك تطبيق بديل يناسب لغات البرمجة التي لا تدعم الفئات المتداخلة (مثل PHP).
الصورة.
- في غياب الفئات المتداخلة فيمكنك تقييد الوصول إلى حقول التذكِرة بإنشاء نظام يجعل النائب لا يعمل مع التذكِرة إلا من خلال واجهة وسيطة مصرَّح عنها بوضوح، ولن تصرِّح إلا عن أساليب متعلقة بالبيانات الوصفية للتذكِرة.
- على الناحية الأخرى، فإن الكائنات البادئة تستطيع العمل مع كائن التذكِرة مباشرة ومن ثم الوصول مباشرة أيضًا إلى الحقول والأساليب المصرَّح عنها في فئة التذكِرة، لكن الجانب السيء في هذا المنظور هو أنك تحتاج إلى التصريح عن كل أولئك الأعضاء لتكون حالتهم عامة.
تطبيق آخر بتغليف أكثر تقييدًا
لدينا تطبيق آخر مفيد حين لا تريد ترك أدنى فرصة للفئات الأخرى في الوصول إلى حالة البادئ من خلال التذكِرة.
الصورة.
- يسمح هذا التطبيق بوجود عدة أنواع من كائنات البادئ والتذكِرة، ويعمل كل بادئ مع فئة التذكرة المتوافقة معه، ولا يكشف البادئ أو التذكِرة حالتهما لأي أحد.
- يُقيَّد النواب الآن صراحةً من تغيير الحالة المخزنة في التذكِرات، بل تصبح فئة النائب مستقلة عن البادئ لأن أسلوب الاستعادة قد حُدِّد الآن في فئة التذكِرة.
- يصبح كل كائن تذكِرة مرتبطًا بالبادئ الذي أنشأه، ويمرر البادئ نفسه إلى منشئ التذكِرة (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).
يجعل نمط التذكِرة الكائن نفسه مسؤولًا عن إنشاء لقطة لحالته، ولا يستطيع أي كائن آخر أن يقرأ ما في اللقطة، مما يجعل بيانات الحالة للكائن الأصلي آمنة ومحمية.
كيفية الاستخدام
- حدد أي الفئات ستلعب دور البادئ. من المهم أن تعرف ما إن كان البرنامج يستخدم كائنًا مركزيًا واحدًا من هذا النوع أو عدة كائنات أصغر.
- أنشئ فئة التذكِرة. وصرِّح عن مجموعة من الحقول -واحدًا واحدًا- التي تعكس (mirror) الحقول المصرَّح عنها داخل فئة البادئ.
- اجعل فئة التذكِرة غير قابلة للتغيير، ينبغي أن تقبل التذكِرة البيانات مرة واحدة من خلال المنشئ (constructor)، ويجب ألا يكون للفئة أي محدِّدات (setters).
- إن كانت لغة البرمجة التي تستخدمها تدعم الفئات المتداخلة، فأدخل التذكِرة داخل البادئ، وإلا فاستخرج واجهة فارغة من فئة التذكِرة واجعل كل الكائنات الأخرى تستخدمها لتشير إلى التذكِرة. قد تضيف بعض العمليات الوصفية إلى الواجهة لكن لا تضيف أي شيء يكشف حالة البادئ.
- أضف أسلوبًا إلى فئة البادئ ينتج فئات التذكِرة. ينبغي أن يمرر البادئ حالته إلى التذكِرة من خلال معطى (argument) واحد أو أكثر من معطيات منشئ التذكِرة. ويجب أن يكون نوع الإعادة للأسلوب من الواجهة التي استخرجتها في الخطوة السابقة (على فرض أنك استخرجتها). وينبغي كذلك أن يعمل أسلوب إنتاج التذكِرة مباشرة مع فئة التذكِرة.
- أضف أسلوبًا لاستعادة حالة البادئ إلى فئته، وينبغي أن يقبل تذكِرةً كمُعطى (argument). وإن استخرجت واجهة في الخطوة السابقة فاجعل نوع المعامِل(parameter). وفي تلك الحالة فإنك تحتاج إلى تعيين الكائن الجديد إلى فئة الوسيط بما أن البادئ يحتاج وصولًا كاملًا إلى ذلك الكائن.
- يجب أن يدرك النائب (caretaker) سواء كان يمثل كائنَ أمرٍ (command object) أو سجلًا أو شيئًا آخر تمامًا، يجب أن يدرك متى يطلب تذكِرات جديدة من البادئ، وكيف يخزنها ومتى يستعيد البادئ مع تذكِرة بعينها.
- قد يُنقل الرابط بين النائبات (caretakers) والبادئات إلى فئة التذكِرة، وفي تلك الحالة يجب أن تكون كل تذكِرة متصلةً بالبادئ الذي أنشأها، وقد ينتقل أسلوب الاستعادة كذلك إلى فئة التذكِرة، لكن هذا سيكون منطقيًا فقط في حالة إن كانت فئة التذكِرة محتواة داخل البادئ أو كانت فئة البادئ توفر محدِّدات كافية لتخطي (override) حالته.
المزايا والعيوب
المزايا
- تستطيع أخذ لقطات من حالة الكائن دون انتهاك تغليفه.
- تستطيع تبسيط شيفرة البادئ بالسماح للنائب أن يحتفظ بسجل لحالة البادئ.
العيوب
- قد يستهلك البرنامج كثيرًا من ذاكرة RAM إن كان العملاء يكثرون من إنشاء التذكِرات.
- لابد أن يتتبع النائب (caretaker) دورة حياة البادئ حتى يستطيع تدمير التذكِرات القديمة.
- لا تستطيع أغلب لغات البرمجة الديناميكية (مثل PHP - Python - JavaScript) أن تضمن أن الحالة داخل التذكِرة ستبقى دون تعديل.
العلاقات مع الأنماط الأخرى
- تستطيع استخدام نمطي الأمر (Command) والتذكِرة (Memento) معًا عند تطبيق خاصية الإرجاع "undo"، وتكون الأوامر في تلك الحالة مسؤولة عن تنفيذ مختلف العمليت على الكائن الهدف، بينما تحفظ التذكِرات حالة ذلك الكائن قبل تنفيذ الأمر مباشرة.
- تستطيع استخدام التذكِرة (memento) مع المكرّر (Iterator) لالتقاط حالة التكرار الحالية وإرجاعها (roll back) عند الحاجة.
- قد يكون نمط النموذج الأولي (Prototype) بديلًا أبسط أحيانًا لنمط التذكِرة (Memento)، وهذا إن كان الكائن -الحالة التي تريد تخزينها في السجل- بسيطًا ومباشرًا وليس به أي روابط إلى مصادر خارجية، أو إن كانت الروابط يمكن إنشاءها مرة أخرى بسهولة.
الاستخدام في لغة جافا
المستوى: ★ ★ ★
الانتشار: ★ ☆ ☆
أمثلة الاستخدام: يمكن تحقيق مبدأ نمط التذكِرة باستخدام التسلسل (serialization) الذي يشيع في لغة جافا، ورغم أنه ليس الطريقة الوحيدة ولا الأفضل لأخذ لقطات snapshot من حالة كائن ما، إلا أنها تسمح بتخزين نسخ احتياطية من الحالة مع حماية هيكل البادئ (originator) من الكائنات الأخرى. إليك بعض الأمثلة من النمط في مكتبات جافا:
- جميع تطبيقات (implementations) مكتبة java.io.Serializable تستطيع محاكاة التذكِرة.
- جميع تطبيقات javax.faces.component.StateHolder.
الاستخدام في لغة #C
المستوى: ★ ★ ★
الانتشار: ★ ☆ ☆
أمثلة الاستخدام: يمكن تحقيق مبدأ نمط التذكِرة باستخدام التسلسل (serialization) الذي يشيع في لغة #C، ورغم أنه ليس الطريقة الوحيدة ولا الأفضل لأخذ لقطات snapshot من حالة كائن ما، إلا أنها تسمح بتخزين نسخ احتياطية من الحالة مع حماية هيكل البادئ (originator) من الكائنات الأخرى.
الاستخدام في لغة PHP
الاستخدام في لغة بايثون
المستوى: ★ ★ ★
الانتشار: ★ ☆ ☆
أمثلة الاستخدام: يمكن تحقيق مبدأ نمط التذكِرة باستخدام التسلسل (serialization) الذي يشيع في لغة بايثون، ورغم أنه ليس الطريقة الوحيدة ولا الأفضل لأخذ لقطات snapshot من حالة كائن ما، إلا أنها تسمح بتخزين نسخ احتياطية من الحالة مع حماية هيكل البادئ (originator) من الكائنات الأخرى.
الاستخدام في لغة روبي
المستوى: ★ ★ ★
الانتشار: ★ ☆ ☆
أمثلة الاستخدام: يمكن تحقيق مبدأ نمط التذكِرة باستخدام التسلسل (serialization) الذي يشيع في لغة روبي، ورغم أنه ليس الطريقة الوحيدة ولا الأفضل لأخذ لقطات snapshot من حالة كائن ما، إلا أنها تسمح بتخزين نسخ احتياطية من الحالة مع حماية هيكل البادئ (originator) من الكائنات الأخرى.
الاستخدام في لغة Swift
المستوى: ★ ★ ★
الانتشار: ★ ☆ ☆
أمثلة الاستخدام: يمكن تحقيق مبدأ نمط التذكِرة باستخدام التسلسل (serialization) الذي يشيع في لغة Swift، ورغم أنه ليس الطريقة الوحيدة ولا الأفضل لأخذ لقطات snapshot من حالة كائن ما، إلا أنها تسمح بتخزين نسخ احتياطية من الحالة مع حماية هيكل البادئ (originator) من الكائنات الأخرى.
الاستخدام في لغة TypeScript
المستوى: ★ ★ ★
الانتشار: ★ ☆ ☆
أمثلة الاستخدام: يمكن تحقيق مبدأ نمط التذكِرة باستخدام التسلسل (serialization) الذي يشيع في لغة TypeScript، ورغم أنه ليس الطريقة الوحيدة ولا الأفضل لأخذ لقطات snapshot من حالة كائن ما، إلا أنها تسمح بتخزين نسخ احتياطية من الحالة مع حماية هيكل البادئ (originator) من الكائنات الأخرى.