Wzorzec projektowy memento w PHP

To pierw­szy wpis w tym roku, mam nadzieję, że wyda się Wam na pozio­mie. Czytam co nieco na temat wzorca memento i trochę ubole­wam, że nie znam C++, gdyż wszyst­kie przy­kłady we Wzor­cach projek­to­wych1 napi­sane są właśnie w tym języku (część gwiaz­dek i innych dwukrop­ków w kodzie jest dla mnie trochę niezrozumiała).

Na warsz­tat wzią­łem dzisiaj wzorzec memento (wg mnie tłuma­cze­nie jako „pamiątka” jest trochę śmieszne; memento to memento i koniec). Za pomocą tego wzorca możemy sporzą­dzić coś na kształt kopii przy­wra­ca­nia stanu obiektu. Ma to zasto­so­wa­nie głów­nie jako mecha­nizm wstecz/cofnij w stan­dar­do­wych apli­ka­cjach. Jak ktoś się uprze, to na pewno w PHP również znaj­dzie zastosowanie.

Napi­sa­nie kodu obiektu trzy­ma­ją­cego stan innego obiektu jest dosyć proste, dlatego podnio­słem trochę poprzeczkę o spraw­dza­nie kto wywo­łuje dany obiekt i czy dane memento jest na pewno dla niego prze­zna­czone. W/w mądra książka mówi do mnie, że tworzyć i przy­wra­cać stan ma obiekt, którego stan doty­czy. Za to prze­cho­wy­wa­niem stanów zajmo­wać się odrębny obiekt — CareTaker (u mnie PrzechowywaczMemento :-) ).

Najpierw idzie obiekt zain­te­re­so­wany prze­cho­wy­wa­niem swojego stanu. Stan każdego obiektu to przy­pi­sane warto­ści do jego zmien­nych. U mnie będzie to tylko zmienna $komunikat.

class JakisObiekt {
 
    /**
     * @var string komunikat do wyswietlenia
     */
    protected $komunikat;
 
    /**
     * @return string komunikat
     */
    public function getKomunikat() {
        return $this->komunikat;
    }
 
    /**
     * @param string $k komunikat do ustawienia
     */
    public function setKomunikat($k) {
        $this->komunikat = $k;
    }
 
    /**
     * @return Memento memento z aktualnym komunikatem obiektu
     */
    public function getMemento() {
        return new Memento($this);
    }
 
    /**
     * @param Memento $memento zawierajace poprzedni komunikat
     */
    public function setMemento(Memento $memento) {
        try {
            $this->komunikat = $memento->getKomunikat(spl_object_hash($this)) . ' (przywrocony z Memento)';
        }
        catch (Exception $e) {
            echo 'Nie udalo sie przywrocic poprzedniego stanu: ' . $e->getMessage();
        }
    }
}

Zain­te­re­so­wać może was funk­cja spl_object_hash. Zwraca unikalny iden­ty­fi­ka­tor (hash) danego obiektu. Memento zapi­suje sobie iden­ty­fi­ka­tor przy tworze­niu, a przy chęci wywo­ła­nia getKomunikat() wymaga poda­nie hasha w celu spraw­dze­nia czy dane memento jest na pewno dla niego.

Dalej idzie kod tytu­ło­wego memento, czyli obiekt prze­cho­wu­jący stan obiektu. Jest ściśle powią­zane z obiek­tem JakisObiekt (lub jego potomkami).

class Memento {
 
    /**
     * @var string hash obiektu tworzacego memento
     */
    private $hash;
 
    /**
     * @var string przechowywany komunikat
     */
    private $komunikat;
 
    public function __construct(JakisObiekt $ob) {
        if ($this->czyLegalnyWywolujacy() === true) {
            $this->hash = spl_object_hash($ob);
            $this->komunikat = $ob->getKomunikat();
        }
        else {
            throw new Exception('Tylko obiekt z rodziny JakisObiekt moze tworzyc klase Memento');
        }
    }
 
    public function getKomunikat($hash) {
        if ($this->czyLegalnyWywolujacy() !== true) {
            throw new Exception('Tylko obiekt z rodziny JakisObiekt moze wywolac funkcje getKomunikat()');
        }
        if ($this->hash !== $hash) {
            throw new Exception('Hash obiektu tworzacego Memento i wywolujacego getKomunikat() nie zgadza sie');
        }
        return $this->komunikat;
    }
 
    private function czyLegalnyWywolujacy() {
        $trace = debug_backtrace(true);
        if (!empty($trace[2]['object']) && is_a($trace[2]['object'],'JakisObiekt')) {
            return true;
        }
        else {
            return false;
        }
    }
}

Tu z kolei powinna Was zain­te­re­so­wać funk­cja czyLegalnyWywolujacy(). To cudo wywo­łuje (praw­do­po­dob­nie) zaso­bo­żerną funk­cję debug_backtrace i spraw­dza kto 2 kroki wcze­śniej popro­sił o wywo­ła­nie funk­cji. Jeżeli jest to kto inny niż obiekt typu JakisObiekt to się obraża i daje sygnał do wywa­le­nia wyjątku. Fajne, co?

