Skip to main content

Understanding the Memento Design Pattern in C#

 

The Memento design pattern is a behavioral pattern that allows you to capture and externalize an object's internal state without violating encapsulation, so the object can be restored to this state later. This pattern is particularly useful for implementing undo-redo functionality.

Understanding the State Design Pattern in C#

Let's consider a scenario where we have a TextEditor class that allows text editing and supports undo functionality. The TextEditor can save its state (the current text) and restore it to a previous state.

Example without Memento Design Pattern

using System;
using System.Collections.Generic;

namespace WithoutMementoPattern
{
    // Originator class
    class TextEditor
    {
        public string Text { get; private set; }

        public void SetText(string text)
        {
            Text = text;
            Console.WriteLine($"Current Text: {Text}");
        }
    }

    // Caretaker class
    class TextEditorHistory
    {
        private Stack<string> _history = new Stack<string>();

        public void SaveState(TextEditor editor)
        {
            _history.Push(editor.Text);
            Console.WriteLine("State saved.");
        }

        public void Undo(TextEditor editor)
        {
            if (_history.Count > 0)
            {
                string previousText = _history.Pop();
                editor.SetText(previousText);
                Console.WriteLine("State restored.");
            }
            else
            {
                Console.WriteLine("No states to restore.");
            }
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            TextEditor editor = new TextEditor();
            TextEditorHistory history = new TextEditorHistory();

            editor.SetText("Version 1");
            history.SaveState(editor);

            editor.SetText("Version 2");
            history.SaveState(editor);

            editor.SetText("Version 3");

            history.Undo(editor); // Should restore to "Version 2"
            history.Undo(editor); // Should restore to "Version 1"
            history.Undo(editor); // No more states to restore
        }
    }
}

Problems in the Non-Pattern Approach

  1. Tight Coupling: The TextEditorHistory class directly accesses and manipulates the Text property of the TextEditor class. This creates a tight coupling between these classes, making the code less flexible and harder to maintain.

  2. Encapsulation Violation: The internal state of the TextEditor is exposed through the Text property. This breaks encapsulation, as the state management logic is not encapsulated within the TextEditor class.

  3. Lack of State Integrity: The history is stored as plain strings, which may not capture all relevant aspects of the state. If the state of the TextEditor becomes more complex, this approach will not scale well and may lead to incomplete or incorrect state restoration.

  4. Code Duplication and Complexity: The TextEditorHistory class has to manage the state history manually, leading to potential code duplication and increased complexity.
How Memento Pattern Solves These Problems
  1. Encapsulation: The Memento Pattern encapsulates the state of the TextEditor within the TextEditorMemento class. The TextEditor class manages its own state and provides methods to save and restore it. This encapsulation ensures that the internal state is not exposed or manipulated directly by other classes.

  2. Single Responsibility: The TextEditor class is responsible for managing its own state, while the TextEditorHistory class is responsible for managing the history of states. This separation of concerns leads to cleaner and more maintainable code.

  3. State Integrity: The TextEditorMemento class captures the entire state of the TextEditor. If the state of the TextEditor becomes more complex, the memento can be extended to include additional state information, ensuring complete and accurate state restoration.

  4. Simplified State Management: The TextEditorHistory class uses the memento objects to manage the state history, reducing code duplication and complexity. The TextEditor class provides a clear interface for saving and restoring its state, simplifying state management.

Revisited Code with Memento Pattern

Here is how we can implement this pattern :

using System;
using System.Collections.Generic;

namespace MementoPattern
{
    // Memento class
    class TextEditorMemento
    {
        public string Text { get; private set; }

        public TextEditorMemento(string text)
        {
            Text = text;
        }
    }

    // Originator class
    class TextEditor
    {
        public string Text { get; private set; }

        public void SetText(string text)
        {
            Text = text;
            Console.WriteLine($"Current Text: {Text}");
        }

        public TextEditorMemento Save()
        {
            return new TextEditorMemento(Text);
        }

        public void Restore(TextEditorMemento memento)
        {
            Text = memento.Text;
            Console.WriteLine($"Restored Text: {Text}");
        }
    }

    // Caretaker class
    class TextEditorHistory
    {
        private Stack<TextEditorMemento> _history = new Stack<TextEditorMemento>();

        public void SaveState(TextEditor editor)
        {
            _history.Push(editor.Save());
            Console.WriteLine("State saved.");
        }

