Wiosenna WYPRZEDAŻ

Pamiątka

Znany też jako: Snapshot, Memento

Cel

Pamiątka to behawioralny wzorzec projektowy pozwalający zapisywać i przywracać wcześniejszy stan obiektu bez ujawniania szczegółów jego implementacji.

Wzorzec projektowy Pamiątka

Problem

Wyobraź sobie, że tworzysz edytor tekstu. Poza zwykłym edytowaniem treści, edytor może ją formatować, wstawiać obrazki, itd.

W jakimś momencie postanawiasz pozwolić użytkownikom cofać dowolną operację wykonaną na tekście. Funkcja taka stała się przez lata popularna, a użytkownicy oczekują jej w każdej aplikacji. W celu implementacji obierasz podejście bezpośrednie. Przed wykonaniem dowolnego działania, aplikacja zapamiętuje stan wszystkich obiektów i zapisuje go w jakimś magazynie. Gdy użytkownik zechce wycofać jakąś zmianę, aplikacja pobiera ostatnią migawkę z historii i za jej pomocą przywraca wcześniejszy stan wszystkich obiektów.

Cofanie działań w edytorze

Przed wykonaniem działania, aplikacja zapisuje migawkę stanu obiektów. Pozwoli ona później przywrócić obiekty do ich poprzedniego stanu.

Pomyślmy o tych migawkach. Jak dokładnie je tworzyć? Prawdopodobnie trzeba byłoby przejrzeć wszystkie pola obiektu, skopiować ich wartości i zapisać w jakimś magazynie. Jednak to zadziałałoby tylko jeśli obiekt ma stosunkowo luźne ograniczenia dostępu do swojej zawartości. Niestety, większość prawdziwych obiektów nie pozwoli obcym tak łatwo zajrzeć w swoją zawartość i najistotniejsze dane będą ukryte w polach prywatnych.

Na razie zignorujmy jednak ten problem i załóżmy, że nasze obiekty są niczym hipisi: wolą otwarte związki i nie ukrywają swojej natury. Chociaż podejście takie pomija powyższe ograniczenie i pozwala wykonać migawkę stanu obiektów, to tworzy przy okazji inne problemy. W przyszłości bowiem możemy zdecydować o refaktoryzacji niektórych klas edytora, lub dodać, bądź usunąć pola. Brzmi łatwo, ale wymagałoby przy okazji zmiany klas odpowiedzialnych za kopiowanie stanów zmienianych obiektów.

Jak skopiować prywatną część stanu obiektu?

Jak skopiować prywatną część stanu obiektu?

Ale jest coś jeszcze. Rozważmy samą “migawkę” stanu edytora. Jakie dane zawierałaby? W najprostszej formie: tekst, współrzędne kursora, bieżącą pozycję przewijania, itd. Aby wykonać migawkę, trzeba zebrać te wartości i umieścić je w jakimś kontenerze.

Najprawdopodobniej będzie trzeba przechowywać mnóstwo takich obiektów kontenerowych w formie listy reprezentującej historię. Dlatego też kontenery zapewne byłyby obiektami jednej klasy. Klasa ta nie miałaby prawie żadnej metody, ale za to wiele pól, odzwierciedlających stan edytora. Aby pozwolić innym obiektom zapisywać i odczytywać dane migawki, trzeba by uczynić jej pola publicznymi. Ale to ujawniłoby wszystkie stany edytora, także te prywatne. Inne klasy stałyby się zależne od najdrobniejszej zmiany klasy migawki, które w przeciwnym wypadku działyby się w obrębie jej prywatnych pól i metod bez wpływu na zewnętrzne klasy.

Wygląda na to, że trafiliśmy w ślepą uliczkę: można albo eksponować wszystkie wewnętrzne szczegóły klas, czyniąc je delikatnymi, albo ograniczyć dostęp do ich stanu, uniemożliwiając tym samym wykonywanie migawek. Czy jest jakiś inny sposób na implementację "cofnij"?

Rozwiązanie

Wszystkie problemy na jakie się natknęliśmy są spowodowane niewłaściwą hermetyzacją. Niektóre obiekty próbują robić więcej niż powinny. Aby zbierać dane w celu wykonania jakiegoś zadania, wkradają się w prywatną przestrzeń innych obiektów, zamiast pozwolić im na samodzielne wykonanie tego zadania.

Wzorzec Pamiątka deleguje tworzenie migawki stanu samemu właścicielowi stanu — obiektowi źródło. Dlatego też, zamiast pozwalać innym obiektom próbować skopiować stan edytora “z zewnątrz”, sama klasa edytora może wykonać migawkę siebie, gdyż ma pełny dostęp do swojego stanu.