Dla spraw­dze­nia czy memento jest w stanie przyj­mo­wać żąda­nia od obiek­tów dzie­dzi­czo­nych po JakisObiekt tworzymy sobie obiekt Dziedziczony.

class Dziedziczony extends JakisObiekt { }

Na koniec podaję kod prze­cho­wy­wa­cza, który zajmuje się zapi­sem kolej­nych wersji i odtwa­rza­niem ich. Po niewiel­kich prze­rób­kach kod może stać się w zasa­dzie uniwersalny.

class PrzechowywaczMemento {
 
    /**
     * @var array tablica asocjacyjna o konstrukcji hash => memento
     */
    private $stany;
 
    public function __construct() {
        $this->stany = array();
    }
 
    public function zapiszStan(JakisObiekt $jo) {
        $this->stany[spl_object_hash($jo)][] = $jo->getMemento();
    }
 
    public function przywrocStan(JakisObiekt $jo) {
        $hash = spl_object_hash($jo);
        $znalezionyStan = (!empty($this->stany[$hash])) ?
                array_pop($this->stany[$hash]) :
                null;
        if ($znalezionyStan !== null) {
            $jo->setMemento($znalezionyStan);
        }
        else {
            echo 'Brak zapisanego wczesniejszego stanu dla tego obiektu <br />';
        }
    }
 
    public function oczyscRejestrMemento() {
        foreach ($this->stany as $key => $val) {
            if (count($val) == 0) {
                unset($this->stany[$key]);
            }
        }
    }
}

Metoda przywrocStan() może być trochę zawiła. Najpierw spraw­dza czy klucz z haszem poda­nego w para­me­trze obiektu w ogóle istnieje. Jeżeli tak to przy­wraca stan, a jeżeli nie to wypi­suje komu­ni­kat. Na koniec mamy bezpa­ra­me­trową metodę oczyscRejestrMemento(). Jeżeli przez prze­cho­wy­wa­cza prze­szło wiele obiek­tów, robi się śmiet­nik kluczy nieza­wie­ra­ją­cych stanów do przywrócenia.

Czas na testy!

$przechowywacz = new PrzechowywaczMemento();
 
$ob = new JakisObiekt();
 
$ob->setKomunikat('pierwszy komunikat');
echo $ob->getKomunikat() . '<br />';
$przechowywacz->zapiszStan($ob);
 
$ob->setKomunikat('drugi komunikat');
echo $ob->getKomunikat() . '<br />';
$przechowywacz->zapiszStan($ob);
 
$ob->setKomunikat('trzeci komunikat (tego nie zapiszemy)');
echo $ob->getKomunikat() . '<br />';
 
// tworzymy drugi obiekt, tym razem klasy Dziedziczony
$ob2 = new Dziedziczony();
 
$ob2->setKomunikat('pierwszy dziedziczony');
echo $ob2->getKomunikat() . '<br />';
$przechowywacz->zapiszStan($ob2);
 
$ob2->setKomunikat('drugi dziedziczony');
echo $ob2->getKomunikat() . '<br />';
$przechowywacz->zapiszStan($ob2);
 
echo '<br />zaczynamy przywracanie<br />------------------------------<br />';
 
// przywracamy stan pierwszego
$przechowywacz->przywrocStan($ob);
echo $ob->getKomunikat() . '<br />';
 
// jeszcze wczesniejsza wersja pierwszego
$przechowywacz->przywrocStan($ob);
echo $ob->getKomunikat() . '<br />';
 
// kolejna proba przywrocenia
$przechowywacz->przywrocStan($ob);
echo $ob->getKomunikat() . '<br />';
echo '------------------------------<br />';
 
// teraz dla drugiego
$przechowywacz->przywrocStan($ob2);
echo $ob2->getKomunikat() . '<br />';
 
// druga proba
$przechowywacz->przywrocStan($ob2);
echo $ob2->getKomunikat() . '<br />';
 
// oczyszczamy rejestr
$przechowywacz->oczyscRejestrMemento();
 
// proba stworzenia obiektu typu memento zakonczona wyjatkiem
$memento = new Memento($ob);

Wyniki testu zgode z przewidywaniem:

pierw­szy komu­ni­kat
drugi komu­ni­kat
trzeci komu­ni­kat (tego nie zapi­szemy)
pierw­szy dzie­dzi­czony
drugi dziedziczony

zaczy­namy przy­wra­ca­nie
——————————
drugi komu­ni­kat (przy­wro­cony z Memento)
pierw­szy komu­ni­kat (przy­wro­cony z Memento)
Brak zapi­sa­nego wcze­sniej­szego stanu dla tego obiektu
pierw­szy komu­ni­kat (przy­wro­cony z Memento)
——————————
drugi dzie­dzi­czony (przy­wro­cony z Memento)
pierw­szy dzie­dzi­czony (przy­wro­cony z Memento)

