Wzorce Projektowe

PL
Data dodania: 2011-10-05, Autor: Mateusz Wójcik, Dodał: Karol, Wyświetleń: 307

Na początku wypada wyjaśnić co to są te tytułowe wzorce projektowe. Otóż wzorzec projektowy (z ang. design pattern) to rozpowszechnione w społeczności programistów danego języka (bądź ogólnie programistów) rozwiązanie powszechnego problemu, sytuacji, z którą możemy się spotkać w czasie projektowania aplikacji. Wzorzec określa dany problem, prezentując jedno, lub więcej jego rozwiązanie.

Stosowanie wzorców tworzy kod znacznie czytelniejszym, przejrzystszym, łatwiejszym do modyfikacji zarówno przez siebie, jak i innego programistę, który miałby za zadanie rozwijać projekt. Dzięki nim nie jesteśmy zmuszani wynajdować na nowo koła i samodzielnie rozwiązywać postawionego przed nami zadania, poprzez to, że wzorce są powszechnie dostępne chociażby w takim serwisie jak phppatterns.com.

Wzorce są ściśle powiązane z programowaniem obiektowym (OOP), więc korzystanie z nich z pewnością umocni naszą umiejętność myślenia w kategoriach obiektowych, zamiast pisania kodu strukturalnego. Wszystkie wzorce prezentowane są raczej w PHP5, który niesie ze sobą nowy, rozwinięty model obiektówki. Gdy nie znamy jeszcze możliwości nowej wersji języka, a także jej składni polecam serię artykułów na webicty.pl - Programowanie obiektowe w PHP5 cz.1, Programowanie obiektowe w PHP5 cz.2, Programowanie obiektowe w PHP5 cz.3.

Zadaniem, które postawiłem sobie pisząc ten tekst jest przedstawienie podstawowych wzorców programistom, którzy nie mieli z nimi do czynienia, bądź ich nie rozumieją. Wzorce są bardzo powszechne wśród programistów PHP, dzięki czemu natrafimy na implementacje w naszym kochanym języku. Nie mowa tu o języku polskim, bo akurat zbyt wielu polskojęzycznych tekstów wielu w Sieci nie ma, więc wyłowione prze ze mnie perełki prezentuję na końcu artykułu.

We wszystkich zamieszczonych przykładach bardzo istotne sa komentarze, dlatego proszę o uważną lekturę :]

MVC

Jest to tak powszechny wzorzec, że często zapominamy o tym, że również należy do tej grupy. Jednak niektórzy początkujący koderzy z niego nie korzystają dlatego postanowiłem go opisać.

MVC to skrót od Model-View-Controller czyli Model-Widok-Kontroler. Polega on na oddzieleniu 3 warstw aplikacji od siebie – warstwy modelu danych (np. Bazy danych, pliki), warstwy odpowiadającej za interfejs użytkownika (np. html, rss) i warstwy kontrolującej całą aplikacją.

Model

Warstwa Modelu odpowiada za pobieranie danych z określonego źródła. Może to być baza danych, mogą to być pliki lub inny byt pozwalający na trwałe przechowywanie danych, które model może pobrać lub zmodyfikować. Zwykle do każdej części aplikacji mamy osobny model – np. użytkownicy, newsy czy artykuły.

Przykładowa klasa modelu:

<?php
class NewsModel {
    /**
     * Pobiera newsy z bazy danych
     * @param integer $limit  Limit zapytania
     * @param integer $offset Offset zapytania
     */
    public function get($limit, $offset= '') {
        #pobieranie i zwracanie newsów
    }
}

$news= new NewsModel();
$listaNewsow= $news->get(10);
?>

Dzięki podobnym rozwiązaniom manipulacja danymi jest niezwykle prosta, a dodatkowo modyfikacja takiej klasy jest łatwa i co ważne – korzystanie z modelu nie zmusza nas do zmiany kodu w wielu miejscach, jak to by było zwykle przy strukturalnej budowie aplikacji. Niezwykle przydaje sie tu klasa do obsługi bazy danych, która również rozwiązuje wiele problemów.

