Jak pisałem we wstępie wzorzec projektowy to odpowiedź na problem. W takim razie jaki problem rozwiązuje ten wzorzec? Kod naszej aplikacji często zależy od instancji obiektów kilku klas, każdą z nich tworzymy z wykorzystaniem słowa new. Wyobraźmy sobie przykład.
<?php $log = new FileLog(); $log->doLog("Aplikacja uruchomiona"); // Dodaje do logów tekstowych nową informację ?>
Wykorzystujemy klasę logowania FileLog, która pomaga nam śledzić proces działania naszej aplikacji. Nie będę zagłębiał się w implementację techniki zapisywania, to nie stanowi istoty naszego problemu.Postawmy sobie pytanie: co jeśli kiedyś będziemy chcieli zmienić typ klasy, która ma być tworzona? Co jeśli nagle stwierdzimy, że odpowiednią metodą zapisywania logów będzie umieszczanie ich w bazie danych? Wywołań instancji FileLog jest prawdopodobnie w naszym kodzie całe mnóstwo - nasza aplikacja na sztywno związała fakt logowania i sposób wykonywania tej czynności. Naprawmy błąd używając naszego wzorca.
<?php $log = Log::load(); // Mmetoda zwróci instancję, ale jakiej klasy? Jak została stworzona? $log->doLog(„Aplikacja uruchomiona”); ?>
Zamknęliśmy sposób tworzenia obiektu w metodzie load nowej klasy Log. Zwraca instancję, którą będziemy traktować identycznie jak wcześniejszy FileLog. Wykorzystując ten wzorzec nie interesuje nas jak została stworzona instancja naszego obiektu logującego. Nie wiemy przede wszystkim z jakiej konkretnie klasy będziemy ostatecznie korzystać, czyli w praktyce w jaki sposób logi będą zapisywane. Musimy jednak zapobiec konfliktom. Zunifikujmy informacje czym jest klasa logowania, co wykonuje - jak dostać się do odpowiedniej funkcjonalności. Ustalmy jak ma wyglądać niezależnie od implementacji. Stwórzmy odpowiedni interfejs:
<?php interface ILog { public function doLog($message); } ?>
W interfejsie opisujemy tylko jedną metodę wspólną dla koncepcji klas logowania – zapisującą podaną wiadomość. Poinformujmy naszą aplikację, że klasa DbLog implementuje interfejs ILog.
<?php class DbLog implements Ilog { public function doLog($message) { // Tu dodajemy informację do bazy danych } } ?>
Świetnie! Jeszcze tylko ostatnia klasa Log, z naszym "budowniczym".
<?php class Log { static public function load() { return new DbLog(); } } ?>
Idąc dalej tropem rozdzielania elementów i modularyzacji spójrzmy na wzorzec "strategii". Wykorzystując go wyłączamy algorytmy, umieszczając je w odpowiednich klasach zewnętrznych. Klarownym przykładem jest walidacja danych, np. przy rejestracji użytkownika:
<?php if( isset($_POST['submit']) ) { if( !isset($_POST['username']) ) { echo 'Podaj nazwe uzytkownika'; } else if( strlen($_POST['username']) <= 5 ) { echo 'Nazwa użytkownika musi mieć przynajmniej 5 znaków'; } else if( /* kolejny warunek itd. */ ) { echo 'Komunikujemy kolejny bląd'; } else { zapisz_uzytkownika($_POST['username']); echo 'Zostaleś zapisany!'; } } ?>
Pełno warunków, logika sprawdzania zmiennej wymieszana z wyświetlaniem komunikatów, na sztywno wpisane warunki - nie jest najlepiej. Zauważmy, że każdy test zmiennej w powyższych instrukcjach warunkowych, czy to sprawdzenie jej istnienia czy długości, jest pewnym algorytmem walidacji. Cały proces jest po prostu wywołaniem kolejnych, różnych, algorytmów (strategii) i przetestowanie zmiennej pod ich kątem. Możemy zmodyfikować powyższy kod by był bardziej czytelny i by zamknąć specyficzne warunki walidacji do późniejszego wykorzystania w innych miejscach aplikacji. Najpierw, klasycznie już budujemy interfejs, tym razem walidatora:
<?php interface IValidate { public function __construct(&$var); public function isValid(); public function getErrors(); } ?>
W następnym kroku tworzymy sobie klasy implementujące powyższy interfejs, dzięki czemu każda będzie osobnym algorytmem walidacji. Metoda isValid() takiej klasy zwróci nam czy zmienna $var poprawnie przeszła walidacje, a metoda getErrors() przekaże nam informacje o błędach. Przykład?
<?php class UsernameLengthValidate implements Ivalidate { private $_var; private $_errors = array(); public function __construct( &$var ) { $this->_var = &var; } public function isValid() { if( strlen($var) <= 5 ) { $this->_errors[] = 'Nazwa musi mieć przynajmniej 5 znaków'; return false; } return true; } public function getErrors() { return $this->_errors; } }
Powinniśmy zastosować tu także klasę abstrakcyjną, jednak nie chcę rozpraszać, zostańmy więc przy meritum tematu. Kod jak dla tak prostej operacji, wydaje się niebagatelnie dłuższy niż wcześniej, ale zauważmy, że walidacja wygląda już o niebo lepiej i pozwala w prosty sposób dodawać kolejne reguły:
<?php if( isset($_POST['submit']) ) { $username = &$_POST['username']; // tworzymy listę walidatorów przekazując zmienną $list = array(); $list[] = new ExistsValidate($username); $list[] = new UsernameLengthValidate($username); // uruchamiamy każdy $errors = array(); foreach( $list as $validate ) { if( !$validate->isValid() ) { $errors = array_merge($errors,$validate->getErrors()); } } // jeśli wystąpiły błędy wyświetlamy je lub uruchamiamy docelową akcję if( count($errors) ) { echo 'Wystąpiły błędy:'; echo '<ul>'; foreach( $errors as $error ) { echo '<li>'.$error.'</li>'; } echo '</ul>'; } else { zapisz_uzytkownika($username); echo 'Zostaleś zapisany!'; } }
Czas teraz dodać naszym przypadkom trochę pikanterii. Wymiana komponentów bibliotek zawsze była kłopotliwa, a teraz weźmy pod uwagę zaprojektowanie architektury typu plug-in. Mając na uwadze otwieranie aplikacji na takie właściwości musimy myśleć jeszcze szerzej niż przy wcześniejszych przykładach. Oczywiście, wzorzec "budowniczego" oddaje nam możliwość "wstrzyknięcia" do kodu takiej klasy i takiego obiektu który sobie ręcznie wybierzemy, ale pójdźmy krok dalej. Najpierw szybki przykład.
<?php $mail = new Mail; $mail->setRecipient('ceo@google.com'); $mail->setBody('Hej! Mam dla Ciebie propozycję nie do odrzucenia..'); $mail->send(); ?>
Proste wykorzystanie pewnej klasy Mail, powtarzające się prawdopodobnie w wielu miejscach aplikacji. Było by świetnie, gdyby możliwość podmiany klasy była łatwa, może nawet z poziomu pliku konfiguracyjnego? Moglibyśmy w łatwy sposób zmieniać sposób wysyłania maili na taki wykorzystujący autoryzację SMTP lub z użyciem zewnętrznej biblioteki.Generalnie chcemy w każdej chwili działania aplikacji mieć możliwość wymiany najważniejszych elementów. Stwórzmy dla naszej aplikacji "kontener zależności", jeden element, "uniwersalnego budowniczego", który będzie decydował jakie implementacje i jakiej klasy wstrzykiwać do naszej aplikacji.By dynamicznie stworzyć instancje klasy możemy użyjemy refleksji, czyli mechanizmów odwrotnej inżynierii wbudowanych w PHP.
<?php $className = 'Mail'; // definiujemy nazwę klasy $reflection = new ReflectionClass($className); // tworzymy obiekt refleksji klasy $mail = $reflection->newInstance(); // tworzymy instancję ?>
To wystarczy by tworzyć obiekty, których nazwa klasy nie jest znana w czasie projektowania, nie jest zapisana w kodzie ręcznie. Muszę nadmienić, że nie pozwolimy sobie teraz na stworzenie rozbudowanej implementacji "kontenera wstrzykiwania zależności", to już temat na inny artykuł. Zobaczmy jednak jak wyglądało by stworzenie bardzo prostego kontenera.
<?php class DIContainer { protected $_storage = array(); public function setClass($alias,$class) { $this->_storage[$alias] = $class; } public function getInstance($alias) { $className = $this->_storage[$alias]; $reflection = new ReflectionClass($className); return $reflection->newInstance(); } } ?>
Wykorzystując metodę setClass() zapisujemy nazwę klasy i nazwę-alias używaną w aplikacji. Z kolei getInstance() zwraca instancję klasy danego aliasu. Skomplikowane? Przykład rozwieje wszelkie wątpliwości.
<?php $di = new DIContainer; $di->setClass('Mail','SmtpMail'); $di->setClass('Db','MysqlDb'); // ...trochę kodu dalej $mail = $di->getInstance('Mail'); $mail->setRecipient('ceo@google.com'); $mail->setBody('Hej! Mam dla Ciebie propozycję nie do odrzucenia..'); $mail->send(); ?>
Najpoważniejsza literówka, dziwne, że nikt tego do tej pory nie wytknął:
Factory to w tłumaczeniu wzorzec projektowy Fabryka, a nie Builder (Budowniczy). Builder z tego co pamiętam służy do określenie sposobu tworzenia obiekty jednego typu. Proszę o poprawienie jeśli jest inaczej
Fabryka to wzorzec do tworzenia obiektu z całej rodziny klas, dzieli się na statyczną fabrykę i na abstrakcyjną implementacja fabryki abstrakcyjnej może wyglądać np. tak:
class ConcreteLoggerFactory implements LoggerFactory {
public Logger getLogger($loggerName) {
switch($loggerName) {
case 'FileLog': return new FileLogger()
//itd
}
}
}
interfajse masz IValidate
ale dziedziczysz już
class UsernameLengthValidate implements Ivalidate.
@cojack, mam nadzieję, że zwięźle napisany tekst pokazujący krótkie i życiowe zastosowanie paru wzorców będzie ciekawą lekturą oraz zachęci do dalszych poszukiwań. Niestety w sieci jest już wiele wywodów akademickich mających mało wspólnego z realnym wykorzystywaniem takich dobrych praktyk. Jeżeli jesteś jednak zainteresowany pisaniem dokładnych opracowań poszczególnych wzorców, na pewno znajdą oddanych czytelników. Pamiętaj tylko by nie stworzyć kolejnej kopii nudnych podręczników. Pozdrawiam!
Sory ale Ty to na kolanie pisałeś podczas przerwy w szkole? Bo ja nie wiem, raptem dwa akapity a temat rzeka. Zresztą opisane wzorce są ciekawe, Twój sposób ich przedstawienia jest bagatelizujący sprawę. o której sam wcześniej napisałeś by jej nie bagatelizować. To ja nie wiem o czym my rozmawiamy. Na moim blogu w ciągu miesiąca znajdzie się obszerny opis paru wzorców obiektowych.
Bardzo fajny artykuł i chętnie przeczytałbym o innych wzorcach projektowych. Jedyną uwagę jaką mam to błędy w listingach. Np. w listingu zaczynającym się od "class UsernameLengthValidate" znalazłem trzy błędy. Prosiłbym o większą staranność