Fatal error: Uncau­ght excep­tion ‚Excep­tion’ with message ‚Tylko obiekt z rodziny Jaki­sO­biekt moze tworzyc klase Memento’ in

Trochę przy­dłu­gawy wyszedł ten post. Mam nadzieję, że komuś udało się dotrwać aż tutaj :-)

  1. E. Gamma (i in.) : Wzorce projek­towe. Elementy opro­gra­mo­wa­nia obiek­to­wego wielo­krot­nego użytku. Gliwice : Helion, 2010, s. 294–301

Podobne wpisy:

  1. Atak klonów w PHP
  2. 3 wzorce projek­towe w ok. 100 liniach kodu PHP
  3. A Ty w jaki sposób łączysz się z bazą danych?
  4. PDO poprzez Depen­dency Injec­tion Conta­iner [cz. 1/2]

5 Comments

  • 3 stycznia 2011 - 09:54 | Permalink

    Kiedyś bardzo mi to było potrzebne, ale nie umia­łem dojść, jak prawi­dłowo zrobić cofnij/dalej…Doszedłem do wnio­sku, że lepiej od stanów prze­cho­wy­wać dzia­ła­nia wyko­ny­wane na obiek­cie. Wtedy można odwi­nąć dzia­ła­nia, a nie prze­cho­wu­jemy wielu kopii dużego obiektu. Oczy­wi­ście ilość kodu do wykle­pa­nia jest sporo więk­sza, ale nie ubijemy kompu­tera prosząc o zapa­mię­ta­nie 100 kroków wstecz dla obrazka powiedzmy 1024x1024x32bpp. Wyma­ga­łoby to 3200MB pamięci dla samej tylko histo­rii. Sporo, nie?

  • 3 stycznia 2011 - 11:56 | Permalink

    Mógł­byś podać może jesz­cze inne zasto­so­wa­nia memento ? Btw. do imple­men­ta­cji mecha­ni­zmu cofnij/wstecz i histo­rii używa się też wzorca ‚Command’

  • Śpiechu
    3 stycznia 2011 - 23:13 | Permalink

    @sokzzuka
    Wśród innych zasto­so­wań na pewno jest wszel­kie żonglo­wa­nie stanami, a więc wyma­rzona sytu­acja do stoso­wa­nia wzorca Itera­tor.
    Można z memento kombi­no­wać jeżeli stajemy przed koniecz­no­ścią prze­rzu­ce­nia siecią obiektu-kolosa, podczas gdy można sam jego stan (lub nawet tylko niewielką jego część, np. ostat­nią zmianę).
    Nikt inny nie ma wejścia do memento poza zain­te­re­so­wa­nym, bo tak jak u mnie — spraw­dza on wywo­łu­ją­cego i jego iden­ty­fi­ka­tor. Czyli ciekawa metoda progra­mo­wa­nia defen­syw­nego. W C++ robi się to inaczej: możemy sobie usta­lić klasę zaprzy­jaź­nioną i de facto otrzy­mu­jemy 2 publiczne inter­fejsy: dla wybra­nych szer­szy i dla pozo­sta­łych węższy.

  • 16 lutego 2011 - 11:43 | Permalink

    Nie rozu­miem uzależ­nie­nia klasy Memento od Jaki­sO­biekt. Chodzi mi o właści­wo­ści i metody zwią­zane z „komu­ni­kat”.
    Czy Memento nie powi­nien być bardziej uniwersalny?

  • Śpiechu
    17 lutego 2011 - 11:50 | Permalink

    @matipl
    Opis w książce GoF mówi, że każdy obiekt ma dopa­so­wane do siebie memento, które odzwier­cie­dla pewien stan „życia” obiektu. Prze­cho­wy­wa­nie wielu memento pozwala przy­wró­cić dokładny stan z wielu momen­tów życia danego obiektu.
    Poszcze­gólne obiekty powinny mieć prawo do wywo­ły­wa­nia metod memento tylko pamię­ta­ją­cych swój stan (stąd tak restryk­cyj­nie spraw­dzamy hash obiektu wywo­łu­ją­cego). Mnie się wydaje, że można aż tak restryk­cyj­nie nie podcho­dzić do tego wzorca. Myślę, że nic się nie stanie, jeżeli seria obiek­tów otrzyma pewien „stan star­towy” usta­wiony przy pomocy jednego memento.
    GoF podkre­śla, że znajo­mość wzor­ców powinna sygna­li­zo­wać pewien sposób podej­ścia do rozwią­za­nia problemu, nieko­niecz­nie aż tak restryk­cyjny jak oni opisali. Coś jak pewien jesz­cze wyższy poziom abstrak­cji w stylu „zróbmy tu obiekt obser­wo­wany, do którego reje­stro­wać się będzie kilku obser­wa­to­rów, którzy będą wyko­rzy­sty­wali otrzy­mane dane na swój sposób”.

  • Dodaj komentarz

    Twój adres e-mail nie zostanie opublikowany.

    Możesz użyć następujących tagów oraz atrybutów HTML-a: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <p> <pre lang="" line="" escaped=""> <q cite=""> <strike> <strong>