        public void Undo(TextEditor editor)
        {
            if (_history.Count > 0)
            {
                TextEditorMemento memento = _history.Pop();
                editor.Restore(memento);
                Console.WriteLine("State restored.");
            }
            else
            {
                Console.WriteLine("No states to restore.");
            }
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            TextEditor editor = new TextEditor();
            TextEditorHistory history = new TextEditorHistory();

            editor.SetText("Version 1");
            history.SaveState(editor);

            editor.SetText("Version 2");
            history.SaveState(editor);

            editor.SetText("Version 3");

            history.Undo(editor); // Should restore to "Version 2"
            history.Undo(editor); // Should restore to "Version 1"
            history.Undo(editor); // No more states to restore
        }
    }
}

Why Can't We Use Other Design Patterns Instead?

  • Command Pattern: The Command pattern encapsulates a request as an object, thereby allowing for parameterization and queuing of requests. It is more suited for handling operations and requests rather than preserving state.
  • Prototype Pattern: The Prototype pattern creates objects by copying existing objects (prototypes). It is not designed for preserving and restoring state.
  • Observer Pattern: The Observer pattern defines a one-to-many dependency between objects, where one object notifies its observers of any state changes. It does not handle state preservation and restoration.

Steps to Identify Use Cases for the Memento Pattern

  1. State Preservation: Identify scenarios where preserving and restoring an object's state is required.
  2. Encapsulation: Ensure that the state needs to be captured and externalized without violating the object's encapsulation.
  3. Undo Functionality: Consider the Memento pattern when implementing undo-redo functionality in an application.
  4. State Management: Use the Memento pattern to manage the history of states in applications where state changes frequently.

By following these steps and implementing the Memento pattern, you can achieve encapsulated state preservation and restoration, enabling undo functionality and improving state management in your application.

Comments

Popular posts from this blog

C# : How can we access private method outside class

Introduction In object-oriented programming, encapsulation is a fundamental principle that restricts direct access to the internal implementation details of a class. Private methods, being part of this internal implementation, are designed to be accessible only within the confines of the class they belong to. However, there might be scenarios where you need to access a private method from outside the class. In this blog post, we'll explore several techniques to achieve this in C#. 1. Reflection: A Powerful Yet Delicate Approach Reflection is a mechanism in C# that allows inspecting and interacting with metadata about types, fields, properties, and methods. While it provides a way to access private methods, it should be used cautiously due to its potential impact on maintainability and performance. using System ; using System . Reflection ; public class MyClass { private void PrivateMethod ( ) { Console . WriteLine ( "This is a private method."

C# : Understanding Types of Classes

In C#, classes serve as the building blocks of object-oriented programming, providing a blueprint for creating objects. Understanding the types of classes and their applications is crucial for designing robust and maintainable software. In this blog, we’ll delve into various types of classes in C#, accompanied by real-world scenarios and code snippets for a practical understanding. 1. Regular (Instance) Classes Definition: Regular classes are the most common type and are used to create instances or objects. They can contain fields, properties, methods, and other members. Example Scenario: A Person class representing individual persons with properties like Name and Age. public class Person { public string Name { get ; set ; } public int Age { get ; set ; } } 2. Static Classes Definition: A static class cannot be instantiated and can only contain static members (methods, properties, fields). It’s often used for utility functions. Example Scenario: A MathUtility cla

20+ LINQ Concepts with .Net Code

LINQ   (Language Integrated Query) is one of the most powerful features in .NET, providing a unified syntax to query collections, databases, XML, and other data sources. Below are 20+ important LINQ concepts, their explanations, and code snippets to help you understand their usage. 1.  Where  (Filtering) The  Where()  method is used to filter a collection based on a given condition. var numbers = new List < int > { 1 , 2 , 3 , 4 , 5 , 6 } ; var evenNumbers = numbers . Where ( n => n % 2 == 0 ) . ToList ( ) ; // Output: [2, 4, 6] C# Copy 2.  Select  (Projection) The  Select()  method projects each element of a sequence into a new form, allowing transformation of data. var employees = new List < Employee > { /* ... */ } ; var employeeNames = employees . Select ( e => e . Name ) . ToList ( ) ; // Output: List of employee names C# Copy 3.  OrderBy  (Sorting in Ascending Order) The  OrderBy()  method sorts the elements of a sequence in ascendi