توضیح دوات: برای راهنمایی و اطلاعات بیشتر در مورد نمودار کلاس به این مقاله مراجعه کنید.
الگوی طراحی Memento یک الگوی رفتاری است که به ما اجازه میدهد حالتهای قبلی یک شیء را ذخیره و بازیابی کنیم، بدون اینکه جزئیات پیادهسازی آن را فاش کنیم.
الگوی Memento تلاش میکند حالت یک شیء را ثبت کرده و به بیرون منتقل کند تا بعداً بتوان شیء را به این حالت بازگرداند. هدف این الگو جدا کردن حالت فعلی یک شیء از حالت قبلی آن است تا در صورتی که اتفاقی برای حالت فعلی بیفتد، بتوان حالت شیء را از Memento آن بازیابی کرد.
میتوانید کد نمونه این مطلب را در GitHub پیدا کنید.
مفهومسازی مسئله
فرض کنیم در حال ایجاد یک برنامه ویرایشگر متن هستیم. علاوه بر ویرایش ساده متن، ویرایشگر ما میتواند متن را قالببندی کرده و تصاویر درونخطی اضافه کند.
در مقطعی تصمیم گرفتیم به کاربران اجازه دهیم هر عملیاتی را که روی متن انجام دادهاند، لغو کنند. این ویژگی آنقدر متداول شده است که مردم از هر برنامهای انتظار دارند آن را داشته باشد. برای پیادهسازی این قابلیت، تصمیم گرفتیم رویکرد مستقیم را انتخاب کنیم. قبل از انجام هر عملی، برنامه حالت تمام اشیاء را ثبت کرده و آنها را در استورج ذخیره میکند. بعداً، زمانی که کاربر تصمیم به بازگرداندن یک عمل میگیرد، برنامه آخرین لحظه ثبتشده را از تاریخچه میگیرد و از آن برای بازیابی حالت تمام اشیاء استفاده میکند.
قبل از اجرای یک عمل، برنامه یک اسنپشات از حالت اشیاء ذخیره میکند که بعداً میتواند برای بازگرداندن اشیاء به حالت قبلی استفاده شود.
بیایید درباره این اسنپشاتهای حالت فکر کنیم. دقیقاً چگونه یکی از این عکسها را تولید میکنیم؟ احتمالاً باید تمام فیلدهای یک شیء را مرور کرده و مقادیر آنها را در استورج کپی کنیم. با این حال، این کار تنها در صورتی عملی است که کلاس محدودیتهای دسترسی به محتویات خود را کاهش دهد. متأسفانه، بیشتر کلاسهای واقعی اجازه نمیدهند دیگران بهسادگی داخل حالت آنها سرک بکشند و تمام دادههای مهم را در فیلدهای خصوصی مخفی میکنند.
این مشکل را فعلاً نادیده بگیریم و فرض کنیم اشیاء ما رفتار بسیار آزادی دارند: ترجیح به ارتباط باز و نگهداری حالت خود بهصورت عمومی. در حالی که این رویکرد مشکل فوری را حل کرده و به ما اجازه میدهد در هر زمان از حالت اشیاء اسنپشات تهیه کنیم، هنوز مشکلات جدی وجود دارد. در آینده، اگر تصمیم به تغییر ساختار برخی از فیلدها بگیریم، این کار نیازمند تغییر کلاسهایی است که مسئول کپی کردن حالت اشیاء تحت تأثیر هستند.
و موارد بیشتری هم وجود دارد. بیایید به اسنپشات حالتهای ویرایشگر فکر کنیم. چه دادههایی را ذخیره خواهد کرد؟ حداقل، باید متن، مختصات مکاننما، موقعیت پیمایش فعلی و استایل را شامل شود. برای ساخت یک اسنپشات، باید این مقادیر را جمعآوری کرده و آنها را در یک ظرف قرار دهیم.
احتمالاً تعداد زیادی از این اشیاء ظرف را در یک لیست ذخیره خواهیم کرد که تاریخچه ویرایشگر را نمایش میدهد. بنابراین این ظروف احتمالاً به اشیاء یک کلاس تبدیل خواهند شد. این کلاس تقریباً هیچ متدی نخواهد داشت، فقط فیلدهایی که حالت ویرایشگر را منعکس میکنند. برای اجازه تعامل دیگر اشیاء با عکسهای فوری، احتمالاً باید تمام فیلدهای آن را عمومی کنیم. این کار تمام حالتهای ویرایشگر، خصوصی یا غیرخصوصی را فاش خواهد کرد. علاوه بر این، کلاسهای دیگر به هر تغییری که در کلاس اسنپشات اتفاق بیفتد وابسته خواهند بود، تغییراتی که در غیر این صورت در فیلدها و متدهای خصوصی بدون تأثیر بر کلاسهای بیرونی اتفاق میافتد.
ما یا باید تمام جزئیات داخلی کلاسها را فاش کنیم و آنها را بسیار آسیبپذیر کنیم، یا دسترسی به حالت آنها را محدود کنیم و تهیه اسنپشات را غیرممکن سازیم.
تمام مشکلاتی که تجربه کردیم، ناشی از نقض کپسولهسازی است. برخی اشیاء سعی میکنند کارهایی بیش از آنچه باید انجام دهند. برای جمعآوری دادههای مورد نیاز برای انجام برخی اقدامات، به حریم خصوصی دیگر اشیاء تجاوز میکنند بهجای اینکه اجازه دهند آنها عمل کنند.
الگوی Memento ایجاد اسنپشات حالت را به صاحب واقعی آن حالت، یعنی شیء مبدأ، واگذار میکند. بنابراین بهجای اینکه اشیاء دیگر سعی کنند حالت ویرایشگر را از «بیرون» کپی کنند، کلاس ویرایشگر خود میتواند عکس فوری بسازد زیرا بهطور کامل به حالت خود دسترسی دارد.
این الگو پیشنهاد میدهد که کپی حالت شیء را در یک شیء memento ذخیره کنیم. محتوای memento برای هیچ شیء دیگری جز شیء تولیدکننده آن قابلدسترس نیست. کلاسهای دیگر باید از طریق یک رابط محدود با mementoها ارتباط برقرار کنند که ممکن است اجازه بازیابی متاداده اسنپشات (مانند زمان ایجاد، عمل انجامشده و غیره) را بدهد، اما نه حالت اصلی شیء موجود در اسنپشات.
چنین سیاست محدودکنندهای به ما اجازه میدهد که mementoها را در دیگر اشیاء ذخیره کنیم، که معمولاً مراقب نامیده میشوند. بله، اصطلاحات این الگو کمی نامناسب است. از آنجایی که مراقب تنها از طریق یک رابط محدود با memento کار میکند، نمیتواند حالت ذخیرهشده در داخل memento را تغییر دهد. در عین حال، مبدأ به تمام فیلدهای داخل memento دسترسی دارد و میتواند حالت قبلی خود را هر زمان که بخواهد بازیابی کند.
ساختاردهی راهحل
الگوی Memento سه نوع پیادهسازی دارد:
پیادهسازی با استفاده از کلاسهای تو در تو
پیادهسازی کلاسیک این الگو به کلاسهای تو در تو متکی است. در این پیادهسازی، سه شرکتکننده وجود دارد:
- Originator: کلاس Originator میتواند عکسهایی از حالت خود تولید کند و همچنین در صورت نیاز، حالت خود را از عکسها بازگرداند.
- Memento: یک شیء مقداری است که بهعنوان عکسلحظهای از حالت Originator عمل میکند. معمولاً Memento را غیرقابل تغییر (immutable) میسازند و دادهها را فقط یکبار از طریق سازنده به آن میدهند.
- Caretaker: وظیفه Caretaker این است که بداند “چه زمانی” و “چرا” باید حالت Originator را ثبت کند و همچنین چه زمانی باید حالت را بازگرداند. یک Caretaker میتواند تاریخچه Originator را با ذخیره یک پشته از Mementoها دنبال کند. وقتی Originator نیاز به بازگشت به گذشته دارد، Caretaker بالاترین Memento را از پشته دریافت کرده و آن را به متد بازیابی Originator میفرستد.
در این پیادهسازی، کلاس Memento داخل Originator قرار میگیرد. این کار به Originator اجازه میدهد به فیلدها و متدهای Memento دسترسی داشته باشد، حتی اگر آنها بهصورت private تعریف شده باشند. از طرف دیگر، Caretaker دسترسی بسیار محدودی به فیلدها و متدهای Memento دارد، که به آن اجازه میدهد Mementoها را در یک پشته ذخیره کند بدون اینکه حالت آنها را تغییر دهد.
پیادهسازی با استفاده از اینترفیسهای میانی
این پیادهسازی جایگزین در مواردی مناسب است که امکان استفاده از کلاسهای تو در تو وجود نداشته باشد.
در صورت نبود کلاسهای تو در تو، میتوانیم دسترسی به فیلدهای Memento را با تعریف یک قرارداد محدود کنیم که در آن Caretaker فقط از طریق یک واسط میانی مشخص با Memento کار کند. این واسط فقط متدهایی مربوط به متاداده Memento را اعلام میکند.
از طرف دیگر، Originator میتواند مستقیماً با شیء Memento کار کند و به فیلدها و متدهای اعلامشده در کلاس Memento دسترسی داشته باشد. نقطهضعف این رویکرد این است که باید تمام اعضای Memento را بهصورت عمومی (public) تعریف کنیم.
پیادهسازی با استفاده از کپسولهسازی سختگیرانه
این پیادهسازی زمانی مفید است که نمیخواهیم حتی کوچکترین احتمالی برای دسترسی سایر کلاسها به حالت Originator از طریق Memento وجود داشته باشد.
این پیادهسازی اجازه میدهد انواع مختلف Originator و Memento داشته باشیم. هر Originator با یک کلاس Memento متناظر کار میکند. نه Originator و نه Memento حالت خود را به هیچکس دیگری افشا نمیکنند.
Caretakerها اکنون بهطور صریح از تغییر حالت ذخیرهشده در Mementoها منع شدهاند. علاوه بر این، کلاس Caretaker از Originator مستقل میشود، زیرا متد بازیابی اکنون در کلاس Memento تعریف شده است.
هر Memento به Originator که آن را تولید کرده است مرتبط میشود. Originator خودش را به همراه مقادیر حالتش به سازنده Memento میفرستد. به لطف رابطه نزدیک بین این کلاسها، یک Memento میتواند حالت Originator را بازگرداند، به شرطی که Originator تنظیمکنندههای مناسب (setters) را تعریف کرده باشد.
برای نشان دادن نحوه کار الگوی Memento، قصد داریم یک سیستم سفارشگیری برای یک رستوران سطح بالا ایجاد کنیم.
تصور کنید سیستمی داریم که رستوران باید اطلاعات مربوط به تأمینکنندگان مواد اولیه خود را ثبت کند. بهعنوان مثال، یک رستوران سطح بالا ممکن است مستقیماً از یک مزرعه محلی سفارش دهد و باید ثبت کند کدام مواد اولیه از کدام تأمینکننده آمدهاند.
در سیستم ما، باید اطلاعاتی که درباره یک تأمینکننده خاص وارد میکنیم را پیگیری کنیم و در صورت وارد کردن اشتباه، مثلاً یک آدرس غلط، بتوانیم آن اطلاعات را به حالت قبلی بازگردانیم. میتوانیم این کار را با استفاده از الگوی Memento انجام دهیم.
ابتدا کلاس Originator را ایجاد میکنیم، یعنی کلاس FoodSupplier
، که Mementoها را ایجاد و استفاده خواهد کرد:
using Memento.Memento;
namespace Memento.Originator
{
///
/// The Originator class, which is the class for which we want to save
/// Mementos for its state.
///
public class FoodSupplier
{
private string? name;
private string? phoneNumber;
private string? address;
public string Name
{
get => name;
set
{
name = value;
Console.WriteLine($"Proprietor: {name}");
}
}
public string PhoneNumber
{
get => phoneNumber;
set
{
phoneNumber = value;
Console.WriteLine($"Phone Number: {phoneNumber}");
}
}
public string Address
{
get => address;
set
{
address = value;
Console.WriteLine($"Address: {address}");
}
}
public FoodSupplierMemento SaveState()
{
Console.WriteLine("\nSaving current state\n");
return new FoodSupplierMemento(name, phoneNumber, address);
}
public void RestoreState(FoodSupplierMemento memento)
{
Console.WriteLine("\nRestoring previous state\n");
Name = memento.Name;
PhoneNumber = memento.PhoneNumber;
Address = memento.Address;
}
}
}
ما همچنین به یک شرکتکننده Memento نیاز داریم که کلاس FoodSupplierMemento
است و توسط FoodSupplier
استفاده میشود:
namespace Memento.Memento
{
///
/// The Memento class
///
public class FoodSupplierMemento
{
public string Name { get; set; }
public string PhoneNumber { get; set; }
public string Address { get; set; }
public FoodSupplierMemento(string name, string phoneNumber, string address)
{
Name = name;
PhoneNumber = phoneNumber;
Address = address;
}
}
}
در نهایت، به Caretaker نیاز داریم که Mementoها را ذخیره میکند اما هرگز آنها را بررسی یا تغییر نمیدهد. نام این کلاس را SupplierMemory
خواهیم گذاشت:
using Memento.Memento;
namespace Memento.Caretaker
{
///
/// The Caretaker class.
/// This class never examines the contents of any Memento and is
/// responsible for keeping that memento.
///
public class SupplierMemory
{
private FoodSupplierMemento memento;
public FoodSupplierMemento Memento
{
set { memento = value; }
get { return memento; }
}
}
}
اکنون، در متد Main()
خود میتوانیم شبیهسازی کنیم که یک تأمینکننده جدید اضافه میکنیم اما به اشتباه آدرس نادرست وارد میکنیم و سپس با استفاده از Memento دادههای قبلی را بازیابی میکنیم:
using Memento.Caretaker;
using Memento.Originator;
FoodSupplier supplier = new FoodSupplier
{
Name = Faker.Name.FullName(),
PhoneNumber = Faker.Phone.Number(),
Address = Faker.Address.StreetAddress()
};
SupplierMemory memory = new SupplierMemory();
memory.Memento = supplier.SaveState();
supplier.Address = Faker.Address.StreetAddress();
supplier.RestoreState(memory.Memento);
و خروجی دموی این الگو به شکل زیر خواهد بود:
مزایا و معایب الگوی طراحی Memento
مزایا
- میتوانیم بدون نقض کپسولهسازی، عکسهایی از حالت شیء تولید کنیم.
- میتوانیم کد Originator را با اجازه دادن به Caretaker برای مدیریت تاریخچه حالت Originator سادهتر کنیم.
معایب
- Caretakerها باید چرخه حیات Originator را دنبال کنند تا بتوانند Mementoهای منسوخ را از بین ببرند.
- اگر مشتریها بهطور مکرر Memento ایجاد کنند، برنامه ممکن است مقدار زیادی از RAM را مصرف کند.
روابط با الگوهای دیگر
- میتوانیم الگوهای Command و Memento را برای پیادهسازی قابلیت Undo با هم استفاده کنیم. در این حالت، دستورات مسئول انجام عملیات مختلف روی یک هدف هستند، در حالی که Mementoها حالت آن شیء را درست قبل از اجرای یک دستور ذخیره میکنند.
- میتوان از الگوی طراحی Memento همراه با Iterator برای ثبت وضعیت فعلی تکرار و بازگرداندن آن در صورت نیاز استفاده کرد.
- گاهی اوقات Prototypeها میتوانند جایگزین سادهتری برای Memento باشند. این روش زمانی مناسب است که شیء موردنظر برای ذخیرهسازی در تاریخچه ساده باشد و ارتباطی با منابع خارجی نداشته باشد یا این ارتباط بهراحتی قابل بازسازی باشد.
نکات پایانی
در این مقاله بررسی کردیم که الگوی Memento چیست، چه زمانی باید از آن استفاده کرد و مزایا و معایب این الگوی طراحی چیست. سپس به بررسی پیادهسازیهای مختلف و روابط الگوی Memento با دیگر الگوهای طراحی کلاسیک پرداختیم.
شایان ذکر است که الگوی Memento، همراه با سایر الگوهای طراحی ارائهشده توسط Gang of Four، یک راهحل همهجانبه یا نهایی برای طراحی برنامهها نیست. در نهایت این وظیفه مهندسین است که بررسی کنند چه زمانی از یک الگوی خاص استفاده کنند. این الگوها زمانی مفید هستند که بهعنوان ابزاری دقیق استفاده شوند، نه بهعنوان یک چکش بزرگ.