Wzorzec Memento - Cofanie zmian w aplikacji webowej bez chaosu!

Diagram przedstawiający memento pattern: Originator tworzy Memento, które Caretaker przechowuje, by przywrócić stan.

Napisano przez

Alex Jabłoński

Opublikowano

1 mar 2026

Spis treści

Wzorzec Memento przydaje się wtedy, gdy aplikacja musi bezpiecznie cofnąć stan obiektu, nie rozbijając jego enkapsulacji. Najczęściej chodzi o undo, ale równie dobrze o rollback po błędzie, przywracanie formularza do wcześniejszej wersji albo cofanie zmian w edytorze. Poniżej pokazuję, jak ten wzorzec działa, kiedy ma sens i jak wdrożyć go w aplikacji webowej bez nadmiernego komplikowania kodu.

Najważniejsze rzeczy do zapamiętania o wzorcu Memento

  • To wzorzec behawioralny służący do zapisywania i odtwarzania stanu obiektu bez ujawniania jego szczegółów implementacyjnych.
  • Opiera się na trzech rolach: originator tworzy migawkę, memento ją przechowuje, a caretaker zarządza historią.
  • Najlepiej sprawdza się przy funkcjach typu undo/redo, rollback i pracy na pojedynczym złożonym obiekcie.
  • Nie jest darmowy, bo każda migawka kosztuje pamięć i czas kopiowania.
  • Najbezpieczniej działa wtedy, gdy zapisujesz tylko taki stan, który naprawdę da się później odtworzyć.

Czym jest ten wzorzec i kiedy naprawdę się przydaje

Najkrócej: to technika, w której obiekt sam przygotowuje swoją migawkę, a reszta systemu tylko ją przechowuje i w odpowiednim momencie odtwarza. Ja lubię myśleć o niej jak o kontrolowanym punkcie przywracania, a nie o zwykłej kopii całego obiektu. Różnica jest ważna, bo w Memento nie chodzi o przypadkowe serializowanie wszystkiego, tylko o zapis tego, co ma znaczenie z perspektywy odzyskania stanu.

W praktyce wzorzec sprawdza się tam, gdzie użytkownik oczekuje możliwości powrotu do wcześniejszego kroku. Edytor tekstu, kreator wieloetapowego formularza, aplikacja do rysowania, konfigurator produktu, a nawet mechanizm rollback po nieudanej operacji w backendzie, to naturalne miejsca dla tego rozwiązania. Jeśli zmiana ma być odwracalna, a obiekt ma ukryte pola, wzorzec Memento daje czystszą odpowiedź niż ręczne wystawianie getterów do wszystkich danych.

W tym podejściu najważniejsze jest zachowanie granicy: obiekt wie, co trzeba zapisać, ale historyjka zmian nie musi wiedzieć, jak ten stan jest zbudowany. To właśnie trzyma architekturę w ryzach. Żeby zobaczyć, jak to działa od środka, trzeba rozdzielić trzy role i ich odpowiedzialności.

Jak działa zapis i odtwarzanie stanu

W klasycznej wersji masz trzy elementy. Originator to obiekt, którego stan chcesz chronić, na przykład edytor tekstu. Memento to zamknięta migawka tego stanu. Caretaker to magazyn historii, który wie, kiedy migawkę zapisać i kiedy ją przywrócić, ale nie zagląda do środka.

Przepływ jest prosty: przed operacją, którą da się cofnąć, originator tworzy snapshot. Caretaker odkłada go do stosu historii. Jeśli użytkownik wybierze cofnięcie, caretaker podaje migawkę z powrotem originatorowi, a ten odtwarza własne pola. W dobrze zaprojektowanej implementacji memento jest niemodyfikowalne, bo inaczej historia szybko zamienia się w przypadkową bazę danych do ręcznych poprawek.

Ja zwykle tłumaczę to tak: originator jest jedynym źródłem prawdy o swoim stanie, memento jest bezpieczną kopertą z kopią, a caretaker jest tylko listonoszem i archiwistą. Taki podział eliminuje pokusę, żeby z zewnątrz grzebać w prywatnych polach. Kiedy już wiesz, kto za co odpowiada, łatwiej przełożyć to na prosty kod w aplikacji webowej.

Praktyczny przykład w aplikacji webowej

W web development najczęściej spotkasz ten wzorzec w edytorach, formularzach i narzędziach typu canvas. Poniżej pokazuję uproszczony przykład w TypeScript. To nie jest najkrótszy możliwy kod, tylko taki, który dobrze pokazuje ideę i da się go odnieść do prawdziwej aplikacji.

type EditorState = {
  text: string;
  cursor: number;
  selectionStart: number;
  selectionEnd: number;
};

class EditorSnapshot {
  constructor(private readonly state: EditorState) {}

  restore(editor: Editor): void {
    editor.applyState(structuredClone(this.state));
  }
}

class Editor {
  private state: EditorState = {
    text: "",
    cursor: 0,
    selectionStart: 0,
    selectionEnd: 0,
  };