Dobrze jest mieć klasę abstrakcyjną modelu, którą będziemy rozszerzać. Jeżeli stworzymy spójną strukturę tych klas a także np. strukturę tabel w bazie danych będziemy mogli tworzyć niezwykle uniwersalne klasy, które w minimalnym stopniu będą musiały rozszerzać klasą abstrakcyjną (np. zapisanie w polu obiektu nazwy tabeli i pola identyfikacyjnego). To pozwala również na takie cuda (Active Record):

<?php
$news=new News();
$news->title='Tytuł';
$news->content='Treść newsa';
$news->save();
?>

Widok

Klasa widoku to zwykle jakiś system szablonów – chociażby krytykowany przez wielu Smarty. Ten system szablonów, mimo wad, jest moim zdaniem idealny dla początkującego programisty, by ten zrozumiał jak rozdzielać kod PHP od HTML-a, a więc w zasadzie Kontroler od Widoku. To początkuje myślenie o budowie aplikacji zgodnie z MVC.

Pewnie najlepszym rozwiązaniem jest system szablonów z PHP, a nie z bezsensownymi klamrami w stylu smarty, jednak musi to być oddzielone od właściwej aplikacji, a więc kontrolera, bo zmieszanie ich tworzy przede wszystkim niepotrzebny burdel w kodzie, jest on nieczytelny, trudny w modyfikacji i nieelegancki.

Kontroler

Kontroler to obiekt kontrolujący całą aplikację. Przyjmuje on parametry z danego źródła – np. z tablicy $_GET i w zależności od nich aktywuje konkretną akcję. Akcje tworzą instancje modeli, które dają im dostęp do danych, a następnie poprzez obiekty widoku tworzą interfejs użytkownika.

Przykładowa implementacja kontrolera, wraz z widokiem i modelem z poprzedniego przykładu:

<?php
class View {
    public function parseTemplate($templateName, $data) {
        #wczytujemy dany template i parsujemy go z użyciem danych przekazanych w parametrze
    }
}
class Controller {
    private $_view, $_newsModel;

    public function __construct() {
        #sprawdzamy czy istnieje metoda i wywołujemy ją jeżeli tak, inaczej domyslna metoda czyli index
        if (method_exists($this, $_GET['akcja']))
            $this-> $_GET['akcja'] ();
        else
            $this->index();
    }

    public function index() {
        $this->_newsModel= new NewsModel();
        $this->_view= new View();
        echo $this->view->parseTemplate('news', $this->_newsModel->get(10)); #przekazujemy pobrane newsy

    }
}
?>

Takie rozdzielenie kodu tworzy aplikację łatwo modyfikowalną – w prosty sposób możemy zmienić konkretne elementy – jak np. wygląd, źródło odbioru danych itp. Program staje się rozszerzalny, a niezależność warstw pozwala na powtórne wykorzystanie kodu.

Kontrolerów zwykle w aplikacji jest wiele - newsy, artykuły, strony kontaktowe itd. Wszystkie powinny dziedziczyć jednak z jednej abstrakcyjnej klasy kontrolera, która powinna zapewniać spójny interfejs.

Bez zrozumienia i korzystania z MVC bardzo ciężko o pojęcie innych wzorców. Jeżeli dalej jest ciężko to polecam Wprowadzenie do MVC.

Singleton

Singleton to najpowszechniejszy, obok MVC wzorzec projektowy, uznawany również za antywzorzec, ale o tym za chwilę.

Wielokrotnie podczas pisania kodu potrzebujemy jakiegoś obiektu, który powinien posiadać jedynie jedną instancję, tj. jedynie jeden fizyczny obiekt tej klasy powinien istnieć. Przykładem na to jest chociażby obiekt łączący się z bazą danych, obiekt zawierający konfigurację czy też po prostu odpowiadający za kierowanie całą aplikacją. Do takich działań nadaje się Singleton.

Często singleton jest obwiniany o zastępstwo zmiennych globalnych, jedynie w bardziej przystępnej formie – idea jest ta sama. Szczególnie uwidacznia się to, gdy stosujemy ten wzorzec zbyt często. O tym problemie możemy przeczytać na blogu Splatcha.

