Autor wpisu: Piotr Śliwa, dodany: 30.01.2010 13:28, tagi: php
Dziś pora na trzeci artykuł z serii "wzorce w praktyce". Tym razem postanowiłem omówić praktyczne zastosowanie mniej znanego i stosowanego (w programowaniu w php) wzorca.
Każdy z Was zapewne wie co to jest transakcja bazodanowa. Jeśli jednak nie, to przypomnę, że transakcja w systemach baz danych polega na tym, że określony zbiór zapytań wykona się poprawnie w całości lub wogóle żadne zapytanie nie zostanie wykonane (de facto "nie wykona się" jest błędnym określeniem, raczej "nie zostanie zatwierdzone"). Więcej na ten temat znajdziecie na wikipedii. Transakcja jest implementacją wzorca projektowego koordynator, a raczej rozbudowanej specjalizacji tego wzorca o nazwie "Zatwierdzanie trójfazowe" (Three-Phase Commit) - właśnie tą odmianę Koordynatora będe omawiał.
Wyjątkowo w skrócie opiszę ideę tego wzorca, gdyż jest on mniej powszechny niż dwa poprzednie wzorce opisywane przeze mnie. Uczestnikami są Koordynator, Użytkownik, Zadanie oraz Klient. Klient zleca Koordynatorowi do wykonania Zadanie, które jest podzielone na kilka części, jedna część Zadania może być wykonana tylko przez jednego Użytkownika. Rola Koordynatora polega na zapewnieniu spójności systemu, czyli podzielne Zadanie musi się wykonać w całości lub żadna z części tego Zadania przydzielona do jednego Użytkownika nie może się w ogóle wykonać.
Przedstawienie problemu
W naszym przykładzie Zadaniem będzie zaktualizowanie jakiegoś rekordu w kilku zdalnych i niezależnych usługach sieciowych będących na różnych serwerach, komunikacja odbywać się może przykładowo przez SOAP. Dla nas ważne jest, aby w każdej usłudze sieciowej był rekord o tym samym stanie, nie możemy dopuścić do sytuacji w której rekord w jednej z usług ma inny stan niż ten rekord w pozostałych usługach.Rozwiązanie nie stosując Koordynatora
[PHP]
- //tablica obiektów reprezentujących różne usługi sieciowe
- //dla ułatwienia przyjmuję, że mają taki sam interfejs - jeśli
- //tak nie jest, można zastosować wzorzec Adapter opisywany w pierwszym
- //artykule z cyklu
- $services = ...;
- //aktualny rekord, który ma być zapisany w zdalnych usługach
- $record = ...;
- foreach($services as $service)
- {
- //zapisanie rekordu w jednej ze zdalnych usług
- $service->save($record);
- }
Kod ten będzie działał, ale jeśli pewnego razu wystąpi problem z połączeniem z jedną ze zdalnych usług lub z innych nieistotnych powodów rekord nie zostanie zapisany, to stracimy spójność - ten sam rekord będzie miał inne wartości w różnych usługach.
Rozwiązanie problemu
Wydzielamy dwie klasy: Koordynatora (Coordinator) oraz Użytkownika (Coordination).Najpierw zajmiemy się szkieletem klasy Użytkownika (Coordination). Obiekt, który może być koordynowany powinien umieć określić (nie wykonując swojej części zadania) czy wykonanie zadania się powiedzie (metoda prepare()), anulować wykonywanie zadania jeśli metoda prepare() zwróciła false - obiekt ten nie jest gotowy na jego wykonanie (metoda abort()). Powinien również umieć zatwierdzić (wykonać) zadanie (metoda commit()) oraz opcjonalnie wycofać zmiany, które nastąpiły po pomyślnym zatwierdzeniu (metoda rollback()).
[PHP]W zamyśle metoda abort() wykona się jeśli metoda prepare() zwróci false, jeśli jednak zwróci true to powinna zostać wykonana metoda commit(). Jeśli metoda commit() wyrzuci jakiś wyjątek (zadanie nie powiodło się), to powinna zostać wykonana metoda rollback(). Pamiętajmy jednak, że jeśli metoda prepare() i-tego obiektu Użytkownika zwróci false, to mają zostać wywołane metody abort() obiektów od 0 do i-1, gdyż dany obiekt który uważa że nie jest w stanie wykonać zadania, nie musi anulować swojej gotowości do jego wykonania, gdyż sam powiedział że się nie przygotował ;) To samo tyczy się metody commit(), jeśli i-ty obiekt wyrzuci w niej wyjątek, to powinna zostać wywołana metoda rollback() obiektów od 0 do i-1, gdyż i-ty obiekt tego zadania nie wykonał.
- class Coordination
- {
- //obiekt jednej usługi zdalnej
- private $service = null;
- private $state = null;
- private $isAborted = false;
- public function __construct($service)
- {
- $this->service = $service;
- }
- /**
- * @return bool Czy zadanie ma szansę się powieść?
- */
- public function prepare()
- {
- try
- {
- //wykonanie jakiegoś testowego żądania, można np. pobrać w tym
- //żądaniu obecny obiekt rekordu który ma być zaktualizowany,
- //aby przy ewentualnym wywołaniu metody rollback() obiekt ten dysponował
- //stabilnym (przed wykonaniem zadania) stanem rekordu.
- $this->state = $this->service->test();
- }
- catch(Exception $e)
- {
- //załóżmy, że jeśli zostanie wyrzucony wyjątek, to żądanie testowe nie powiodło się
- //poprawność testu można stwierdzić również np. po wartości zwracanej
- return false;
- }
- return true;
- }
- /**
- * Anulowanie wykonania zadania
- */
- public function abort()
- {
- $this->isAborted = true;
- $this->state = null;
- }
- /**
- * Zatwierdzenie zadania
- * @throws Exception Zadanie nie powiodło się
- */
- public function commit()
- {
- if($this->isAborted)
- {
- throw new LogicException('Zadanie zostało anulowane');
- }
- $this->service->doTask(...);
- }
- /**
- * Przywrócenie starego stabilnego stanu
- */
- public function rollback()
- {
- $this->service->doTask($this->state);
- }
- }
Klasa Koordynatora powinna umieć zarejestrować Użytkowników (metoda register()) oraz w skoordynowany sposób wykonać zadanie (metoda commit()).
[PHP]
- class Coordinator
- {
- private $coordinations = array();
- private $index;
- public function register(Coordination $coordination)
- {
- $this->cordinations[] = $coordination;
- }
- public function commit()
- {
- $count = count($this->coordinations);
- $abort = false;
- //sprawdzenie, czy wszyscy uczestnicy są gotowi na wykonanie swojej części zadania
- for($this->index=0; $this->index < $count; $this->index++)
- {
- if(!$this->coordinations[$this->index]->prepare())
- {
- $abort = true;
- break;
- }
- }
- //jeden z Użytkowników nie jest gotowy na wykonanie zadania, anuluj zadanie
- if($abort)
- {
- for($i=($this->index-1); $i >= 0; $i--)
- {
- $this->coordinations[$i]->abort();
- }
- return false;
- }
- try
- {
- //zatwierdź zadanie
- for($this->index=0; $this->index < $count; $this->index++)
- {
- $this->coordinations[$this->index]->commit();
- }
- }
- catch(Exception $e)
- {
- //jeden z Użytkowników nie zdołał zatwierdzić zadania, wycofaj zmiany
- for($i=($this->index-1); $i >= 0; $i--)
- {
- $this->coordinations[$i]->rollback();
- }
- return false;
- }
- return true;
- }
- }