Wzorzec projektowy Pamiątka (Memento) to jeden z behawioralnych wzorców projektowych opisanych przez Gang of Four. W tym artykule poznasz jego specyfikę oraz dowiesz się, kiedy warto go wykorzystać. Przykłady kodu w artykule przygotowane zostały w TypeScripcie.
Ten wpis jest kolejnym wpisem z serii o wzorcach projektowych. Jeśli chcesz poznać inne wzorce projektowe lub dowiedzieć się czym są wzorce projektowe, to koniecznie sprawdź mój wpis o wzorcach projektowych.
Wzorzec projektowy Pamiątka (Memento) w teorii
Intencją wzorca Pamiątka (Memento) jest zapisanie wewnętrznego stanu obiektu w formie zewnętrznego obiektu (snapshotu). Dzięki temu mamy możliwość późniejszego przywrócenia stanu obiektowi. Stan oryginalnego obiektu (Originator) zapisywany jest w obiekcie pamiątki. Pamiątkami zarządza Caretaker bez możliwości ingerencji w ich zawartość. Obiekty pamiątki nie powinny być modyfikowane po stworzeniu. Caretaker nie zna wewnętrznej struktury Pamiątki ani sposobu jej tworzenia czy interpretacji – zarządza jedynie ich kolekcją.
Dzięki wykorzystaniu Pamiątki nie ma również konieczności psucia hermetyzacji stanu Originatora. Bez wykorzystania Memento pozyskanie stanu wymagałoby upublicznienia stanu obiektu. W wielu przypadkach publiczny stan jest niepożądany.
Rozwiązywany problem może budzić skojarzenia z Event Sourcingiem. Jest to słuszne skojarzenie. Zarówno wzorzec Pamiątki, jak i event sourcing rozwiązują podobne problemy. Jednak sposób rozwiązania problemu w obu przypadkach jest istotnie różny.
Memento przechowuje pełen stan obiektu na dany moment. W przypadku Event Sourcingu do czynienia mamy z szeregiem zdarzeń. W przeciwieństwie do Memento Event Sourcing nie przechowuje pełnego stanu — jego rekonstrukcja może wymagać przetworzenia wszystkich zdarzeń. Memento nie wymusza tworzenia snapshotu po każdej zmianie. Event Sourcing przechowuje pełną historię zdarzeń, co pozwala na odtworzenie stanu z dowolnego miejsca w historii.
Diagram klas
Poniższy diagram przedstawia strukturę klas typową dla wzorca Pamiątki.
Zastosowanie
Opis wzorca dość intuicyjnie pozwala znaleźć praktyczne przykłady jego wykorzystania. Z pewnością miałeś/aś okazję pracować z dowolnym programem do edycji plików z możliwością cofnięcia lub przywrócenia zmian. Jednym ze sposobów implementacji tego mechanizmu jest właśnie wzorzec Pamiątka.
Wzorzec Pamiątka znajdzie też zastosowanie w aplikacjach, które wymagają możliwości wykonania rollbacku. Przykładowo, możemy mieć aplikację pozwalającą na edycję szaty graficznej – np. rozmiaru i kroju czcionki, jednostek miary czy kolorystyki.
Wejście w ustawienia i wybranie konkretnej opcji natychmiastowo aplikuje zmiany, co pozwala użytkownikowi ocenić, czy dana konfiguracja mu odpowiada. Jednak zmiany te powinny zostać zapisane dopiero po ich zatwierdzeniu. W przypadku anulowania edycji chcemy cofnąć stan do tego sprzed rozpoczęcia zmian.
Aby to osiągnąć, możemy skorzystać ze wzorca Pamiątki. Przed rozpoczęciem edycji zapisujemy aktualną konfigurację jako Pamiątkę. Jeśli użytkownik kliknie „Anuluj”, przywracamy stan z Pamiątki.
Innym ciekawym przykładem praktycznego zastosowania jest zapisywanie i wczytywanie stanu gry. W takcie zapisu tworzony jest obiekt Pamiątki, a przy wczytywaniu jest aplikowany.
Praktyczny przykład
Przykład symuluje edytor tekstu z możliwością cofania zmian. Jest to przykład przewijający się w wielu materiałach. Jednak pozwala on w bardzo łatwy sposób zrozumieć jak w praktyce wykorzystać omawiany wzorzec. Dlatego również i ja z niego skorzystam.
Implementacja składa się z trzech klas:
TextDocument
(Originator) – zawiera stan dokumentu oraz wytwarzający Pamiątki. Stanem dokumentu jest jego tekstowa zawartość.DocumentSnapshot
(Memento) – przechowuje stan dokumentu z danej chwili. Zawartość dokumentu jest tylko do odczytu;TextDocumentHistory
(Caretaker) – przechowuje i udostępnia obiekty DocumentSnapshot;
class DocumentSnapshot {
constructor( private readonly documentContent: string ) {}
getContent(): string {
return this.documentContent;
}
}
class TextDocument {
private _content: string = '';
addContent( text: string ) {
this._content += text;
}
getContent(): string {
return this._content;
}
save(): DocumentSnapshot {
return new DocumentSnapshot( this._content );
}
restore( snapshot: DocumentSnapshot ) {
this._content = snapshot.getContent();
}
}
class TextDocumentHistory {
private history: DocumentSnapshot[] = [];
addRevision( snapshot: DocumentSnapshot) {
this.history.push( snapshot );
}
getLastRevision(): DocumentSnapshot | undefined {
return this.history.pop();
}
}
const editor = new TextDocument();
const textDocumentHistory = new TextDocumentHistory();
editor.addContent( 'Test content!' );
textDocumentHistory.addRevision( editor.save() );
editor.addContent( ' Incorrect content' );
console.log( editor.getContent() ); // Test content! Incorrect content
editor.restore( textDocumentHistory.getLastRevision()! );
console.log( editor.getContent() ); // Test content!
Podsumowanie
Jeśli masz jakiś ciekawy praktyczny przykład, gdzie udało Ci się wykorzystać Memento, to zachęcam do podzielenia się nim w komentarzu.
Książkę Design Patterns: Elements of Reusable Object-Oriented Software możesz kupić, klikając TEN link. Kupując z tego linku, wspierasz rozwój bloga.
Przygotuj się lepiej do rozmowy o pracę!
Odbierz darmowy egzemplarz e-booka 106 Pytań Rekrutacyjnych Junior JavaScript Developer i realnie zwiększ swoje szanse na rozmowie rekrutacyjnej! Będziesz też otrzymywać wartościowe treści i powiadomienia o nowych wpisach na skrzynkę e-mail.
Dlaczego warto?
E-booka odbierzesz korzystając z formularza poniżej 👇