Zasada działania singletonu polega na przechowywaniu przez niego statycznego pola, które jest instancją tego właśnie obiektu (zatem singleton przechowuje sam siebie w statycznym polu). Najlepiej zrozumiemy to, gdy ujrzymy przykładową implementację wzorca:

<?php
class Singleton {
    static $instance;
    private $variable='value1';

    /**
     * Prywatny konstruktor - uniemożliwia normalne utworzenie obiektu tej klasy
     */
    private function __construct(){}

    /**
     * Prywatna metoda __clone - uniemozliwia utworzenie kopii obiektu
     */
    private function __clone(){}

    /**
     * Zwraca instancję obiektu i tworzy ja gdy istnieje taka potrzeba
     */
    static function instance(){
        if(empty(self::$instance)) self::$instance=new Singleton;
        return self::$instance;
    }

    public function setVariable($value){
        $this->variable=$value;
    }

    public function getVariable(){
        return $this->variable;
    }
}
?>

Jak widać konstruktor, oraz magiczna metoda __clone odpowiadająca za tworzenie kopii obiektu są zadeklarowane z słowem kluczowym private – zapobiega ono wywołaniu tych funkcji z zewnątrz obiektu, co w przypadku tych dwóch funkcji uniemożliwia zarówno utworzenie obiektu ( new Singleton) jak i skopiowanie go (clone $instanceOfSingleton) poza obrębem klasy.

Najważniejsza część klasy – metoda instance(). Jest ona statyczna, zatem nie musimy mieć istniejącego obiektu tej klasy, by ją wywołać. Sprawdza ona czy statyczne pole instance jest puste – jeżeli tak, to przypisuje do niej nową instancję klasy, następnie ją zwracając. W przeciwnym wypadku tylko zwraca ten obiekt.

Pozostałe dwie funkcje oraz pole $variable umieszczone są tam w celach czysto testowych. Spójrzmy na kod:

<?php
$instance=Singleton::instance();
$instance->setVariable('value2');
echo $instance->getVariable();

echo '<br />';

$instance2=Singleton::instance();
echo $instance2->getVariable();
?>

Na początku tworzymy odniesienie do Singletona w zmiennej $instance. Przypisujemy jej polu $variable wartość value2 i wyświetlamy ją. Następnie tworzymy drugie odniesienie - $instance2 i wyświetlamy wartość jego pola $variable, za pomocą metody getVariable(). Co się okazuje? Modyfikacja obiektu za pomocą wskaźnika $instance zadziałała również dla drugiej zmiennej $instance2, gdyż obie zmienne wskazywały na ten sam obiekt – to właśnie idea singletona – w każdym miejscu kodu korzystamy z tego samego obiektu.

O singletonie, a także Registry Map poczytamy na blogu Athlana: Singleton, Registry Map

Dekorator

Dekorator rozszerza możliwości klasy o nowe rzeczy, bez modyfikacji jej struktury. Jest więc podobnym działaniem jak dziedziczenie i rozszerzanie klas w ten sposób. Jednak nie uwłaczając dziedziczeniu to korzystanie z niego zmusza nas do ciągłej modyfikacji kodu, przez co klasy stają się obszerne i właściwie gdy mamy do czynienia z podobnymi klasami – np. modelami okazuje się, że gdy mamy zamiar zmienić pewną część kodu, to zmieniać to musimy w każdym pliku.

Problem ten nie występuje, gdy mądrze zastosujemy Dekorator. Jego działanie opiera się zazwyczaj o spójny interfejs, do którego należy zarówno dekorator, jak i obiekt który ma być dekorowany, najważniejsze w tym procesie jest jedno – dla obiektu, użytkownika korzystającego z udekorowanej klasy nie ma żadnej różnicy w sposobie korzystania z niej – mogą oni być całkowicie nieświadomi tego, że nie korzystają z pierwotnego obiektu. Dlatego tak ważny jest tu interfejs, który zarówno identyfikuje obiekt, jak i zapewnia identyczny sposób korzystania z klasy.

Przykładowe zastosowanie dekoratora – logowanie zdarzeń, statystyki, manipulacja danymi przed jakimś wydarzeniem, systemy uprawnień użytkownika – jak widać dekorator sprawdza się na wielu polach walki.