Wzorzec proponuje przechowywanie kopii stanu w specjalnym obiekcie zwanym pamiątką. Zawartość pamiątki nie jest dostępna innym obiektom, oprócz jej twórcy. Inne obiekty muszą komunikować się z pamiątką za pośrednictwem ograniczonego interfejsu, który pozwala na pobieranie metadanych migawki (czas utworzenia, nazwa wykonanej operacji, itd.), ale nie stanu pierwotnego obiektu zawartego w migawce.

Źródło ma pełen dostęp do pamiątki, zaś zarządca tylko do jej metadanych

Źródło ma pełen dostęp do pamiątki, zaś zarządca tylko do jej metadanych.

Tak restrykcyjna polityka pozwala przechowywać pamiątki w obrębie innych obiektów, zwykle zwanych zarządcami. Skoro zarządca współpracuje z pamiątką tylko za pośrednictwem ograniczonego interfejsu, to nie jest w stanie naruszyć stanu w niej przechowywanego. Ponadto, źródło ma pełen dostęp do wszystkich pól pamiątki, co pozwala na przywracanie poprzednich stanów wedle potrzeb.

W naszym przykładzie edytora tekstowego możemy stworzyć osobną klasę historii która przyjmie rolę zarządcy. Stos pamiątek przechowany w zarządcy powiększy się przed każdą operacją edytora. Można nawet przedstawić reprezentację tego stosu w graficznym interfejsie użytkownika aplikacji, wyświetlając historię wcześniej wykonanych operacji.

Gdy użytkownik wywoła wycofanie operacji, historia pobierze najnowszą pamiątkę ze stosu i przekaże ją edytorowi z żądaniem cofnięcia. Edytor ma pełny dostęp do pamiątki, więc zmieni swój stan w oparciu o wartości w niej zawarte.

Struktura

Implementacja w oparciu o zagnieżdżone klasy