  setText(text: string): void {
    this.state = {
      ...this.state,
      text,
      cursor: text.length,
    };
  }

  setSelection(selectionStart: number, selectionEnd: number): void {
    this.state = {
      ...this.state,
      selectionStart,
      selectionEnd,
    };
  }

  createSnapshot(): EditorSnapshot {
    return new EditorSnapshot(structuredClone(this.state));
  }

  applyState(nextState: EditorState): void {
    this.state = nextState;
  }

  getState(): EditorState {
    return structuredClone(this.state);
  }
}

class History {
  private undoStack: EditorSnapshot[] = [];
  private redoStack: EditorSnapshot[] = [];

  save(snapshot: EditorSnapshot): void {
    this.undoStack.push(snapshot);
    this.redoStack = [];
  }

  undo(editor: Editor): void {
    const previous = this.undoStack.pop();
    if (!previous) return;

    this.redoStack.push(editor.createSnapshot());
    previous.restore(editor);
  }

  redo(editor: Editor): void {
    const next = this.redoStack.pop();
    if (!next) return;

    this.undoStack.push(editor.createSnapshot());
    next.restore(editor);
  }
}

Najważniejsze są tu dwie rzeczy. Po pierwsze, migawka jest tworzona z kopii stanu, a nie z referencji do tego samego obiektu. Po drugie, po nowej zmianie czyścimy stos redo, bo inaczej użytkownik mógłby wrócić do gałęzi historii, która już nie istnieje. To właśnie ten drobiazg często odróżnia działające undo od prowizorki.

W praktyce możesz ten sam schemat zastosować do formularza wieloetapowego, rysunku na canvasie albo konfiguratora filtrów. Jeśli stan jest zagnieżdżony, sięgnij po kopię głęboką albo projektuj go jako zestaw wartości, które da się bezbłędnie odtworzyć. Sama implementacja nie wystarczy jednak w każdym projekcie, bo koszt pamięci i złożoność szybko rosną.

Kiedy wybrać to rozwiązanie, a kiedy odpuścić

Nie każdy przypadek cofania zmian wymaga Memento. Czasem wystarczy prosty store stanu, czasem lepiej sprawdza się Command, a czasem zwykła kopia danych będzie wystarczająca. Kluczowe jest to, czy naprawdę potrzebujesz odtworzyć konkretny stan obiektu, czy raczej zapisać historię akcji.

Podejście Co zapisujesz Największy plus Typowe ograniczenie Kiedy ma sens
Memento Stan obiektu w danym momencie Chroni enkapsulację i pozwala wrócić do dokładnej migawki Zużywa pamięć przy częstych snapshotach Edytory, formularze, pojedyncze obiekty z historią
Command Akcję i dane potrzebne do jej cofnięcia Dobrze wspiera kolejkę operacji i redo Nie zawsze odtwarza stan tak precyzyjnie jak snapshot Systemy z komendami użytkownika i złożonym przepływem akcji
Serializacja całego obiektu Całą strukturę danych Szybko się ją wdraża Kruche przy zmianach schematu i łatwo narusza ukrywanie detali Prosty backup, eksport, prototyp
Zewnętrzny store z time travel Globalny stan aplikacji Centralizacja i łatwiejsze debugowanie Większa złożoność synchronizacji Aplikacje z jednym źródłem prawdy i rozbudowanym UI

Jeśli jedna migawka ma 100 KB, a zapisujesz ich 50, to zużywasz około 5 MB. Przy 1 MB na snapshot robi się już 50 MB i wtedy problem przestaje być akademicki. W aplikacjach webowych to potrafi wyjść bardzo szybko, szczególnie gdy zapisujesz stan po każdym znaku wpisanym przez użytkownika.

Właśnie dlatego nie traktuję tego wzorca jako domyślnego rozwiązania na wszystko. Gdy bardziej zależy ci na historii działań niż na stanie końcowym, Command zwykle daje lepszą strukturę. Po takim wyborze zostaje już tylko uniknięcie błędów wdrożeniowych, a tych widuję zaskakująco często.

Najczęstsze błędy przy wdrożeniu

Największy błąd to zapisywanie referencji zamiast kopii. Na papierze wygląda to poprawnie, ale po kilku zmianach wszystkie migawki pokazują ten sam, już zmodyfikowany obiekt. Drugi klasyk to upychanie w memento zbyt dużej ilości danych, także takich, które można obliczyć ponownie. To nie jest historia, tylko bałagan w przebraniu.

  • Udostępnianie wnętrza migawki - jeśli caretaker może czytać pola snapshotu, bardzo szybko tracisz sens całego wzorca.
  • Brak głębokiej kopii - przy obiektach zagnieżdżonych płytka kopia zwykle nie wystarcza.
  • Zapominanie o redo - po nowej edycji trzeba wyczyścić gałąź przyszłości, inaczej historia staje się sprzeczna.
  • Łączenie kilku niezależnych obiektów w jedną migawkę - działa tylko wtedy, gdy ich stan zmienia się razem i w tej samej logice.
  • Brak limitu historii - bez ograniczenia liczby snapshotów pamięć rośnie liniowo, a czas przeglądania historii też zaczyna boleć.