Zacznijmy od przykładu bez dekoratora:

<?php
/**
 * Interfejs usługi
 */
interface Service{
    /**
     * metoda doAction ma za zadanie wywołania danej akcji o zadanych parametrach
     * @param string $actionName   Nazwa akcji
     * @param array  $actionParams Parametry akcji
     */
    public function doAction($actionName, $actionParams);
}

/**
 * Usługa news - jak sama nazwa wskazuje służy do obsługi newsów
 */
class News implements Service{
    public function doAction($actionName, $actionParams){
        #robienie czegoś, np. wyswietlenie newsów, zaleznie od actionName i actionParams
    }
}

/**
 * Kontroler - kontroluje działanie aplikacji
 */
class Controller{
    private $_service;

    /**
     * Konstruktor przyjmuje parametry - jest to bardzo skrótowe podejście, zwykle konstruktor sam wybiera te parametry z danego requesta - np. z tablicy $_GET
     * @param string $actionName   Nazwa akcji
     * @param array  $actionParams Parametry akcji
     */
    public function __construct($actionName, $actionParams){

        $this->_service=new News; #tworzenie nowej usługi
        $this->_service->doAction($actionName, $actionParams); #wywołanie akcji

    }

}
?>

Na początku zaprezentowany jest interfejs Service, który implementować będą nasze usługi (typu News, Artykuły itede a także dekorator). Następnie widzimy przykładową implementację – klasę News, oczywiście bez treści, bo to nieistotne. Kolejna klasa to przykładowy, uproszczony kontroler. Widzimy, gdzie tworzona jest usługa i następnie wywoływana jego akcja.

Teraz przychodzi czas na dekorator i jego użycie:

<?php
/**
 * Dekorator
 */
class ServiceDecorator implements Service {

    private $_decoratedService;
    /**
     * w konstruktorze dekorator przyjmuje obiekt dekorowany
     */
    public function __construct(Service $service){
        $this->_decoratedService=$service;
    }

    public function doAction($actionName, $actionParams){

        dodajDoStatystyk($actionName, $actionParams);#fikcyjna funkcja, która prowadzi statystyki
        $this->_decoratedService->doAction($actionName, $actionParams); #faktyczne wywołanie usługi

    }
}

/**
 * Kontroler - kontroluje działanie aplikacji
 */
class Controller{
    private $_service;

    /**
     * Konstruktor przyjmuje parametry - jest to bardzo skrótowe podejście, zwykle konstruktor sam wybiera te parametry z danego requesta - np. z tablicy $_GET
     * @param string $actionName   Nazwa akcji
     * @param array  $actionParams Parametry akcji
     */
    public function __construct($actionName, $actionParams){

        $this->_service=new ServiceDecorator(new News); #tworzenie dekoratora, przyjumjącego usługę
        $this->_service->doAction($actionName, $actionParams); #wywołanie akcji - nic sie nie zmienia, mimo, że jest to dekorator

    }

}
?>

Dekorator zazwyczaj przyjmuje w konstruktorze obiekt, który ma udekorować, wzbogacić. Trzyma go w prywatnym polu i na nim wykonuje ostateczne działania – w tym wypadku wywołanie akcji. Jednak może dodać nową funkcjonalność – tutaj wysłanie danych do statystyki.

W kontrolerze zmienia się zaledwie jedna linijka! W niej przekazujemy obiekt News do dekoratora. Zauważmy, że dalej korzystanie z udekorowanego obiektu jest takie samo – jak wspomniałem obiekt korzystający z dekoratora nie odczuwa żadnych zmian!

By wykonać to samo bez dekoratora musielibyśmy albo zmieniać kontroler, lub po kolei każdą usługę – nie dość, że to mozolne i kod staje się nieczytelny to w dodatku nieeleganckie – klasy powinno sie rozszerzać lub dekorować by zmieniać ich funkcjonalność, a nie zmieniać strukturę ich kodu i użyteczności.

Bardzo dobry artykuł o dekoratorze znajdziemy na PHP Solmag szukać „Dekorator: wzorzec projektowy na każdą bolączkę”.

Strategy

