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

Implementing and Integrating RabbitMQ in .NET Core Application: Shopping Cart and Order API

RabbitMQ is a robust message broker that enables communication between services in a decoupled, reliable manner. In this guide, we’ll implement RabbitMQ in a .NET Core application to connect two microservices: Shopping Cart API (Producer) and Order API (Consumer). 1. Prerequisites Install RabbitMQ locally or on a server. Default Management UI: http://localhost:15672 Default Credentials: guest/guest Install the RabbitMQ.Client package for .NET: dotnet add package RabbitMQ.Client 2. Architecture Overview Shopping Cart API (Producer): Sends a message when a user places an order. RabbitMQ : Acts as the broker to hold the message. Order API (Consumer): Receives the message and processes the order. 3. RabbitMQ Producer: Shopping Cart API Step 1: Install RabbitMQ.Client Ensure the RabbitMQ client library is installed: dotnet add package RabbitMQ.Client Step 2: Create the Producer Service Add a RabbitMQProducer class to send messages. RabbitMQProducer.cs : using RabbitMQ.Client; usin...

Clean Architecture: What It Is and How It Differs from Microservices

In the tech world, buzzwords like   Clean Architecture   and   Microservices   often dominate discussions about building scalable, maintainable applications. But what exactly is Clean Architecture? How does it compare to Microservices? And most importantly, is it more efficient? Let’s break it all down, from understanding the core principles of Clean Architecture to comparing it with Microservices. By the end of this blog, you’ll know when to use each and why Clean Architecture might just be the silent hero your projects need. What is Clean Architecture? Clean Architecture  is a design paradigm introduced by Robert C. Martin (Uncle Bob) in his book  Clean Architecture: A Craftsman’s Guide to Software Structure and Design . It’s an evolution of layered architecture, focusing on organizing code in a way that makes it  flexible ,  testable , and  easy to maintain . Core Principles of Clean Architecture Dependency Inversion : High-level modules s...

How Does My .NET Core Application Build Once and Run Everywhere?

One of the most powerful features of .NET Core is its cross-platform nature. Unlike the traditional .NET Framework, which was limited to Windows, .NET Core allows you to build your application once and run it on Windows , Linux , or macOS . This makes it an excellent choice for modern, scalable, and portable applications. In this blog, we’ll explore how .NET Core achieves this, the underlying architecture, and how you can leverage it to make your applications truly cross-platform. Key Features of .NET Core for Cross-Platform Development Platform Independence : .NET Core Runtime is available for multiple platforms (Windows, Linux, macOS). Applications can run seamlessly without platform-specific adjustments. Build Once, Run Anywhere : Compile your code once and deploy it on any OS with minimal effort. Self-Contained Deployment : .NET Core apps can include the runtime in the deployment package, making them independent of the host system's installed runtime. Standardized Libraries ...