الفرق بين المراجعتين لصفحة: «Design Patterns/memento»
أسامه-دمراني (نقاش | مساهمات) 2.2 محتوى |
جميل-بيلوني (نقاش | مساهمات) طلا ملخص تعديل |
||
(9 مراجعات متوسطة بواسطة 3 مستخدمين غير معروضة) | |||
سطر 5: | سطر 5: | ||
تخيل أنك تنشئ برنامج محرر نصي، ويستطيع محررك هذا أن ينسق النصوص ويدخل صورًا سطرية (inline images) وغير ذلك، إضافة إلى ميزة تحرير النصوص الافتراضية. ولنقل أنك قررت عند نقطة ما أن تسمح للمستخدمين بإلغاء أي عمليات أجريت على النص، وهي ميزة انتشرت كثيرًا في السنين الماضية إلى حد أن الناس صارت تتوقع من أي تطبيق أو برنامج أن تكون فيه تلك الخاصية. | تخيل أنك تنشئ برنامج محرر نصي، ويستطيع محررك هذا أن ينسق النصوص ويدخل صورًا سطرية (inline images) وغير ذلك، إضافة إلى ميزة تحرير النصوص الافتراضية. ولنقل أنك قررت عند نقطة ما أن تسمح للمستخدمين بإلغاء أي عمليات أجريت على النص، وهي ميزة انتشرت كثيرًا في السنين الماضية إلى حد أن الناس صارت تتوقع من أي تطبيق أو برنامج أن تكون فيه تلك الخاصية. | ||
ولتطبيق هذه الخاصية هنا فإنك تختار الطريق المباشر، وهو جعل البرنامج يسجل حالة جميع الكائنات ويحفظها في ذاكرة ما قبل تنفيذ أي عملية، ثم يجلب البرنامج آخر نسخة من تاريخ تلك اللقطات المسجلة ويستخدمها لاستعادة حالة جميع الكائنات، وذلك عندما يقرر المستخدم استعادة إجراء أو عملية ما. | ولتطبيق هذه الخاصية هنا فإنك تختار الطريق المباشر، وهو جعل البرنامج يسجل حالة جميع الكائنات ويحفظها في ذاكرة ما قبل تنفيذ أي عملية، ثم يجلب البرنامج آخر نسخة من تاريخ تلك اللقطات المسجلة ويستخدمها لاستعادة حالة جميع الكائنات، وذلك عندما يقرر المستخدم استعادة إجراء أو عملية ما. (انظر ش.1) | ||
[[ملف:dpmnt.problem1-en.png|تصغير|(ش.!) قبل تنفيذ أي عملية فإن البرنامج يحفظ لقطة من حالة الكائنات يمكن استخدامها لاحقًا لاستعادة الكائنات لحالتها السابقة.]] | |||
ستظهر هنا مشكلة في كيفية أخذ تلك اللقطات التي ستُحفظ، فالبديهي أن تنسخ قيم جميع الحقول في كائن ما إلى الذاكرة، لكن هذا لن ينجح إلا إن كان الكائن نفسه سيسمح لك لهذا، لكن أغلب الكائنات الحقيقية لن تسمح لغيرها بمعرفة ما فيها بسهولة، فهي تريد إخفاء البيانات الهامة في حقول خاصة (private). | |||
قبل تنفيذ أي عملية فإن البرنامج يحفظ لقطة من حالة الكائنات يمكن استخدامها لاحقًا لاستعادة الكائنات لحالتها السابقة. | |||
ستظهر هنا مشكلة في كيفية أخذ تلك اللقطات التي ستُحفظ، فالبديهي أن تنسخ قيم جميع الحقول في كائن ما إلى الذاكرة، لكن هذا لن ينجح إلا إن كان الكائن نفسه سيسمح لك لهذا، لكن أغلب الكائنات الحقيقية لن تسمح لغيرها بمعرفة ما فيها بسهولة، فهي تريد إخفاء البيانات الهامة في حقول خاصة (private). | |||
لكن لنفرض أن الكائنات التي لدينا ستتصرف مثل الهيبيز، إذ تفضل العلاقات المفتوحة وتبقي حالتها عامة، ستحل هذه الفرضية المشكلة التي ذكرناها وستسمح لك بأخذ لقطات من حالات الكائنات متى شئت، لكن بها مشاكل كبيرة، فقد تقرر في المستقبل أن تعيد هيكلة بعض فئات المحرر، أو تقرر إضافة أو حذف بعض الحقول، سيتطلب هذا منك سَلسَلة (chaining) الفئات المسؤولة عن نسخ حالة الكائنات التي ستتأثر بتعديلك. (انظر ش.2) | |||
[[ملف:dpmnt.problem2-en.png|تصغير|(ش.2) كيف تنسخ حالة الكائن الخاصة (private)]] | |||
ويجب أن تحتوي اللقطة (snapshot) الفعلية على النص الفعلي وإحداثيات المؤشر (cursor coordinates) وموضع التمرير الحالي (scroll position)، ... . وستحتاج أن تجمع هذه القيم وتضعها في حاوية ما من أجل إنشاء لقطة لحالة المحرر. والغالب أنك ستخزن كثيرًا من كائنات الحاوية تلك داخل قائمة ما تمثل سجل التغييرات (history)، ومن ثم ستكون هذه الحاويات كائنات لفئة واحدة ليس لها أي أساليب لكن في نفس الوقت فيها حقول كثيرة تعكس حالة المحرر. | ويجب أن تحتوي اللقطة (snapshot) الفعلية على النص الفعلي وإحداثيات المؤشر (cursor coordinates) وموضع التمرير الحالي (scroll position)، ... . وستحتاج أن تجمع هذه القيم وتضعها في حاوية ما من أجل إنشاء لقطة لحالة المحرر. والغالب أنك ستخزن كثيرًا من كائنات الحاوية تلك داخل قائمة ما تمثل سجل التغييرات (history)، ومن ثم ستكون هذه الحاويات كائنات لفئة واحدة ليس لها أي أساليب لكن في نفس الوقت فيها حقول كثيرة تعكس حالة المحرر. | ||
سطر 28: | سطر 22: | ||
ويفوض نمط التذكِرة (memento) إنشاء لقطات الحالة إلى المالك الفعلي لتلك الحالة، وهو الكائن البادئ (originator)، ومن ثم فبدلًا من محاولة الكائنات أن تنسخ حالة المحرر من الخارج، فإن فئة المحرر نفسها تستطيع أخذ لقطة (snapshot) بما أن لديها الحق الكامل في الوصول إلى حالتها الخاصة. | ويفوض نمط التذكِرة (memento) إنشاء لقطات الحالة إلى المالك الفعلي لتلك الحالة، وهو الكائن البادئ (originator)، ومن ثم فبدلًا من محاولة الكائنات أن تنسخ حالة المحرر من الخارج، فإن فئة المحرر نفسها تستطيع أخذ لقطة (snapshot) بما أن لديها الحق الكامل في الوصول إلى حالتها الخاصة. | ||
ويقترح نمط التذكرة تخزين نسخة من حالة الكائن في كائن خاص يسمى memento (التذكِرة)، ولا يُسمح لأي كائن آخر بالوصول إلى محتوياته باستثناء الكائن الذي أنشأ تلك المحتويات، ويجب على الكائنات الأخرى أن تتواصل مع كائنات التذكِرة الأخرى باستخدام واجهة محدودة قد تسمح بجلب البيانات الوصفية للقطة (وقت الإنشاء، اسم العملية المنفَّذة، ..) لكن لا تسمح بجلب حالة الكائن الأصلي المحتوى في اللقطة نفسها. | ويقترح نمط التذكرة تخزين نسخة من حالة الكائن في كائن خاص يسمى memento (التذكِرة)، ولا يُسمح لأي كائن آخر بالوصول إلى محتوياته باستثناء الكائن الذي أنشأ تلك المحتويات، ويجب على الكائنات الأخرى أن تتواصل مع كائنات التذكِرة الأخرى باستخدام واجهة محدودة قد تسمح بجلب البيانات الوصفية للقطة (وقت الإنشاء، اسم العملية المنفَّذة، ..) لكن لا تسمح بجلب حالة الكائن الأصلي المحتوى في اللقطة نفسها. (انظر ش.3) | ||
[[ملف:dpmnt.solution-en.png|تصغير|(ش.3) الكائن البادئ (originator) له كامل الحق في الوصول إلى التذكِرة، في حين أن الكائنات النائبة (caretakers) لا تستطيع الوصول إلا إلى البيانات الوصفية.]] | |||
الكائن البادئ (originator) له كامل الحق في الوصول إلى التذكِرة، في حين أن الكائنات النائبة (caretakers) لا تستطيع الوصول إلا إلى البيانات الوصفية. | |||
وتلك السياسة المتحفظة تسمح لك بتخزين التذكِرات داخل كائنات أخرى تسمى كائنات نائبة (caretakers)، وبما أن الكائن النائب يعمل مع التذكِرة من خلال واجهة محدودة حصرًا فلا يستطيع أن يتلاعب بالحالة المخزنة داخل التذكرة. وفي نفس الوقت فإن الكائن البادئ له وصول إلى كافة الحقول التي في التذكِرة، مما يسمح له باستعادة الحالة السابقة متى شاء. | وتلك السياسة المتحفظة تسمح لك بتخزين التذكِرات داخل كائنات أخرى تسمى كائنات نائبة (caretakers)، وبما أن الكائن النائب يعمل مع التذكِرة من خلال واجهة محدودة حصرًا فلا يستطيع أن يتلاعب بالحالة المخزنة داخل التذكرة. وفي نفس الوقت فإن الكائن البادئ له وصول إلى كافة الحقول التي في التذكِرة، مما يسمح له باستعادة الحالة السابقة متى شاء. | ||
سطر 44: | سطر 35: | ||
=== تطبيق مبني على الفئات المتداخلة === | === تطبيق مبني على الفئات المتداخلة === | ||
يعتمد التطبيق النموذجي للنمط على دعم الفئات المتداخلة (nested classes) الموجودة في العديد من لغات البرمجة المشهورة مثل ++C و#C وجافا. | يعتمد التطبيق النموذجي للنمط على دعم الفئات المتداخلة (nested classes) الموجودة في العديد من لغات البرمجة المشهورة مثل ++C و#C وجافا. | ||
[[ملف:dpmnt.structure1-indexed.png|تصغير|(ش.4)]] | |||
# تستطيع فئة البادئ (originator) أن تنتج لقطات لحالتها إضافة إلى استرجاع حالتها من لقطات مسجَّلة من قبل عند الحاجة. | # تستطيع فئة البادئ (originator) أن تنتج لقطات لحالتها إضافة إلى استرجاع حالتها من لقطات مسجَّلة من قبل عند الحاجة. | ||
# التذكِرة (memento) هي كائنُ قيمةٍ يتصرف كلقطة من حالة البادئ، ومن الشائع جعل التذكِرة ثابتة لا تقبل التغيير، وتُمرَّر البيانات إليها مرة واحدة من خلال المنشئ. | # التذكِرة (memento) هي كائنُ قيمةٍ يتصرف كلقطة من حالة البادئ، ومن الشائع جعل التذكِرة ثابتة لا تقبل التغيير، وتُمرَّر البيانات إليها مرة واحدة من خلال المنشئ. | ||
سطر 53: | سطر 43: | ||
=== تطبيق مبني على واجهة وسيطة === | === تطبيق مبني على واجهة وسيطة === | ||
هناك تطبيق بديل يناسب لغات البرمجة التي لا تدعم الفئات المتداخلة (مثل PHP). | هناك تطبيق بديل يناسب لغات البرمجة التي لا تدعم الفئات المتداخلة (مثل PHP). | ||
[[ملف:dpmnt.structure2-indexed.png|تصغير|(ش.5)]] | |||
# في غياب الفئات المتداخلة فيمكنك تقييد الوصول إلى حقول التذكِرة بإنشاء نظام يجعل النائب لا يعمل مع التذكِرة إلا من خلال واجهة وسيطة مصرَّح عنها بوضوح، ولن تصرِّح إلا عن أساليب متعلقة بالبيانات الوصفية للتذكِرة. | # في غياب الفئات المتداخلة فيمكنك تقييد الوصول إلى حقول التذكِرة بإنشاء نظام يجعل النائب لا يعمل مع التذكِرة إلا من خلال واجهة وسيطة مصرَّح عنها بوضوح، ولن تصرِّح إلا عن أساليب متعلقة بالبيانات الوصفية للتذكِرة. | ||
# على الناحية الأخرى، فإن الكائنات البادئة تستطيع العمل مع كائن التذكِرة مباشرة ومن ثم الوصول مباشرة أيضًا إلى الحقول والأساليب المصرَّح عنها في فئة التذكِرة، لكن الجانب السيء في هذا المنظور هو أنك تحتاج إلى التصريح عن كل أولئك الأعضاء لتكون حالتهم عامة. | # على الناحية الأخرى، فإن الكائنات البادئة تستطيع العمل مع كائن التذكِرة مباشرة ومن ثم الوصول مباشرة أيضًا إلى الحقول والأساليب المصرَّح عنها في فئة التذكِرة، لكن الجانب السيء في هذا المنظور هو أنك تحتاج إلى التصريح عن كل أولئك الأعضاء لتكون حالتهم عامة. | ||
سطر 60: | سطر 49: | ||
=== تطبيق آخر بتغليف أكثر تقييدًا === | === تطبيق آخر بتغليف أكثر تقييدًا === | ||
لدينا تطبيق آخر مفيد حين لا تريد ترك أدنى فرصة للفئات الأخرى في الوصول إلى حالة البادئ من خلال التذكِرة. | لدينا تطبيق آخر مفيد حين لا تريد ترك أدنى فرصة للفئات الأخرى في الوصول إلى حالة البادئ من خلال التذكِرة. | ||
[[ملف:dpmnt.structure3-indexed.png|تصغير|(ش.6)]] | |||
# يسمح هذا التطبيق بوجود عدة أنواع من كائنات البادئ والتذكِرة، ويعمل كل بادئ مع فئة التذكرة المتوافقة معه، ولا يكشف البادئ أو التذكِرة حالتهما لأي أحد. | # يسمح هذا التطبيق بوجود عدة أنواع من كائنات البادئ والتذكِرة، ويعمل كل بادئ مع فئة التذكرة المتوافقة معه، ولا يكشف البادئ أو التذكِرة حالتهما لأي أحد. | ||
# يُقيَّد النواب الآن صراحةً من تغيير الحالة المخزنة في التذكِرات، بل تصبح فئة النائب مستقلة عن البادئ لأن أسلوب الاستعادة قد حُدِّد الآن في فئة التذكِرة. | # يُقيَّد النواب الآن صراحةً من تغيير الحالة المخزنة في التذكِرات، بل تصبح فئة النائب مستقلة عن البادئ لأن أسلوب الاستعادة قد حُدِّد الآن في فئة التذكِرة. | ||
# يصبح كل كائن تذكِرة مرتبطًا بالبادئ الذي أنشأه، ويمرر البادئ نفسه إلى منشئ التذكِرة (constructor) مع القيَم الخاصة بحالته، وتستطيع التذكِرة استعادة الحالة الخاصة ببادئها بشرط أن يوضح البادئ المحددات المناسبة (setters)، وذلك بفضل العلاقة الوثيقة بين الفئتين. | # يصبح كل كائن تذكِرة مرتبطًا بالبادئ الذي أنشأه، ويمرر البادئ نفسه إلى منشئ التذكِرة (constructor) مع القيَم الخاصة بحالته، وتستطيع التذكِرة استعادة الحالة الخاصة ببادئها بشرط أن يوضح البادئ المحددات المناسبة (setters)، وذلك بفضل العلاقة الوثيقة بين الفئتين. | ||
== مثال | == مثال توضيحي == | ||
يستخدم هذا المثال نمط التذكِرة مع نمط الأمر (command) لتخزين لقطات (snapshots) من الحالة المعقدة للمحرر النصي واستعادة حالة سابقة من تلك اللقطات عند الحاجة. | يستخدم هذا المثال نمط التذكِرة مع نمط الأمر (command) لتخزين لقطات (snapshots) من الحالة المعقدة للمحرر النصي واستعادة حالة سابقة من تلك اللقطات عند الحاجة. (انظر ش.7) | ||
[[ملف:dpmnt.example.png|تصغير|(ش.7) حفظ لقطات من حالة المحرر النصي.]] | |||
حفظ لقطات من حالة المحرر النصي. | |||
تتصرف كائنات الأمر كنائبات (caretakers)، وتجلب تذكِرة المحرر قبل تنفيذ العمليات المرتبطة بالأوامر، وحين يحاول مستخدم أن يتراجع عن أحدث أمر تم تنفيذه فإن المحرر يستطيع استخدام التذكِرة المخزنة في ذلك الأمر لاسترجاع نفسه إلى حالة سابقة. | تتصرف كائنات الأمر كنائبات (caretakers)، وتجلب تذكِرة المحرر قبل تنفيذ العمليات المرتبطة بالأوامر، وحين يحاول مستخدم أن يتراجع عن أحدث أمر تم تنفيذه فإن المحرر يستطيع استخدام التذكِرة المخزنة في ذلك الأمر لاسترجاع نفسه إلى حالة سابقة. | ||
سطر 173: | سطر 157: | ||
* جميع تطبيقات (implementations) مكتبة [http://docs.oracle.com/javase/8/docs/api/java/io/Serializable.html java.io.Serializable] تستطيع محاكاة التذكِرة. | * جميع تطبيقات (implementations) مكتبة [http://docs.oracle.com/javase/8/docs/api/java/io/Serializable.html java.io.Serializable] تستطيع محاكاة التذكِرة. | ||
* جميع تطبيقات [http://docs.oracle.com/javaee/7/api/javax/faces/component/StateHolder.html javax.faces.component.StateHolder]. | * جميع تطبيقات [http://docs.oracle.com/javaee/7/api/javax/faces/component/StateHolder.html javax.faces.component.StateHolder]. | ||
=== محرر الأشكال وخاصية التراجع/الإعادة === | |||
يسمح هذا المحرر الرسومي بتغيير لون وموضع الأشكال على الشاشة، مع إمكانية التراجع عن أي تعديل أو تكراره. وتبنى خاصية التراجع "undo" على تعاون بين نمطي التذكِرة (Memento) والأمر (Command)، ويتتبع المحرر سجل الأوامر المنفذة إذ يأخذ نسخة احتياطية قبل تنفيذ الأمر ويصلها بكائن الأمر، ثم يرسل الأمر المنفَّذ بعد تنفيذه إلى السجل. | |||
وحين يطلب المستخدم التراجع عن عملية ما فإن المحرر يجلب أحدث أمر من السجل ويستعيد الحالة من النسخة المحفوظة داخل ذلك الأمر، وإن طلب المستخدم عملية تراجع أخرى فإن المحرر يأخذ الأمر التالي من السجل، وهكذا. | |||
تُحفظ الأوامر الملغاة (reverted) في السجل حتى يجري المستخدم بعض التعديلات على الأشكال التي على الشاشة، هذه الخاصية ضرورية من أجل إعادة تنفيذ الأوامر المتراجع عنها. | |||
==== المحرر ==== | |||
===== editor/Editor.java: شيفرة المحرر ===== | |||
<syntaxhighlight lang="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."); | |||
} | |||
} | |||
} | |||
</syntaxhighlight> | |||
===== editor/Canvas.java: شيفرة الحاوية (Canvas Code) ===== | |||
<syntaxhighlight lang="java"> | |||
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); | |||
} | |||
} | |||
</syntaxhighlight> | |||
==== السجل (History) ==== | |||
===== history/History.java: السجل يخزن الأوامر (commands) والتذكِرات (mementos) ===== | |||
<syntaxhighlight lang="javascript"> | |||
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); | |||
} | |||
} | |||
</syntaxhighlight> | |||
===== history/Memento.java: فئة التذكِرة ===== | |||
<syntaxhighlight lang="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); | |||
} | |||
} | |||
</syntaxhighlight> | |||
==== الأوامر Commands ==== | |||
===== commands/Command.java: فئة الأمر الأساسية (Base Command Class) ===== | |||
<syntaxhighlight lang="java"> | |||
package refactoring_guru.memento.example.commands; | |||
public interface Command { | |||
String getName(); | |||
void execute(); | |||
} | |||
</syntaxhighlight> | |||
===== commands/ColorCommand.java: يغير لون الشكل المختار ===== | |||
<syntaxhighlight lang="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); | |||
} | |||
} | |||
} | |||
</syntaxhighlight> | |||
===== commands/MoveCommand.java: يحرك الشكل المختار ===== | |||
<syntaxhighlight lang="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); | |||
} | |||
} | |||
} | |||
</syntaxhighlight> | |||
==== shapes: أشكال مختلفة ==== | |||
===== shapes/Shape.java ===== | |||
<syntaxhighlight lang="text"> | |||
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); | |||
} | |||
</syntaxhighlight> | |||
===== shapes/BaseShape.java ===== | |||
<syntaxhighlight lang="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); | |||
} | |||
// ... | |||
} | |||
} | |||
</syntaxhighlight> | |||
===== shapes/Circle.java ===== | |||
<syntaxhighlight lang="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); | |||
} | |||
} | |||
</syntaxhighlight> | |||
===== shapes/Dot.java ===== | |||
<syntaxhighlight lang="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()); | |||
} | |||
} | |||
</syntaxhighlight> | |||
===== shapes/Rectangle.java ===== | |||
<syntaxhighlight lang="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); | |||
} | |||
} | |||
</syntaxhighlight> | |||
===== shapes/CompoundShape.java ===== | |||
<syntaxhighlight lang="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); | |||
} | |||
} | |||
} | |||
</syntaxhighlight> | |||
===== Demo.java: شيفرة البدء (Initialization Code) ===== | |||
<syntaxhighlight lang="java"> | |||
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) | |||
) | |||
); | |||
} | |||
} | |||
</syntaxhighlight> | |||
===== OutputDemo.png: لقطة صورة ===== | |||
[[ملف:dpmnt.OutputDemo.png]] | |||
== الاستخدام في لغة #C == | == الاستخدام في لغة #C == | ||
سطر 180: | سطر 1٬098: | ||
'''أمثلة الاستخدام:''' يمكن تحقيق مبدأ نمط التذكِرة باستخدام التسلسل (serialization) الذي يشيع في لغة #C، ورغم أنه ليس الطريقة الوحيدة ولا الأفضل لأخذ لقطات snapshot من حالة كائن ما، إلا أنها تسمح بتخزين نسخ احتياطية من الحالة مع حماية هيكل البادئ (originator) من الكائنات الأخرى. | '''أمثلة الاستخدام:''' يمكن تحقيق مبدأ نمط التذكِرة باستخدام التسلسل (serialization) الذي يشيع في لغة #C، ورغم أنه ليس الطريقة الوحيدة ولا الأفضل لأخذ لقطات snapshot من حالة كائن ما، إلا أنها تسمح بتخزين نسخ احتياطية من الحالة مع حماية هيكل البادئ (originator) من الكائنات الأخرى. | ||
===مثال تصوري=== | |||
يوضح هذا المثال بنية نمط الوسيط، ويركز على إجابة الأسئلة التالية: | |||
*ما الفئات التي يتكون منها؟ | |||
*ما الأدوار التي تلعبها هذه الفئات؟ | |||
*كيف ترتبط عناصر النمط ببعضها؟ | |||
====Program.cs: مثال تصوري ==== | |||
<syntaxhighlight lang="c#"> | |||
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(); | |||
} | |||
} | |||
} | |||
</syntaxhighlight> | |||
==== Output.txt: نتائج التنفيذ ==== | |||
<syntaxhighlight lang="text"> | |||
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 | |||
</syntaxhighlight> | |||
== الاستخدام في لغة PHP == | == الاستخدام في لغة PHP == | ||
'''المستوى: ★ ★ ★''' | |||
'''الانتشار: ★ ☆ ☆''' | |||
'''أمثلة الاستخدام:''' قابلية التطبيق الفعلية لنمط التذكِرة في لغة [[PHP]] موضع نظر، إذ أنك تستطيع إنشاء نسخة من حالة الكائن بطريقة أسهل بمجرد استخدام التسلسل (serialization). | |||
=== مثال تصوري === | |||
يوضح هذا المثال بنية نمط '''التذكِرة'''، ويركز على إجابة الأسئلة التالية: | |||
*ما الفئات التي يتكون منها؟ | |||
*ما الأدوار التي تلعبها هذه الفئات؟ | |||
*كيف ترتبط عناصر النمط ببعضها؟ | |||
سيكون من السهل عليك استيعاب المثال التالي بعد تعلم بنية النمط، بناء على استخدام واقعي له في لغة PHP. | |||
====index.php: مثال تصوري ==== | |||
<syntaxhighlight lang="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(); | |||
</syntaxhighlight> | |||
==== Output.txt: نتائج التنفيذ ==== | |||
<syntaxhighlight lang="text"> | |||
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 | |||
</syntaxhighlight> | |||
== الاستخدام في لغة بايثون == | == الاستخدام في لغة بايثون == | ||
سطر 188: | سطر 1٬572: | ||
'''الانتشار: ★ ☆ ☆''' | '''الانتشار: ★ ☆ ☆''' | ||
'''أمثلة الاستخدام:''' يمكن تحقيق مبدأ نمط التذكِرة باستخدام التسلسل (serialization) الذي يشيع في لغة | '''أمثلة الاستخدام:''' يمكن تحقيق مبدأ نمط التذكِرة باستخدام التسلسل (serialization) الذي يشيع في لغة [[Python|بايثون]]، ورغم أنه ليس الطريقة الوحيدة ولا الأفضل لأخذ لقطات snapshot من حالة كائن ما، إلا أنها تسمح بتخزين نسخ احتياطية من الحالة مع حماية هيكل البادئ (originator) من الكائنات الأخرى. | ||
===مثال تصوري=== | |||
يوضح هذا المثال بنية نمط التذكِرة، ويركز على إجابة الأسئلة التالية: | |||
*ما الفئات التي يتكون منها؟ | |||
*ما الأدوار التي تلعبها هذه الفئات؟ | |||
*كيف ترتبط عناصر النمط ببعضها؟ | |||
==== main.py: مثال تصوري ==== | |||
<syntaxhighlight lang="python"> | |||
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() | |||
</syntaxhighlight> | |||
==== Output.txt: نتائج التنفيذ ==== | |||
<syntaxhighlight lang="text"> | |||
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 | |||
</syntaxhighlight> | |||
== الاستخدام في لغة روبي == | == الاستخدام في لغة روبي == | ||
سطر 196: | سطر 1٬762: | ||
'''أمثلة الاستخدام:''' يمكن تحقيق مبدأ نمط التذكِرة باستخدام التسلسل (serialization) الذي يشيع في لغة روبي، ورغم أنه ليس الطريقة الوحيدة ولا الأفضل لأخذ لقطات snapshot من حالة كائن ما، إلا أنها تسمح بتخزين نسخ احتياطية من الحالة مع حماية هيكل البادئ (originator) من الكائنات الأخرى. | '''أمثلة الاستخدام:''' يمكن تحقيق مبدأ نمط التذكِرة باستخدام التسلسل (serialization) الذي يشيع في لغة روبي، ورغم أنه ليس الطريقة الوحيدة ولا الأفضل لأخذ لقطات snapshot من حالة كائن ما، إلا أنها تسمح بتخزين نسخ احتياطية من الحالة مع حماية هيكل البادئ (originator) من الكائنات الأخرى. | ||
===مثال تصوري=== | |||
يوضح هذا المثال بنية نمط التذكِرة، ويركز على إجابة الأسئلة التالية: | |||
*ما الفئات التي يتكون منها؟ | |||
*ما الأدوار التي تلعبها هذه الفئات؟ | |||
*كيف ترتبط عناصر النمط ببعضها؟ | |||
==== main.rb: مثال تصوري ==== | |||
<syntaxhighlight lang="ruby"> | |||
# يحمل البادئ حالة مهمة قد تتغير مع الوقت، ويحدد ذلك البادئ أسلوبًا لحفظ | |||
# الحالة داخل تذكِرة وأسلوبًا آخر لاستعادة الحالة منها. | |||
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 | |||
</syntaxhighlight> | |||
==== output.txt: نتائج التنفيذ ==== | |||
<syntaxhighlight lang="text"> | |||
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 | |||
</syntaxhighlight> | |||
== الاستخدام في لغة Swift == | == الاستخدام في لغة Swift == | ||
سطر 203: | سطر 1٬939: | ||
'''أمثلة الاستخدام:''' يمكن تحقيق مبدأ نمط التذكِرة باستخدام التسلسل (serialization) الذي يشيع في لغة Swift، ورغم أنه ليس الطريقة الوحيدة ولا الأفضل لأخذ لقطات snapshot من حالة كائن ما، إلا أنها تسمح بتخزين نسخ احتياطية من الحالة مع حماية هيكل البادئ (originator) من الكائنات الأخرى. | '''أمثلة الاستخدام:''' يمكن تحقيق مبدأ نمط التذكِرة باستخدام التسلسل (serialization) الذي يشيع في لغة Swift، ورغم أنه ليس الطريقة الوحيدة ولا الأفضل لأخذ لقطات snapshot من حالة كائن ما، إلا أنها تسمح بتخزين نسخ احتياطية من الحالة مع حماية هيكل البادئ (originator) من الكائنات الأخرى. | ||
===مثال تصوري=== | |||
يوضح هذا المثال بنية نمط التذكِرة، ويركز على إجابة الأسئلة التالية: | |||
*ما الفئات التي يتكون منها؟ | |||
*ما الأدوار التي تلعبها هذه الفئات؟ | |||
*كيف ترتبط عناصر النمط ببعضها؟ | |||
==== Example.swift: مثال تصوري ==== | |||
<syntaxhighlight lang="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() | |||
} | |||
} | |||
</syntaxhighlight> | |||
==== Output.txt: نتائج التنفيذ ==== | |||
<syntaxhighlight lang="text"> | |||
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 | |||
</syntaxhighlight> | |||
=== مثال واقعي === | |||
==== Example.swift: مثال واقعي ==== | |||
<syntaxhighlight lang="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" | |||
} | |||
} | |||
} | |||
</syntaxhighlight> | |||
==== Output.txt: نتائج التنفيذ ==== | |||
<syntaxhighlight lang="text"> | |||
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} | |||
</syntaxhighlight> | |||
== الاستخدام في لغة TypeScript == | == الاستخدام في لغة TypeScript == | ||
سطر 210: | سطر 2٬245: | ||
'''أمثلة الاستخدام:''' يمكن تحقيق مبدأ نمط التذكِرة باستخدام التسلسل (serialization) الذي يشيع في لغة TypeScript، ورغم أنه ليس الطريقة الوحيدة ولا الأفضل لأخذ لقطات snapshot من حالة كائن ما، إلا أنها تسمح بتخزين نسخ احتياطية من الحالة مع حماية هيكل البادئ (originator) من الكائنات الأخرى. | '''أمثلة الاستخدام:''' يمكن تحقيق مبدأ نمط التذكِرة باستخدام التسلسل (serialization) الذي يشيع في لغة TypeScript، ورغم أنه ليس الطريقة الوحيدة ولا الأفضل لأخذ لقطات snapshot من حالة كائن ما، إلا أنها تسمح بتخزين نسخ احتياطية من الحالة مع حماية هيكل البادئ (originator) من الكائنات الأخرى. | ||
===مثال تصوري=== | |||
يوضح هذا المثال بنية نمط التذكِرة، ويركز على إجابة الأسئلة التالية: | |||
*ما الفئات التي يتكون منها؟ | |||
*ما الأدوار التي تلعبها هذه الفئات؟ | |||
*كيف ترتبط عناصر النمط ببعضها؟ | |||
==== index.ts: مثال تصوري ==== | |||
<syntaxhighlight lang="typescript"> | |||
/** | |||
* يحمل البادئ حالة مهمة قد تتغير مع الوقت، ويحدد ذلك البادئ أسلوبًا لحفظ | |||
* الحالة داخل تذكِرة وأسلوبًا آخر لاستعادة الحالة منها. | |||
*/ | |||
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(); | |||
</syntaxhighlight> | |||
==== Output.txt: نتائج التنفيذ ==== | |||
<syntaxhighlight lang="text"> | |||
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 | |||
</syntaxhighlight> | |||
== انظر أيضًا == | == انظر أيضًا == | ||
* [[Design Patterns/iterator|نمط المكرِّر Iterator.]] | |||
* [[Design Patterns/prototype|نمط النموذج الأولي Prototype.]] | |||
* [[Design Patterns/decorator|نمط المزخرِف Decorator.]] | |||
== مصادر == | == مصادر == | ||
* [https://refactoring.guru/design-patterns/memento/ توثيق نمط التذكِرة (Memento) في موقع refactoring.guru]. | |||
[[تصنيف:Design Patterns]] | |||
[[تصنيف:Memento Design Pattern]] | |||
[[تصنيف:Behavioral Patterns]] |
المراجعة الحالية بتاريخ 10:48، 7 أكتوبر 2022
نمط التذكرة هو نمط تصميم سلوكي يسمح لك بحفظ واسترجاع الحالة السابقة لكائن ما دون كشف تفاصيل استخداماته أو تطبيقاته (implementations).
المشكلة
تخيل أنك تنشئ برنامج محرر نصي، ويستطيع محررك هذا أن ينسق النصوص ويدخل صورًا سطرية (inline images) وغير ذلك، إضافة إلى ميزة تحرير النصوص الافتراضية. ولنقل أنك قررت عند نقطة ما أن تسمح للمستخدمين بإلغاء أي عمليات أجريت على النص، وهي ميزة انتشرت كثيرًا في السنين الماضية إلى حد أن الناس صارت تتوقع من أي تطبيق أو برنامج أن تكون فيه تلك الخاصية.
ولتطبيق هذه الخاصية هنا فإنك تختار الطريق المباشر، وهو جعل البرنامج يسجل حالة جميع الكائنات ويحفظها في ذاكرة ما قبل تنفيذ أي عملية، ثم يجلب البرنامج آخر نسخة من تاريخ تلك اللقطات المسجلة ويستخدمها لاستعادة حالة جميع الكائنات، وذلك عندما يقرر المستخدم استعادة إجراء أو عملية ما. (انظر ش.1)
ستظهر هنا مشكلة في كيفية أخذ تلك اللقطات التي ستُحفظ، فالبديهي أن تنسخ قيم جميع الحقول في كائن ما إلى الذاكرة، لكن هذا لن ينجح إلا إن كان الكائن نفسه سيسمح لك لهذا، لكن أغلب الكائنات الحقيقية لن تسمح لغيرها بمعرفة ما فيها بسهولة، فهي تريد إخفاء البيانات الهامة في حقول خاصة (private).
لكن لنفرض أن الكائنات التي لدينا ستتصرف مثل الهيبيز، إذ تفضل العلاقات المفتوحة وتبقي حالتها عامة، ستحل هذه الفرضية المشكلة التي ذكرناها وستسمح لك بأخذ لقطات من حالات الكائنات متى شئت، لكن بها مشاكل كبيرة، فقد تقرر في المستقبل أن تعيد هيكلة بعض فئات المحرر، أو تقرر إضافة أو حذف بعض الحقول، سيتطلب هذا منك سَلسَلة (chaining) الفئات المسؤولة عن نسخ حالة الكائنات التي ستتأثر بتعديلك. (انظر ش.2)
ويجب أن تحتوي اللقطة (snapshot) الفعلية على النص الفعلي وإحداثيات المؤشر (cursor coordinates) وموضع التمرير الحالي (scroll position)، ... . وستحتاج أن تجمع هذه القيم وتضعها في حاوية ما من أجل إنشاء لقطة لحالة المحرر. والغالب أنك ستخزن كثيرًا من كائنات الحاوية تلك داخل قائمة ما تمثل سجل التغييرات (history)، ومن ثم ستكون هذه الحاويات كائنات لفئة واحدة ليس لها أي أساليب لكن في نفس الوقت فيها حقول كثيرة تعكس حالة المحرر.
ومن أجل السماح للكائنات الأخرى بالكتابة والقراءة من وإلى اللقطة التي أخذناها، فسنحتاج إلى جعل الحقول عامة، وسيكشف هذا كل حالات المحرر سواء كانت خاصة أم لا، وستصير الفئات الأخرى معتمدة على كل تغيير صغير يحدث داخل الحقول الخاصة والأساليب دون التأثير على الفئات الخارجية (outer classes).
تقودك هذه الطريقة إلى طريق مسدود، فإما أن تكشف كل التفاصيل الداخلية للفئات جاعلًا إياها هشة أو تقيد الوصول إلى حالتها جاعلًا من المستحيل إنتاج لقطات، لذا لا بد من أسلوب أفضل لإدخال زر التراجع (undo) في المحرر.
الحل
كان التغليف (encapsulation) غير السليم هو سبب جميع المشاكل التي تعرضنا لها قبل قليل، إذ تحاول بعض الكائنات أن تفعل أكثر مما يفترض بها فعله، فتقتحم المساحة الخاصة للكائنات الأخرى من أجل جمع البيانات المطلوبة لتنفيذ إجراء ما، بدلًا من السماح لتلك الكائنات أن تنفذ الإجراء الفعلي.
ويفوض نمط التذكِرة (memento) إنشاء لقطات الحالة إلى المالك الفعلي لتلك الحالة، وهو الكائن البادئ (originator)، ومن ثم فبدلًا من محاولة الكائنات أن تنسخ حالة المحرر من الخارج، فإن فئة المحرر نفسها تستطيع أخذ لقطة (snapshot) بما أن لديها الحق الكامل في الوصول إلى حالتها الخاصة.
ويقترح نمط التذكرة تخزين نسخة من حالة الكائن في كائن خاص يسمى memento (التذكِرة)، ولا يُسمح لأي كائن آخر بالوصول إلى محتوياته باستثناء الكائن الذي أنشأ تلك المحتويات، ويجب على الكائنات الأخرى أن تتواصل مع كائنات التذكِرة الأخرى باستخدام واجهة محدودة قد تسمح بجلب البيانات الوصفية للقطة (وقت الإنشاء، اسم العملية المنفَّذة، ..) لكن لا تسمح بجلب حالة الكائن الأصلي المحتوى في اللقطة نفسها. (انظر ش.3)
وتلك السياسة المتحفظة تسمح لك بتخزين التذكِرات داخل كائنات أخرى تسمى كائنات نائبة (caretakers)، وبما أن الكائن النائب يعمل مع التذكِرة من خلال واجهة محدودة حصرًا فلا يستطيع أن يتلاعب بالحالة المخزنة داخل التذكرة. وفي نفس الوقت فإن الكائن البادئ له وصول إلى كافة الحقول التي في التذكِرة، مما يسمح له باستعادة الحالة السابقة متى شاء.
وفي مثال المحرر النصي الذي نتعرض له، فنحن نستطيع إنشاء فئة سجل منفصلة لتتصرف ككائن نائب (caretaker)، بحيث أنه في كل مرة يكون المحرر على وشك تنفيذ عملية ما فإنه يكدِّس تذكِرات داخل الكائن النائب، وتستطيع أن تستخرج هذا المكدَّس داخل الواجهة الرسومية للبرنامج، عارضًا بذلك سجل العمليات المنفَّذة من قِبل مستخدم ما.
وحين يضغط المستخدم على زر تراجع (undo) فإن السجل يجلب آخر تذكِرة من المكدَّس ويمررها مرة أخرى إلى المحرر طالبًا تراجع الحالة إليها، وبما أن المحرر له وصول كامل إلى التذكِرة فإنه يغير حالة نفسه بالقيم المأخوذة من التذكِرة.
البُنية
تطبيق مبني على الفئات المتداخلة
يعتمد التطبيق النموذجي للنمط على دعم الفئات المتداخلة (nested classes) الموجودة في العديد من لغات البرمجة المشهورة مثل ++C و#C وجافا.
- تستطيع فئة البادئ (originator) أن تنتج لقطات لحالتها إضافة إلى استرجاع حالتها من لقطات مسجَّلة من قبل عند الحاجة.
- التذكِرة (memento) هي كائنُ قيمةٍ يتصرف كلقطة من حالة البادئ، ومن الشائع جعل التذكِرة ثابتة لا تقبل التغيير، وتُمرَّر البيانات إليها مرة واحدة من خلال المنشئ.
- لا يدرك النائب (caretaker) متى ولماذا يلتقط حالة البادئ فحسب، بل يعرف كذلك متى يجب أن تُسترجَع الحالة. ويستطيع أن يحتفظ بسجل لتاريخ البادئ عبر تخزين مكدَّس من التذكِرات، وعند حاجة البادئ إلى العودة إلى حالة سابقة فإن النائب يجلب أحدث تذكِرة من المكدَّس ويمررها إلى أسلوب استعادة البادئ.
- في هذا التطبيق للنمط فإن فئة التذكرة تكون محتواة بداخل البادئ، ويسمح هذا للبادئ بالوصول إلى الحقول والأساليب الخاصة بالتذكِرة رغم أنها مصرَّحٌ عنها بأنها خاصة (private). ومن الناحية الأخرى فإن النائب ليس لديه إلا وصول محدود جدًا إلى حقول التذكِرة وأساليبها، مما يسمح له بتخزين التذكِرات في مكدَّس دون القدرة على تغيير حالتها أو التعديل فيها.
تطبيق مبني على واجهة وسيطة
هناك تطبيق بديل يناسب لغات البرمجة التي لا تدعم الفئات المتداخلة (مثل PHP).
- في غياب الفئات المتداخلة فيمكنك تقييد الوصول إلى حقول التذكِرة بإنشاء نظام يجعل النائب لا يعمل مع التذكِرة إلا من خلال واجهة وسيطة مصرَّح عنها بوضوح، ولن تصرِّح إلا عن أساليب متعلقة بالبيانات الوصفية للتذكِرة.
- على الناحية الأخرى، فإن الكائنات البادئة تستطيع العمل مع كائن التذكِرة مباشرة ومن ثم الوصول مباشرة أيضًا إلى الحقول والأساليب المصرَّح عنها في فئة التذكِرة، لكن الجانب السيء في هذا المنظور هو أنك تحتاج إلى التصريح عن كل أولئك الأعضاء لتكون حالتهم عامة.
تطبيق آخر بتغليف أكثر تقييدًا
لدينا تطبيق آخر مفيد حين لا تريد ترك أدنى فرصة للفئات الأخرى في الوصول إلى حالة البادئ من خلال التذكِرة.
- يسمح هذا التطبيق بوجود عدة أنواع من كائنات البادئ والتذكِرة، ويعمل كل بادئ مع فئة التذكرة المتوافقة معه، ولا يكشف البادئ أو التذكِرة حالتهما لأي أحد.
- يُقيَّد النواب الآن صراحةً من تغيير الحالة المخزنة في التذكِرات، بل تصبح فئة النائب مستقلة عن البادئ لأن أسلوب الاستعادة قد حُدِّد الآن في فئة التذكِرة.
- يصبح كل كائن تذكِرة مرتبطًا بالبادئ الذي أنشأه، ويمرر البادئ نفسه إلى منشئ التذكِرة (constructor) مع القيَم الخاصة بحالته، وتستطيع التذكِرة استعادة الحالة الخاصة ببادئها بشرط أن يوضح البادئ المحددات المناسبة (setters)، وذلك بفضل العلاقة الوثيقة بين الفئتين.
مثال توضيحي
يستخدم هذا المثال نمط التذكِرة مع نمط الأمر (command) لتخزين لقطات (snapshots) من الحالة المعقدة للمحرر النصي واستعادة حالة سابقة من تلك اللقطات عند الحاجة. (انظر ش.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).
يجعل نمط التذكِرة الكائن نفسه مسؤولًا عن إنشاء لقطة لحالته، ولا يستطيع أي كائن آخر أن يقرأ ما في اللقطة، مما يجعل بيانات الحالة للكائن الأصلي آمنة ومحمية.
كيفية الاستخدام
- حدد أي الفئات ستلعب دور البادئ. من المهم أن تعرف ما إن كان البرنامج يستخدم كائنًا مركزيًا واحدًا من هذا النوع أو عدة كائنات أصغر.
- أنشئ فئة التذكِرة. وصرِّح عن مجموعة من الحقول -واحدًا واحدًا- التي تعكس (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.
محرر الأشكال وخاصية التراجع/الإعادة
يسمح هذا المحرر الرسومي بتغيير لون وموضع الأشكال على الشاشة، مع إمكانية التراجع عن أي تعديل أو تكراره. وتبنى خاصية التراجع "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
{
// يحمل البادئ حالة مهمة قد تتغير مع الوقت، ويحدد ذلك البادئ أسلوبًا لحفظ
// الحالة داخل تذكِرة وأسلوبًا آخر لاستعادة الحالة منها.
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