Strategy to kolejny popularny wzorzec projektowy. Z pewnością spotkaliśmy się z taką sytuacją, w której obróbka danych może nastąpić na parę sposobów. Przykładowo newsy można wyświetlić na parę sposobów – może to być HTML, może to być RSS, Atom itd. To są właśnie strategie. Innymi przykładami są – zapisywanie obrazka w JPEG lub GIF lub w innym formacie, odczytanie danych z bazy, bądź pliku czy XML, wywoływanie akcji na podstawie danych z $_GET czy $_COOKIE. Takich przykładów działań, które możemy wykonać na wiele sposobów jest wiele.

Spójrzmy na kod:

<?php
interface DisplayStrategy {
    public function generate($news);
}

class XHTMLStrategy implements DisplayStrategy {
    public function generate($news) {
        #generowanie kodu XHTML
    }
}

class RSSStrategy implements DisplayStrategy {
    public function generate($news) {
        #generowanie kodu RSS
    }
}

class WAPStrategy implements DisplayStrategy {
    public function generate($news) {
        #generowanie kodu dla komórek
    }
}

class NewsController {
    private $_newsModel, $_displayStrategy;

    public function __construct($displayMode) {
        switch($displayMode){
            case 'rss': $displayStrategy=new RSSStrategy(); break;
            case 'wap': $displayStrategy=new WAPStrategy(); break;
            default: $displayStrategy=new XHTMLStrategy();
        }
        $this->_displayStrategy= $displayStrategy; #strategia wyswietlajaca newsy
        $this->_newsModel= new NewsModel(); #jakiś tam model newsów - nieistotne
    }

    public function display() {
        echo $this->_displayStrategy->generate($this->_newsModel->getNews()); #przekazujemy strategii newsy z modelu newsModel i wyswietlamy wygenerowany przez strategie kod
    }
}

$news= new NewsController('xhtml');
$news->display();
?>

W przykładzie mamy klasę kontrolującą – NewsController, która ma za zadanie wyświetlić newsy. Utworzone są również strategie będące implementacją interfejsu DisplayStrategy – XHTMLStrategy, RSSStrategy i WAPStrategy. W zależności od parametru kostruktora, kontroler wybiera odpowiednią strategię i za jej pomocą później wyświetla newsy.

Bez użycia wzorca wykonanie tego jest jak zwykle mozolne i brzydkie – niekończące się instrukcje warunkowe, nowe metody w klasie, nie mające z nią właściwie nic wspólnego… Użycie wzorca Strategy tworzy kod znowu eleganckim i łatwym w modyfikacji.

Pokrótce wspomnę w tym miejscu o wzorcu Factory, który właściwie również został tu zastosowany w pewnej formie. Wzorzec Factory ma na celu stworzenie obiektu w zależności od danej sytuacji – w tym przypadku parametru. Zwykle wybiera on spośród klas należących do jednego interfejsu, dzięki czemu obsługa ich jest jednakowa. Zazwyczaj jest to oddzielna metoda – właściwie pełna nazwa wzorca to Factory Merhod.

Obserwator

Wzorzec obserwator przydaje się, gdy modyfikacja jednych danych wpływa na inne dane czy obiekty. Standardowym wykorzystaniem tego jest dodanie newsa – należy wtedy odświeżyć cache, rss, wysłać newsletter lub powiadomić pingiem np. Technorati.

Dlatego wykorzystuje się obserwatory – zwykle posiadające jedną metodę – update, która modyfikuje kluczowe dane. Obserwatory trzymane są w polu obiektu, który jest przez nie obserwowany. Ten, po wykonaniu modyfikacji danych powiadamia wszystkie obserwatory o tym zdarzeniu, które mogą w ten sposób wykonać swoje działania. Spójrzmy na przykład.

<?php
interface observer {
    public function update();
}

class CacheObserver implements observer {
    public function update() {
        #odświeża cache
    }
}

class RSSObserver implements observer {
    public function update() {
        #odświerza RSS
    }
}

class News {
    private $title, $content, $_observers;

    public function setTitle($title) {
        $this->title= $title;
    }
    public function setContent($content) {
        $this->content= $content;
    }

