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
- Tight Coupling: The
TextEditorHistory
class directly accesses and manipulates theText
property of theTextEditor
class. This creates a tight coupling between these classes, making the code less flexible and harder to maintain. - Encapsulation Violation: The internal state of the
TextEditor
is exposed through theText
property. This breaks encapsulation, as the state management logic is not encapsulated within theTextEditor
class. - 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. - Code Duplication and Complexity: The
TextEditorHistory
class has to manage the state history manually, leading to potential code duplication and increased complexity.
- Encapsulation: The Memento Pattern encapsulates the state of the
TextEditor
within theTextEditorMemento
class. TheTextEditor
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. - Single Responsibility: The
TextEditor
class is responsible for managing its own state, while theTextEditorHistory
class is responsible for managing the history of states. This separation of concerns leads to cleaner and more maintainable code. - State Integrity: The
TextEditorMemento
class captures the entire state of theTextEditor
. If the state of theTextEditor
becomes more complex, the memento can be extended to include additional state information, ensuring complete and accurate state restoration. - Simplified State Management: The
TextEditorHistory
class uses the memento objects to manage the state history, reducing code duplication and complexity. TheTextEditor
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
- State Preservation: Identify scenarios where preserving and restoring an object's state is required.
- Encapsulation: Ensure that the state needs to be captured and externalized without violating the object's encapsulation.
- Undo Functionality: Consider the Memento pattern when implementing undo-redo functionality in an application.
- 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
Post a Comment