Pamiątka
Cel
Pamiątka to behawioralny wzorzec projektowy pozwalający zapisywać i przywracać wcześniejszy stan obiektu bez ujawniania szczegółów jego implementacji.
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.
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.
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.
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).
-
Klasa Źródło może tworzyć migawki swego stanu, a także przywracać wcześniejszy stan z migawek, gdy zachodzi taka potrzeba.
-
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.
-
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.
-
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!).
-
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.
-
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.
-
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.
-
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.
-
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.
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.
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ć
-
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.
-
Stwórz klasę pamiątki. Jeden po drugim zadeklaruj zestaw pól odzwierciedlających pola zadeklarowane w klasie źródła.
-
Uczyń klasę pamiątki niezmienialną. Pamiątka powinna przyjmować dane tylko raz — za pośrednictwem konstruktora. Klasa nie powinna mieć metod setter.
-
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.
-
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.
-
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.
-
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.
-
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.