    /**
     * Dodaje obserwator do listy
     */
    public function addObserver(observer $observer) {
        $this->_observers[]= $observer;
    }

    /**
     * Powiadamia wszystkich obserwatorów
     */
    private function notify() {
        foreach ($this->_observers as $observer) $observer->update();
    }

    public function add() {
        #dodanie newsa
        $this->notify(); #powiadamia obserwatorów
    }

}

$news= new News();

$news->addObserver(new RSSObserver()); #dodajemy obserwator
$news->addObserver(new CacheObserver()); #dodajemy obserwator

$news->setTitle('Tytuł newsa');
$news->setContent('Treść newsa');
$news->add();
?>

Widzimy dwie klasy implementujące interfejs observer – CacheObserver i RSSObserver. Jest także klasa News, odpowiadająca za dodanie newsa. Ma ona metodę addObserver, dodającą obserwator do tablicy _observers, która po dodaniu newsa jest iterowana poprzez metodę notify. Na każdym iterowanym obserwatorze wywoływana jest metoda update, która jak ustaliliśmy modyfikuje jakieś tam dane.

Jak widzimy dodanie kolejnego obserwatora jest bajecznie proste i wymaga dodania zaledwie jednej linijki – bez potrzeby modyfikacji pierwotnej klasy News! Gdybyśmy chcieli wykonać to bez tego wzorca, należałoby modyfikować kod klasy News, a także innych klas, które korzystałyby z tych obserwatorów – zwykle są one bardzo uniwersalne.

Adapter

Często bywa tak, że mamy przydatną klasę i chcemy ją wykorzystać. Być może ma ona podobne cele co nasze inne klasy – przykładowo modele. Jednak ich interfejs jest inny – nie możemy w łatwy sposób jej użyć. Z pomocą przychodzi Adapter.

Adapter „adaptuje” klasę do innego interfejsu. Maskuje ją i w swoich własnych metodach wywołuje metody adaptowanej klasy, często lekko modyfikując zwracany wynik, tak by był spójny z innymi klasami. Spójrzmy na przykład (przyznam ubogi, bo nie miałem dobrego pomysłu na Adapter :] )

<?php
interface Model {
    public function get($limit, $offset);
}

class News {
    public function pobierz($limit, $offset) {
        #pobiera dane
    }
}

class NewsAdapter implements Model {
    private $_adaptedObject;
    public function __cosntruct() {
        $this->_adapted= new News();
    }

    public function get($limit, $offset) {
        return $this->_adaptedObject->pobierz($limit, $offset);
    }
}
?>

Widzimy interfejs Model, do którego wyraźnie nie pasuje klasa News. Adaptujemy więc ją by mogła spełniać jej warunki i być tak używana.

Oczywiście w tym wypadku można było zmienić nazwę metody, lecz co będzie wtedy gdy ta klasa jest używana w innej części aplikacji? Lub jest klasą osoby trzeciej i nie możemy jej zmieniać? Wtedy adapter jest jak znalazł.

Na koniec

Mam nadzieję, że tym długim wpisem przybliżyłem Wam pojęcie wzorców projektowych i teraz zaczniecie, lub jeszcze częściej będziecie z nich korzystać. Z doświadczenia wiem, że z projektu na projekt chętniej sięga się po wzorce, gdyż coraz częściej widzimy, ze można pewną rzecz zrobić w inny, łatwiejszy sposób, żeby w późniejszych pracach się nie przemęczyć zbytnio – modyfikacje są banalne. Na dole prezentuję najciekawsze linki o tej tematyce – w większości w języku angielskim.

Linki

Źródło

 


Aby dodawać komentarze musisz być zalogowany!


Kontakt

Jeśli chcesz się z nami skontaktować napisz na adres: info(at)binboy.org lub odwiedź nasz profil na Facebooku!

O Nas

Serwis binboy.org to kopalnia wiedzy dla wszystkich z branży IT, w szczególności dla programistów i webmasterów. To duży zbiór kursów programowania, tutoriali, darmowych ebooków, setki kodów źródłowych itp.

Bądź w kontakcie

Panel użytkownika

Zaloguj się do panelu użytkownika.
Nie masz konta? Zarejestruj się!
Zapomniałeś hasła?