W praktyce warto też uważać na stan pochodny, czyli taki, który można wyliczyć z innych pól. Jeśli zapiszesz go osobno, a potem zmienisz logikę obliczeń, odtwarzanie zacznie zwracać niespójne wyniki. Po nazwaniu tych pułapek zostaje już ostatni krok, czyli ustawienie wzorca tak, żeby pomagał projektowi, a nie dokładał mu długu technicznego.

Jak wdrożyć go tak, żeby historia zmian nie zamieniła się w chaos

Gdy projektuję taki mechanizm, zaczynam od trzech pytań: co dokładnie odwracam, kto tworzy migawkę i jak długo historia ma żyć. To wystarcza, żeby odsiać większość niepotrzebnej złożoności. W aplikacjach webowych często najlepiej działa historyczny stos ograniczony do 20, 50 albo 100 kroków, zależnie od wielkości stanu i częstotliwości zmian. Nie ma tu jednej magicznej liczby, ale brak limitu prawie zawsze kończy się gorzej niż rozsądne ograniczenie.

  • Trzymaj historię blisko warstwy UI albo aplikacyjnej, a nie w samym modelu domenowym, jeśli zaczyna to mieszać odpowiedzialności.
  • Projektuj migawki jako obiekty niemutowalne, nawet jeśli oznacza to dodatkową kopię danych przy zapisie.
  • Jeśli stan jest duży, rozważ checkpointy co kilka kroków zamiast pełnego snapshotu po każdej drobnej zmianie.
  • Jeżeli liczysz na audyt, nie myl go z undo. Audyt chce wiedzieć, co się zmieniło, a undo chce po prostu wrócić do poprzedniego stanu.
  • Gdy operacje są ważniejsze niż stan, połącz Memento z Command, zamiast zmuszać jeden wzorzec do wszystkiego.

To podejście dobrze skaluje się w praktyce, bo daje jasny podział ról i nie zmusza całej aplikacji do znajomości szczegółów obiektu. Jeśli dobrze ustawisz granice odpowiedzialności, wzorzec Memento pozostaje prosty, a użytkownik dostaje dokładnie to, czego oczekuje: pewny powrót do poprzedniego stanu bez chaosu w kodzie.

FAQ - Najczęstsze pytania

Memento to wzorzec behawioralny, który pozwala na zapisywanie i odtwarzanie poprzedniego stanu obiektu bez naruszania jego enkapsulacji. Działa jak "punkt przywracania", umożliwiając cofanie zmian, np. w edytorach czy formularzach.

Wzorzec Memento jest idealny, gdy potrzebujesz funkcji undo/redo, rollbacku po błędzie, lub przywracania formularzy do wcześniejszych wersji. Sprawdza się w systemach, gdzie użytkownik oczekuje możliwości powrotu do poprzedniego stanu obiektu.

Wzorzec Memento opiera się na trzech rolach: Originator (obiekt, którego stan jest zapisywany), Memento (migawka stanu Originatora) i Caretaker (zarządza historią migawek, decydując kiedy zapisać lub przywrócić stan).

Główną wadą Memento jest zużycie pamięci, zwłaszcza przy częstym tworzeniu migawek dużych obiektów. Każda migawka to kopia stanu, co może prowadzić do szybkiego wzrostu zapotrzebowania na zasoby, jeśli nie jest odpowiednio zarządzane.

Memento zapisuje stan obiektu w danym momencie, pozwalając na powrót do konkretnej migawki. Command natomiast zapisuje akcje i dane potrzebne do ich cofnięcia. Memento jest lepsze do precyzyjnego odtwarzania stanu, Command do zarządzania historią operacji.

Oceń artykuł

Ocena: 0.00 Liczba głosów: 0

Tagi:

memento pattern wzorzec memento w aplikacjach webowych jak wdrożyć wzorzec memento kiedy stosować wzorzec memento

Udostępnij artykuł

Alex Jabłoński

Alex Jabłoński

Nazywam się Alex Jabłoński i od 9 lat zajmuję się programowaniem webowym. Moja przygoda z tą dziedziną zaczęła się od prostych projektów, które z czasem przerodziły się w pasję do tworzenia użytecznych i estetycznych aplikacji internetowych. Fascynuje mnie nie tylko sam proces kodowania, ale także to, jak technologie wpływają na nasze życie i jak możemy je wykorzystać, aby rozwiązywać codzienne problemy. Piszę o różnych aspektach programowania, od podstawowych języków po bardziej zaawansowane techniki i narzędzia. Staram się, aby moje teksty były przystępne i zrozumiałe, a skomplikowane zagadnienia przedstawiam w prosty sposób. Regularnie śledzę nowinki w branży, co pozwala mi dostarczać aktualne i rzetelne informacje. Moim celem jest nie tylko edukacja, ale także inspirowanie innych do rozwijania swoich umiejętności w programowaniu.

Napisz komentarz