Design Patterns,  SOLID

Memento

ממנטו היא תבנית עיצוב למימוש מנגנון שיחזור – כלומר לעשות undo או ctrl +z לפעולה שעשינו. 

לפעמים התבנית הזו נקראת snapshot בגלל שהיא שומרת ‘תמונת-מצב’ קודמת ומאפשרת לחזור אליה. 

הדרך הנוחה והמקובלת להסביר תבנית זו היא על ידי דוגמא של עורך טקסט.  

 נניח שיש לי עורך טקסט הממומש בקלאס בשם Editor או כפי שהוא נקרא במקור – Originator . אכניס לתוכו ערכים באופן הבא: 

 var editor = new Editor(); 

editor.Content = "a"; 
editor.Content = "b"; 
editor.Content = "c"; 

ואני מצפה שתהיה לי אפשרות לבטל את הפעולה / פעולות האחרונות. למשל שתהיה לי מתודה כזו: 

editor.undo(); 

איך הייתם מממשים מנגנון כזה? 

אם למשל הייתי מוסיף לעורך שלי שדה נוסף מסוג STRING בשם prevContent ובכל פעם שאני רוצה לשנות את הערך של CONTENT הייתי שומר לפני זה את הערך הנוכחי בשדה החדש, אז הייתי יכול לעשות UNDO רק פעם אחת, רק על השינוי האחרון. 

לכן אני צריך רשימה של ערכים קודמים. 

אבל ההגיון אומר שאני לא רוצה רק  התוכן, אלא גם את הכותרת, את צבע הרקע וכו’ וכו’ – כפי שאנחנו רגילים בכל תוכנה מצויה בשוק. 

אז מה שיצא זה שאצטרך על כל אחד מהאיברים של העורך שלי גם שדה בקלאס וגם רשימה של כל המצבים הקודמים של אותו השדה.  

כלומר – הרבה שדות וכנגדם הרבה רשימות. קצת בלאגן. 

אז איך נפתור את זה? 

ניצור קלאס חדש שמגלם את המצב של העורך ברגע נתון. נקרא לו Memento. על שמו נקראת התבנית הזו כי הוא מתאר את הרגע הנתון הספציפי. 

מה שנצטרך להחזיק כרגע הוא רק רשימה של הקלאס החדש שלנו. 

זהו פיתרון טוב יותר בגלל  

  1. שהוא מאפשר לנו ללכת אחורה הרבה פעמים  
  2. ובגלל שאנחנו לא מפוצצים את העורך ביותר מדי שדות. 

אבל עדיין יש לנו בעייה. הפיתרון הזה מפר את העקרון הנקרא SRP – . 

Single Responsibility Principle 

העקרון הזה אומר שלכל קלאס צריכה להיות רק אחריות אחת. והקלאס שלנו הוא גם עורך, אך הוא גם שומר על ההיסטוריה.  

אם כך – נרצה לשמור את ההיסטוריה בקלאס נפרד. 

ניצור קלאס חדש בשם CareTaker, ולקלאס זה תהיה רק אחריות אחת – שמירת ההיסטוריה . 

הקלאס הזה יכיל רשימה של Memento. 

כמו כן נצטרך שתי מתודות של הכנסה לרשימה והוצאה ממנה. 

עכשיו העורך הראשי שלנו יצטרך שתי מתודות חדשות – CreateState ו RestoreState. 

היחסים בין כל המחלקות יראה כך: 

המימוש בקוד יהיה כך: 

תהיה לנו מחלקה מקורית של העורך:

class Originator
    {
        public string Content { get; set; }

        public Memento CreateState()
        {
            return new Memento(Content);
        }

        public void Restore(Memento state)
        {
            Content = state.Content;
        }

        public string GetContent()
        {
            return Content;
        }
    }

כמובן, תהיה לנו מחלקת היסטוריה הנקראת Memento:

public class Memento
    {
        public readonly string Content;

        public Memento(string content)
        {
            Content = content;
        }


    }

ותהיה לנו מחלקת עזר לניהול הList.

 class Caretaker
    {
        private List<Memento> mementoes = new List<Memento>();

        public void Push(Memento state)
        {
            mementoes.Add(state);
        }

        public Memento Pop()
        {
            var lastIndex = mementoes.Count - 1;
            var lastState = mementoes[lastIndex];
            mementoes.Remove(lastState);
            return lastState;
        }
    }

נוכל להשתמש בקוד באופן הבא:

class Program
    {
        static void Main(string[] args)
        {
            var editor = new Originator();
            var history = new Caretaker();

            editor.Content = "a";
            history.Push(editor.CreateState());
          
            editor.Content = "b";
            history.Push(editor.CreateState());
            
            editor.Content = "c";
            editor.Restore(history.Pop());

            Console.WriteLine(editor.Content);
        }
    }