Klasyczna implementacja wzorca polega na zagnieżdżaniu klas, które jest możliwe w wielu popularnych językach programowania (jak C++, C# i Java).

Pamiątka w oparciu o zagnieżdżanie klasPamiątka w oparciu o zagnieżdżanie klas
  1. Klasa Źródło może tworzyć migawki swego stanu, a także przywracać wcześniejszy stan z migawek, gdy zachodzi taka potrzeba.

  2. Pamiątka to obiekt wartości pełniący rolę pamiątki stanu źródła. Popularną praktyką jest czynienie pamiątki niezmienialną i ustawianie jej danych jednorazowo — poprzez konstruktor.

  3. Zarządca wie nie tylko “kiedy” i “po co” rejestrować stan zarządcy, ale także kiedy należy przywrócić wcześniejszy stan.

    Zarządca może śledzić historię źródła przechowując stos pamiątek. Gdy źródło musi wrócić się w przeszłość, zarządca pobiera najnowszą pamiątkę ze stosu i przekazuje ją metodzie przywracającej źródła.

  4. W tej implementacji klasa pamiątka jest zagnieżdżona wewnątrz klasy źródła. Pozwala to źródłu mieć dostęp do pól i metod pamiątki, mimo że są zadeklarowane jako prywatne. Z drugiej strony, zarządca ma bardzo ograniczony dostęp do pól i metod pamiątek, co pozwala mu przechowywać je w formie stosu ale bez możliwości zmiany ich stanu.

Implementacja na podstawie interfejsu pośredniego

Istnieje alternatywna implementacja, odpowiednia w przypadku języków programowania które nie wspierają zagnieżdżania klas (mówię o tobie, PHP!).

Pamiątka bez użycia klas zagnieżdżonychPamiątka bez użycia klas zagnieżdżonych
  1. Wobec niemożności zagnieżdżania klas można ograniczyć dostęp do pól pamiątki stosując konwencję według której zarządcy współdziałają z pamiątkami wyłącznie za pośrednictwem jasno zadeklarowanego interfejsu pośredniego. Taki interfejs deklarowałby tylko metody związane z metadanymi pamiątki.

  2. Z drugiej strony, źródła mogą współpracować z pamiątkami bezpośrednio, poprzez dostęp do pól i metod w nich zadeklarowanych. Wadą tego podejścia jest konieczność deklaracji wszystkich składowych pamiątki jako publiczne.

Implementacja z jeszcze ściślejszą hermetyzacją

Kolejna implementacja jest użyteczna, gdy nie chcemy pozostawić nawet najmniejszego ryzyka, że obce klasy uzyskają dostęp do stanu źródła za pośrednictwem pamiątki.

Pamiątka ze ścisłą hermetyzacjąPamiątka ze ścisłą hermetyzacją
  1. Ta implementacja pozwala mieć wiele typów źródeł i pamiątek. Każde źródło współdziała z odpowiednią dla niego klasą pamiątki. Ani źródła, ani pamiątki nie eksponują swojego stanu komukolwiek.

  2. Zarządcom uniemożliwiono teraz zmianę stanu przechowywanego w pamiątkach. Co więcej, klasa zarządcy staje się niezależna od źródła, ponieważ metoda przywracająca stan jest teraz zdefiniowana w klasie pamiątki.

  3. Każda pamiątka staje się powiązana ze źródłem które ją utworzyło. Źródło przekazuje samo siebie do konstruktora pamiątki wraz z wartościami opisującymi jego stan. Dzięki bliskiemu związkowi pomiędzy tymi klasami, pamiątka może przywrócić stan źródła, gdyż ten drugi ma zdefiniowane stosowne metody setter.

Pseudokod

W poniższym przykładzie zastosowano wzorce Pamiątka oraz Polecenie w celu przechowywania migawek stanu rozbudowanego edytora tekstu i przywracania wcześniejszego stanu z owych migawek gdy zajdzie potrzeba.

Struktura przykładu użycia wzorca Pamiątka

Zapisywanie migawek stanu edytora tekstu.

Obiekty typu polecenie pełnią rolę zarządców. Pobierają pamiątkę edytora przed wykonaniem działań związanych z poleceniem. Gdy użytkownik spróbuje cofnąć ostatnio wykonane polecenie, edytor może skorzystać z pamiątki przechowanej w tym poleceniu aby przywrócić wcześniejszy stan siebie.

Klasa pamiątka nie deklaruje żadnych pól publicznych, getterów, ani setterów. Dzięki temu żaden obiekt nie może zmienić zawartości pamiątki. Pamiątki są powiązane z tym obiektem edytora, który je utworzył. Pozwala to pamiątce przywrócić stan związanego z nią edytora przekazując przechowywane dane obiektowi edytora za pośrednictwem funkcji setter. Ponieważ pamiątki są połączone z konkretnymi obiektami edytorów, aplikacja mogłaby wspierać wiele niezależnych okien edycji ze scentralizowanym stosem historii.

// Źródło posiada jakieś istotne dane które mogą się z czasem
// zmienić. Definiuje także metodę służącą zapisywaniu swojego
// stanu wewnątrz pamiątki i inną metodę do przywracania swojego
// stanu na podstawie pamiątki.
class Editor is
    private field text, curX, curY, selectionWidth

    method setText(text) is
        this.text = text

    method setCursor(x, y) is
        this.curX = x
        this.curY = y

    method setSelectionWidth(width) is
        this.selectionWidth = width

    // Zapisuje bieżący stan w pamiątce.
    method createSnapshot():Snapshot is
        // Pamiątka to niezmienialny obiekt i dlatego źródło
        // przekazuje swój stan pamiątce w formie parametru
        // konstruktora.
        return new Snapshot(this, text, curX, curY, selectionWidth)

// Pamiątka przechowuje przeszły stan edytora.
class Snapshot is
    private field editor: Editor
    private field text, curX, curY, selectionWidth

    constructor Snapshot(editor, text, curX, curY, selectionWidth) is
        this.editor = editor
        this.text = text
        this.curX = x
        this.curY = y
        this.selectionWidth = selectionWidth

    // W którymś momencie będzie można przywrócić poprzedni stan
    // edytora korzystając z obiektu pamiątki.
    method restore() is
        editor.setText(text)
        editor.setCursor(curX, curY)
        editor.setSelectionWidth(selectionWidth)

// Obiekt polecenie może pełnić rolę nadzorcy. W takim przypadku
// polecenie zapisuje w sobie pamiątkę zanim zmieni stan źródła.
// Gdy otrzyma rozkaz wycofania działania, przywraca stan źródła
// na podstawie zachowanej pamiątki.
class Command is
    private field backup: Snapshot

    method makeBackup() is
        backup = editor.createSnapshot()

    method undo() is
        if (backup != null)
            backup.restore()
    // ...

Zastosowanie

Stosuj wzorzec Pamiątka gdy chcesz tworzyć migawki stanu obiektu i móc przywracać poprzedni jego stan.

Wzorzec Pamiątka pozwala tworzyć pełne kopie stanu obiektu, wraz z jego danymi prywatnymi i przechowywać je poza obiektem. Chociaż większość kojarzy ten wzorzec z przypadkiem użycia “cofnij”, to jest on również nieodzowny w przypadku transakcji (np. gdy chcesz cofnąć wykonywane działanie w razie pojawienia się błędu).

Stosuj ten wzorzec gdy bezpośredni dostęp do pól/getterów/setterów obiektu psuje hermetyzację.

Pamiątka czyni obiekt odpowiedzialnym za tworzenie migawek swojego stanu. Żaden inny obiekt nie ma prawa odczytać migawki, co zabezpiecza stan pierwotnego obiektu.

Jak zaimplementować

  1. Określ która klasa będzie pełniła rolę źródła. Ważne jest ustalenie czy program będzie miał jeden centralny obiekt tego typu, czy parę mniejszych.

  2. Stwórz klasę pamiątki. Jeden po drugim zadeklaruj zestaw pól odzwierciedlających pola zadeklarowane w klasie źródła.

  3. Uczyń klasę pamiątki niezmienialną. Pamiątka powinna przyjmować dane tylko raz — za pośrednictwem konstruktora. Klasa nie powinna mieć metod setter.

  4. Jeśli stosowany język programowania wspiera zagnieżdżane klasy, zagnieźdź pamiątkę w obrębie klasy źródła. Jeśli nie wspiera, wyekstrahuj pusty interfejs z klasy pamiątka i spraw, by inne obiekty korzystały z niego w kontakcie z pamiątką. Można dodać do takiego interfejsu funkcje związane z metadanymi, ale żadnych funkcji eksponujących stan źródła.

  5. Dodaj metodę tworzącą pamiątki do klasy źródła. Źródło powinno przekazywać swój stan do pamiątki za pośrednictwem jednego lub wielu argumentów konstruktora pamiątki.

    Typem zwracanym przez metodę powinien być interfejs wyekstrahowany w poprzednim etapie (zakładając, że w ogóle się go wyekstrahowało). Za kulisami, metoda tworząca pamiątkę powinna współdziałać bezpośrednio z klasą pamiątka.

  6. Dodaj do klasy źródła metodę służącą przywracaniu stanu. Powinna przyjmować w charakterze argumentu obiekt typu pamiątka. Jeśli wyekstrahowano interfejs w poprzednim etapie, uczyń go typem parametru. W tym przypadku, trzeba rzutować obiekt przyjmowany na klasę pamiątka, ponieważ źródło musi mieć pełen dostęp do tego obiektu.

  7. Zarządca, niezależnie od tego czy reprezentuje obiekt polecenie, historię lub coś jeszcze innego, powinien wiedzieć kiedy żądać nowych pamiątek od źródła, jak je przechowywać i kiedy przywracać stan źródła za pomocą jakiejś pamiątki.

  8. Połączenie między zarządcami a źródłami można przenieść do klasy pamiątka. W tym przypadku, każda pamiątka musi być połączona ze źródłem które je utworzyło. Metoda przywracająca również trafiłaby do klasy pamiątka. To wszystko jednak ma sens tylko jeśli klasa pamiątka jest zagnieżdżona wewnątrz klasy źródła lub jeśli klasa źródła udostępnia funkcje setter służące nadpisywaniu jej stanu.

Zalety i wady

  • Można tworzyć migawki stanu obiektów bez naruszania ich hermetyzacji.
  • Można uprościć kod źródła, pozwalając zarządcy śledzić historię stanu źródła.
  • Aplikacja może wymagać dużej ilości pamięci RAM jeśli klienci zbyt często będą tworzyć pamiątki.
  • Zarządcy powinni śledzić cykl życia źródła, aby być w stanie kasować zbędne pamiątki.
  • Większość dynamicznych języków programowania, jak PHP, Python i JavaScript nie daje gwarancji niezmienialności stanu pamiątki.

Powiązania z innymi wzorcami

  • Można stosować Polecenie i Pamiątkę jednocześnie — implementując funkcjonalność “cofnij”. W takim przypadku, polecenia są odpowiedzialne za wykonywanie różnych działań na obiekcie docelowym, zaś pamiątki służą zapamiętaniu stanu obiektu tuż przed wykonaniem polecenia.

  • Można zastosować Pamiątkę wraz z Iteratorem by zapisać bieżący stan iteracji, co pozwoli w razie potrzeby do niego powrócić.

  • Czasem Prototyp może być prostszą alternatywą dla Pamiątki. Jest to możliwe jeśli obiekt, którego stan chcesz przechować w historii, jest w miarę nieskomplikowany. Obiekt taki nie powinien też mieć powiązań z zewnętrznymi zasobami, albo muszą one być łatwe do ponownego nawiązania.

Przykłady kodu

Pamiątka w języku C# Pamiątka w języku C++ Pamiątka w języku Go Pamiątka w języku Java Pamiątka w języku PHP Pamiątka w języku Python Pamiątka w języku Ruby Pamiątka w języku Rust Pamiątka w języku Swift Pamiątka w języku TypeScript