Ostatnio sporo czytam na temat programowania obiektowego. W niemal każdej książce, która wpadła mi w łapki, przykłady wzorców projektowych podane są w języku Java. Wyjątkiem jest Programowanie obiektowe w PHP 5 Hasina Haydera.
Do napisania postu skłoniły mnie pojawiające się gdzieniegdzie narzekania programistów na PHP, że jakoby ten język to zabawka, że nie da się w nim tworzyć w pełni obiektowego kodu itd. Dzisiaj zademonstruję działanie 3 wzorców projektowych na raz (Strategia, Obserwator, Łańcuch zobowiązań).
Chcemy zrobić coś takiego:

Mamy więc jeden obiekt, który zawiera w sobie dane na temat obiektów, które chce powiadamiać, że coś się u niego zmieniło. Tak działa typowy wzorzec obserwator. Ja to jednak zmodyfikowałem tak, że pierwszy obserwator w łańcuchu otrzymuje informację o zmianie stanu obiektu obserwowanego. Jeżeli umie sobie poradzić z przekazanym komunikatem — reaguje. Jeżeli ta informacja go zbytnio nie interesuje — przekazuje następnemu obiektowi w łańcuchu. Tak działa wzorzec łańcuch zobowiązań. Po co mi wzorzec strategia? Z czystego lenistwa :-) Po co pisać kilka rodzajów obserwatorów do testu, które reagują w inny sposób na komunikaty, skoro można implementować interfejs reagujący na różne komunikaty. Jeżeli będę chciał testować inny rodzaj komunikatów to napiszę sobię nową implementację interfejsu reagującego i tyle :-)
Bierzemy się więc do roboty. Najpierw tworzymy sobie 2 interfejsy. 1 dla wzorca obserwator, drugi dla łańcucha zobowiązań.
interface Obserwujacy {
public function powiadomienie($komunikat);
}
interface Lancuch {
public function zareaguj($komunikat);
}Następnie tworzymy sobie jakiś obiekt obserwowany:
class Obserwowany {
private $obserwatorzy = array();
private $komunikat;
public function addObserwujacy(Obserwujacy $o) {
$this->obserwatorzy[] = $o;
}
public function setKomunikat($k) {
$this->komunikat = $k;
$this->powiadamiaj();
}
private function powiadamiaj() {
foreach ($this->obserwatorzy as $obserwator) {
$obserwator->powiadomienie($this->komunikat);
}
}
}Obiekt ma zaledwie 3 metody. Pierwsza dodaje kolejnego obserwatora do listy, druga zmienia stan obiektu (wysyła komunikat wszystkim), trzecia dla każdego obserwatora wywołuje metodę powiadamiającą o zmianie stanu.
Następnie tworzymy klasę Obserwator, która reaguje na wszystkie komunikaty.
class Obserwator implements Obserwujacy , Lancuch {
protected $reagujNa; // implements ReagujNa
protected $nazwa;
protected $lancuch;
public function __construct($nazwa, Lancuch $l) {
$this->reagujNa = new ReagujNaWszystko();
$this->nazwa = $nazwa;
$this->lancuch = $l;
}
public function powiadomienie($komunikat) {
$this->zareaguj($komunikat);
}
public function zareaguj($komunikat) {
if ($this->reagujNa->reaguj($komunikat)) {
echo '<strong>' . $this->nazwa . '</strong> otrzymal komunikat ' . $komunikat . ' i zareagowal.<br />';
}
else {
$this->lancuch->zareaguj($komunikat);
}
}
}Zmienna $reagujNa implementuje interfejs ReagujNa, który stworzymy poniżej. Niestety w PHP nie da się wymusić określonego typu zmiennej — w tym lepsza jest Java. Jeżeli ktoś koniecznie chce, to może sobie sprawdzać czy dana zmienna rzeczywiście implementuje interfejs przed wywołaniem.
interface ReagujNa {
public function reaguj($komunikat);
}
class ReagujNaWszystko implements ReagujNa {
public function reaguj($komunikat) {
return true;
}
}
class ReagujNaLiterki implements ReagujNa {
public function reaguj($komunikat) {
if (is_string($komunikat)) {
return true;
}
else {
return false;
}
}
}
class ReagujNaNumerki implements ReagujNa {
public function reaguj($komunikat) {
if (is_numeric($komunikat)) {
return true;
}
else {
return false;
}
}
}Mamy tutaj trzy klasy posiadające po jednej metodzie, które zwracają prawdę lub fałsz w zależności od podanego parametru. Dalej wypadałoby wykorzystać te klasy zmieniając zachowanie obiektu Obserwator.
class ObserwatorLiterek extends Obserwator {
public function __construct($nazwa, Lancuch $l) {
parent::__construct($nazwa,$l);
$this->reagujNa = new ReagujNaLiterki();
}
}
class ObserwatorNumerkow extends Obserwator {
public function __construct($nazwa, Lancuch $l) {
parent::__construct($nazwa,$l);
$this->reagujNa = new ReagujNaNumerki();
}
}Na koniec potrzebne jest nam jeszcze ostatnie ogniwo łańcucha, nazwane przeze mnie Limiter. Cokolwiek limiter dostaje — wyrzuca na wyjściu i tyle.
class Limiter implements Lancuch {
private $nazwa;
public function __construct ($nazwa) {
$this->nazwa = $nazwa;
}
public function zareaguj($komunikat) {
echo '<strong>Limiter ' . $this->nazwa . '</strong> otrzymal komunikat ' . $komunikat . '<br />';
}
}Pora to wszystko przetestować. Tworzymy obiekt obserwowany,
$obserwowany = new Obserwowany();
a następnie 4 ciągi obserwatorów. Tworzymy go od końca. Coś jak Matrioszki.
// ciag 1
$limiter1 = new Limiter('ciagu 1');
$obserwatorWszystkiego = new Obserwator('Obserwator Wszystkiego ciagu 1',$limiter1);
$obserwowany->addObserwujacy($obserwatorWszystkiego);
// ciag 2
$limiter2 = new Limiter('ciagu 2');
$obserwatorLiterek = new ObserwatorLiterek('Obserwator Literek ciagu 2',$limiter2);
$obserwowany->addObserwujacy($obserwatorLiterek);
// ciag 3
$limiter3 = new Limiter('ciagu 3');
$obserwatorNumerkow = new ObserwatorNumerkow('Obserwator Numerkow ciagu 3',$limiter3);
$obserwowany->addObserwujacy($obserwatorNumerkow);
// ciag 4
$limiter4 = new Limiter('ciagu 4');
$obsAll = new Obserwator('Obserwator Wszystkiego ciagu 4',$limiter4);
$obsNum = new ObserwatorNumerkow('Obserwator Numerkow ciagu 4',$obsAll);
$obsLit = new ObserwatorLiterek('Obserwator Literek ciagu 4',$obsNum);
$obserwowany->addObserwujacy($obsLit);Ostatnim krokiem jest sprawdzenie jak to teraz działa.
$obserwowany->setKomunikat('abc');
echo '----------------------------------';
$obserwowany->setKomunikat(123);Wynik:
Obserwator Wszystkiego ciagu 1 otrzymal komunikat abc i zareagowal.
Obserwator Literek ciagu 2 otrzymal komunikat abc i zareagowal.
Limiter ciagu 3 otrzymal komunikat abc
Obserwator Literek ciagu 4 otrzymal komunikat abc i zareagowal.
----------------------------------
Obserwator Wszystkiego ciagu 1 otrzymal komunikat 123 i zareagowal.
Limiter ciagu 2 otrzymal komunikat 123
Obserwator Numerkow ciagu 3 otrzymal komunikat 123 i zareagowal.
Obserwator Numerkow ciagu 4 otrzymal komunikat 123 i zareagowal.
A teraz wyjaśnienie jak to wszystko działa:
Ciąg 1 zawiera w sobie tylko obserwatora wszystkiego i limiter. Obserwator każdy komunikat traktuje jako informację dla siebie i nie przekazuje dalej do limitera (przez co komunikat nigdy do niego nie dojdzie).
Ciąg 2 ma tylko obserwatora literek i limiter. Na pierwszy, słowny komunikat zareagował. Na komunikat zawierający liczbę nie zareagował i przekazał dalej do limitera.
Ciąg 3 ma w zamian obserwatora numerków i limiter. Zachował się dokładnie odwrotnie od poprzedniego.
Ciąg 4 ma kolejno obserwatorów literek, numerków, wszystkiego i limiter. Teraz dokładnie widać jak to działa. Jeżeli dany obserwator nie znalazł informacji przydatnej dla siebie — po prostu przekazał następnemu obserwatorowi w ciągu. Warto zauważyć, że obserwator wszystkiego i limiter w tym przypadku nigdy nie zareaguje, gdyż komunikat zostanie rozwiązany zanim do niego dojdzie.
Dla chętnych zamieszczam plik wzorce.php (skasowali, a kopii nie mam :-( ). Zachęcam do eksperymentów i ewentualnej przebudowy obserwatorów tak, aby reagowały na konkretne słowa lub liczby.