Dowiedz się, jak automatycznie wychwytywać zmiany w niektórych obiektach aplikacji
W wielu sytuacjach, szczególnie podczas
pisania mniej trywialnych programów, będziesz
potrzebował dostępu do informacji o wszelkich
zmianach stanu niektórych obiektów. W typowych
programach taką zmianą stanu aplikacji może być
np. kliknięcie na przycisku, w aplikacjach webowych
zaś, zmiana stanu obiektu przechowującego
konfigurację lub informacja o zmianach obiektu
zdalnego – dostępnego poprzez SOAP bądź
XML-RPC. W tym artykule skorzystamy z prostego
przykładu, tworząc aplikację zawierającą klasę,
której obiekt ma za zadanie przychwycić i obsłużyć
wyjątki.
Zależy nam jednak na tym, aby o złapanym
wyjątku, czyli o zmianie stanu naszego obiektu,
który będzie się tym zajmował, poinformowane
zostały także inne klasy. Będą one odpowiedzialne
za wyświetlenie stosownego komunikatu
na ekranie oraz za zapis informacji o wyjątku
w pliku logu.
Wiele osób radzi sobie z podobnymi problemami
na różne sposoby. W niektórych sytuacjach
można przeładować stronę, co spowoduje
umieszczenie w obiektach aktualnych danych.
Jest to jednak rozwiązanie mało praktyczne dla
użytkownika, a w dodatku powoduje spowolnienie
pracy aplikacji. Można także zmusić klasę
obserwowaną do wywoływania odpowiednich
metod klas, które chcą wiedzieć o zmianie jej
stanu:
objWyswietlanie = new
Wyswietlanie();
$this->objLogowanie = new
Logowanie();
}
public function zlap(Exception $objException) {
$this->objWyswietlanie->obsluz($objException);
$this->objLogowanie->obsluz($objException);
}
}
class Wyswietlanie {
public function obsluz($objException)
{
echo \"WYJATEK:\n\";
echo $objException;
}
}
class Logowanie {
private $resFile = null;
public function __construct() {$this->resFile = fopen(\'exceptions.log\', \'a\');
}
public function obsluz($objException)
{
fwrite($this->resFile, \"WYJATEK:\n$objException\n\");
}
}
$objObslugaWyjatkow = new Obsługa-Wyjatkow();
try {
throw new Exception(\"Zlap mnie\");
} catch (Exception $objException) {
$objObslugaWyjatkow->zlap($objException);
}
?>
import java.io.IOException;
public class ObslugaWyjatkow {private Wyswietlanie wyswietlanie;
private Logowanie logowanie;
public ObslugaWyjatkow() throws
IOException {
wyswietlanie = new Wyswietlanie();
logowanie = new Logowanie();
}
public void zlap(Exception ex)
throws IOException {
wyswietlanie.obsluz(ex);
logowanie.obsluz(ex);
}
}
public class Wyswietlanie {
public void obsluz(Exception ex)
{
System.out.println(\"WYSWIETL:\n\"+ex);
}
}
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
public class Logowanie {
private FileWriter fw;
public Logowanie() throws IOException
{
fw = new FileWriter(new File(\"exceptions.log\"));
}
public void obsluz(Exception ex)
throws IOException {
fw.write(\"WYSWIETL:\n\"+ex);
fw.flush();
}
}
import java.io.IOException;
class Main {
public static void main(String[]
args) throws IOException {
ObslugaWyjatkow ow = new ObslugaWyjatkow();
try {
throw new Exception(\"Zlap mnie\");
} catch (Exception ex) {ow.zlap(ex);
}
}
}
Jak działa powyższy przykład, zapewne już
wiesz. Klasa {stala}ObslugaWyjatkow{/stala} zajmuje się obsługą
wyłapanych w bloku try…catch wyjątków. Po przekazaniu
wyjątku do metody zlap tej klasy, wywołuje
ona metody obsluz klas obsługujących wyjątki.
Dysponujemy tu klasą Wyswietlanie, wyświetlającą
złapany wyjątek oraz klasą Logowanie, zapisującą
złapane wyjątki do pliku.
Taki sposób rozwiązania przedstawionego
problemu ma jednak jedną zasadniczą wadę:
dodanie kolejnego sposobu obsługi wyjątków,
na przykład wysyłanie informacji o nich na adres
e-mail, powoduje konieczność modyfikacji kodu
klasy obserwowanej {stala}ObslugaWyjatkow{/stala}. Nie jest to
więc rozwiązanie idealne.
Wzorzec obserwator
Rozwiązaniem jest skorzystanie ze wzorca obserwator.
We wzorcu tym wyróżniamy dwie klasy:
klasę obserwującą oraz klasę obserwowaną. Klasa
obserwująca, aby dostawać informacje o zmianie
stanu klasy obserwowanej, musi o to poprosić,
czyli się zarejestrować. Następnie, w przypadku
zmiany stanu, klasa obserwowana automatycznie
wywołuje odpowiednią metodę wszystkich obiektów
obserwujących, które ją o to prosiły:
arrObserwatorzy[] =$objObserwator;
}
public function usunObserwatora-
(Obserwator $objObserwator) {unset($this->arrObserwatorzy[$objObserwator]);
}
public function powiadom() {
foreach ($this->arrObserwatorzy as $objObserwator) {
$objObserwator->obsluz($this->objException);
}
}
public function zlap(Exception$objException) {
$this->objException = $objException;
$this->powiadom();
}
}
class Wyswietlanie implements Obserwator{
public function obsluz(Exception$objException) {
echo \"WYJATEK:\n\";
echo $objException;
}
public function __toString() {
return \'Wyswietlanie\';
}
}
class Logowanie implements Obserwator
{
private $resFile = null;
public function __construct() {$this->resFile = fopen(\'exceptions.log\', \'a\');
}
public function obsluz(Exception$objException) {
fwrite($this->resFile, \"WYJATEK:\n$objException\n\");
}
public function __toString() {
return \'Logowanie\';
}
}
$objObslugaWyjatkow = new Obsługa-Wyjatkow();
$objWyswietlanie = new Wyswietlanie();
$objLogowanie = new Logowanie();
$objObslugaWyjatkow->dodajObserwatora($objWyswietlanie);
$objObslugaWyjatkow->dodajObserwatora($objLogowanie);
try {
throw new Exception(\"Zlap
mnie\");
} catch (Exception $objException) {
$objObslugaWyjatkow->zlap($objException);
}
$objObslugaWyjatkow->usunObserwatora($objLogowanie);
try {
throw new Exception(\"Zlap mnie
2\");
} catch (Exception $objException) {
$objObslugaWyjatkow->zlap($objException);
}
?>
public interface Obserwator {
public void obsluz(Exceptionex);
}
interface Obserwowana {
public void dodajObserwatora(Obserwator obserwator);
public void usunObserwatora(Obserwator obserwator);
public void powiadom();
}
public class Wyswietlanie implements Obserwator {
public void obsluz(Exception ex)
{
System.out.println(\"WYSWIETL:\n\"+ex);
}
}
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
public class Logowanie implements
Obserwator {
private FileWriter fw;
public Logowanie() {
try {
fw = new FileWriter(new
File(\"exceptions.log\"));
} catch(IOException ex) {}
}
public void obsluz(Exception ex)
{
try {
fw.write(\"WYSWIETL:\n\"+ex);
fw.flush();
} catch(IOException e) {}
}
}
import java.io.IOException;
class Main {
public static void main(String[]
args) throws IOException {
ObslugaWyjatkow ow = new ObslugaWyjatkow();
Logowanie logowanie = new Logowanie();
Wyswietlanie wyswietlanie = new Wyswietlanie();
ow.dodajObserwatora(logowanie);
ow.dodajObserwatora(wyswietlanie);
try {
throw new Exception(\"Zlap mnie\");
} catch (Exception ex) {ow.zlap(ex);
}
}
}
W jaki sposób działa nasza aplikacja? Klasa
obserwowana implementuje interfejs (nazwaliśmy
go tu Obserwowana), wymuszający dostarczenie
metod:
- {stala}dodajObserwatora{/stala} – dodającą do kolekcji klasę,
która chce być informowana o zmianach stanu
klasy obserwowanej:
public function dodajObserwatora(
Obserwator $objObserwator) {
$this->arrObserwatorzy [$objObserwator->toString()] = $objObserwator;
}
public void dodajObserwatora(Obserwator obserwator) {
obserwatorzy.add(obserwator);
}
- {stala}usunObserwatora{/stala} – usuwającą z kolekcji klasę
zarejestrowaną wcześniej za pomocą metody
{stala}dodajObserwatora{/stala}:
public function usunObserwatora-
(Obserwator $objObserwator) {
unset($this->arrObserwatorzy[$objObserwator->__toString()]);
}
public void usunObserwatora(Obwzorce.serwator obserwator) {
obserwatorzy.remove(obserwator);
}
- powiadom – metoda to powinna być wywoływana za każdym razem, gdy
zmieni się stan obserwowanej klasy, a więc wówczas, gdy klasy obserwujące
mają zostać poinformowane o zmianie.
Klasy obserwujące implementują inny interfejs (Obserwator), wymuszając
dostarczenie implementacji metody obsłuz. Metoda ta odpowiada w przypadku
naszego programu, za odpowiednią obsługę zgłoszonego wyjątku.
Klasa obserwowana musi więc przechowywać informacje o obiektach, które
chcą otrzymywać informacje o zmianie jej stanu. W tym celu można skorzystać
z kolekcji (ArrayList w Javie) bądź wbudowanych typów tablicowych (tak
jak w PHP). Po zmianie stanu klasy, czyli w naszym przypadku gdy przekazany
zostanie jej nowy wyjątek, musi ona zadbać o poinformowanie o tym stanie
klas zgromadzonych w kolekcji. Zadanie to załatwia jedna pętla:
foreach ($this->arrObserwatorzy as $objObserwator) {
$objObserwator->obsluz($this->objException);
}
for (Obserwator o: obserwatorzy) {o.obsluz(exception);}
Zwróć tutaj uwagę na pewną wadę PHP. O ile tablice rozrastają się wraz
z dodawanymi do niej elementami (w przeciwieństwie do tych standardowych
z Javy, co eliminuje konieczność użycia kolekcji), a ponadto można je indeksować
napisami, o tyle PHP nie potrafi w takiej sytuacji automatycznie wywołać
metody {stala}_toString(){/stala}.
Obserwator a Java
Java dostarcza gotowych narzędzi do korzystania ze wzorca obserwator.
Klasa, która ma być obserwowana, musi dziedziczyć po klasie {stala}java.util.
Observable{/stala}. Po zmianie stanu, klasa obserwowana powinna wywołać metodę
{stala}setChanged(){/stala}, a następnie {stala}notifyObservers(){/stala} lub {stala}notifyObservers(Object arg){/stala}
(wszystkie te metody wchodzą w skład klasy {stala}java.utils.Observable{/stala}).
Metoda {stala}notifiObservers(){/stala} wysyła powiadomienie o zmianie stanu do
obiektów obserwujących. Jeśli przekaże się jej jako parametr obiekt, zostanie
on przekazany obserwatorom.
Oto nasza klasa po odpowiednich modyfikacjach:
import java.io.IOException;
import java.util.Observable;
public class ObslugaWyjatkow extends Observable {
public void zlap(Exception ex) throws IOException {setChanged();
notifyObservers(ex);
}
}
Obserwatorzy muszą implementować interfejs {stala}java.util.Observer{/stala}oraz
dostarczać metodę update({stala}Observable obs, Object arg{/stala}). Metoda ta jest wywoływana
przez metodę obserwowaną i przyjmuje dwa argumenty.
Pierwszym
z nich jest obiekt klasy obserwowanej, która wysyła powiadomienie o zmianie
swojego stanu. Z kolei drugi to obiekt przekazany podczas wywołania metody
{stala}notifyObserver{/stala} lub null, gdy została wywołana jej bezparametrowa wersja.
A oto odpowiednio zmodyfikowane klasy naszych obserwatorów:
import java.util.Observer;
import java.util.Observable;
public class Wyswietlanie implements Observer {
public void update(Observable obs, Object ex) {
System.out.println(\"WYSWIETL:\n\"+ex);
}
}
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.util.Observer;
import java.util.Observable;
public class Logowanie implements Observer {
private FileWriter fw;
public Logowanie() {
try {
fw = new FileWriter(new File(\"exceptions.log\"));
} catch(IOException ex) {}
}
public void update(Observable obs, Object ex) {
try {
fw.write(\"WYSWIETL:\n\"+ex);
fw.flush();
} catch(IOException e) {}
}
}
Jak widzisz, program stał się o wiele prostszy, a w szczególności implementacja
klasy obserwowanej. Całą \”czarną robotę\” załatwia tu za nas klasa
Observable.
Na koniec pozostało jeszcze zarejestrowanie naszych obserwatorów w klasie
{stala}ObslugaWyjatkow{/stala}. Służy do tego metoda {stala}addObserver{/stala}. Jeśli chcesz usunąć
obserwatora, możesz skorzystać z metody {stala}deleteObserver{/stala}. Obie te metody
przyjmują jako parametr obiekt dodawanego bądź usuwanego obserwatora.
Dodatkowo można usunąć wszystkie obserwatory naraz za pomocą metody
deleteObservers(). A oto nowa wersja klasy Main:
import java.io.IOException;
class Main {
public static void main(String[] args) throws IOException
{
ObslugaWyjatkow ow = new ObslugaWyjatkow();
Logowanie logowanie = new Logowanie();
Wyswietlanie wyswietlanie = new Wyswietlanie();
ow.addObserver(logowanie);
ow.addObserver(wyswietlanie);
try {
throw new Exception(\"Zlap mnie\");
} catch (Exception ex) {ow.zlap(ex);
}
}
}
Prawda, że proste? Nie obejdzie się jednak bez kubełka zimnej wody. Z klas
Javy wspomagających tworzenie obserwatorów nie zawsze da się skorzystać.
Pamiętaj, że Observable jest klasą, więc gdy odziedziczysz po niej, budując
własną klasę, zablokujesz sobie możliwość dziedziczenia po jakiejkolwiek innej
klasie. Również wówczas gdy twoja klasa już dziedziczy lub ma dziedziczyć, zamykasz
sobie drogę do możliwości skorzystania z wbudowanego obserwatora.
Mam jednak nadzieję, że przekonałeś się, iż samodzielne zbudowanie
aplikacji w oparciu o ten wzorzec projektowy jest naprawdę bardzo łatwe
i zechcesz zastosować go w praktyce.
Obserwator w pigułce
1. Tworzymy interfejs, który będzie implementowany przez obserwowane
klasy. Powinien on dostarczać metod umożliwiających dodawanie oraz
usuwanie obserwatorów.
2. Tworzymy interfejs, który będzie implementowany przez klasy obserwatorów.
Powinien on dostarczać metodę wywoływaną w celu poinformowania
klasy implementującej o zmianie stanu obiektu klasy obserwowanej.
3. Tworzymy klasę obserwatora implementującą odpowiedni interfejs i dostarczającą
implementacji metod przez niego wymaganych, jak również
metodę informującą obserwatorów o zmianach stanu.
4. Tworzymy klasy obserwatorów implementujące odpowiedni interfejs,
a więc i ciało metody wywoływanej w momencie zmiany stanu obiektu
klasy obserwowanej.