Wzorce Projektowe

MatheW

Wzorce projektowe

1 Wzorce projektowe
     1.1 MVC
          1.1.1 Model
          1.1.2 Widok
          1.1.3 Kontroler
     1.2 Dekorator
     1.3 Strategy
     1.4 Obserwator
     1.5 Adapter
     1.6 Na koniec

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.

Wzorce są ściśle powiązane z programowaniem obiektowym (<abbr title="Object Oriented Programming">OOP</abbr>), 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:

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):

$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, jakie się mu zarzuca 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:

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.

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:

/**
 * 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:

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:

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.

interface observer {
    public function update();
}

class CacheObserver implements observer {
    public function update() {
        #odświerza 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

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.

Autor: Mateusz 'MatheW' Wójcik
Kopiowanie bez podania pierwotnego źródła zabronione.

Linki:

PHP

13 komentarzy

Dobry artykuł!

Bardzo przejrzysty i zrozumiały. Dziękuję.
192.168.l.l

Dziękuję bardzo, dobry artykuł
192.168.O.1

Bardzo dobry artykuł, ale moim skromnym zdaniem zrozumieć wzorce projektowe w teorii a zrozumieć wzorce projektowe w praktyce to są dwa światy. Czytałem ostatnio książkę na temat zaawansowanego PHP. Tam każdy kolejny rozdział był poświęcony jednemu wzorcowi. Po przeczytaniu i tak nie widziałem jakiegoś sensownego zastosowania. Ostatnio wraz ze znajomym, który ma olbrzymią wiedzę w projektowaniu systemów zaczęliśmy pisać od podstaw własny framework. Dopiero podczas pracy nad takim projektem zacząłem dostrzegać po co tak na prawdę są wzorce.

Głupota....chciałem zwrócić coś konstruktorem. Temat można zamknąć.

Witam, prośba o pomoc.

Mam taką kasę dla singletona.

Dlaczego nie mogę w getInstance() zrobić czegoś takiego self::$_instance->query('Select 1');
?
Info w komentarzach, 7 i 15 linijki kodu.

class DataBase {
public static $_instance = null;
private function __clone(){}
private function __construct() {
$dns = 'mysql:host=' . DB_HOST . ';dbname=' . DB_NAME . ';charset=' . DB_CHARSET;
$conn = new \PDO($dns,DB_USER,DB_PASSW);
// a tutaj moge $conn->query('Select 1');
return $conn;
}

public static function getInstance() {
if(!(self::$_instance instanceof DataBase)) {
self::$_instance = new DataBase();
}
// dlaczego nie mogę self::$_instance->query('Select 1');
return self::$_instance;
}

"Może nie wczytałem się dokładnie, ale wzorzec Dekorator dodaje nowa funkcjonalność. Gdzie tu jest nowa funkcjonalność?"

Dekorator wzbogaca obiekt o nową funkcjonalność, lub nową cechę jakkolwiek to nazwać. To co jest tu prezentowane to bardzo okrojone przykłady a nie rzeczywisty kod. Może sprawdź: http://blog.smentek.eu/2010/01/30/entry/15/wzorzec-projektowy-dekorator/ gdzie jest to bardziej łopatologicznie wyłożone.

Może nie wczytałem się dokładnie, ale wzorzec Dekorator dodaje nowa funkcjonalność. Gdzie tu jest nowa funkcjonalność?
Co do wzorca MVC. Nie stosuje się go w czystej postaci w aplikacjach webowych. Do tego używamy następcy, wzorca Model 2. Trudno, żeby View był subskrybentem Modelu i dostawał od niego powiadomienia o zmianie stanu, czyż nie...?
Strony www tak nie działają :)

no i front controllera ni ma :P ale ogolnie spoko :P thx za artykul

Dobry artykuł, bardzo dobry :-) Nie wspomniałeś o wzorcu Fasada, ale z tego co wyczytałem tutaj, to chyba to samo co Adapter

Mi raczej chodziło o "strategy" i "obserwator" :)

Co do MVC - ludzie tworzą wiki, więc jakość artykułów jest różna. Dla porównania angielska wersja:

http://en.wikipedia.org/wiki/Model-view-controller

Natomiast jeżeli chodzi o Linux i Linuksa oraz wszelkie "x"-y na końcu to:
http://www.linux.pl/?id=article&show=38

co do mvc: http://pl.wikipedia.org/wiki/MVC

co do polskich nazw - wychodze z takiego zalozenia skoro pisze sie Linux i Linuksa, to na takiej samej zasadzie decorator i dekoratora. Decoratora wlasnio brzmialo by dziwnie :] Gdzie dalo sie spolszcyc ladnie spolszczalem.

Nie nazwał bym MVC wzorcem projektowym, a raczej architekturalnym. Nie rozwiązuje ono pojedynczego problemu, jak to jest postawione przez wzorcami projektowymi, a jest jedynie pewnym wzorcem budowy całej aplikacji, takim samym jak filtr, klient-serwer, architektura wielowarstwowa itp.

I taka dodatkowa uwaga. Jak polskie nazwy wzorców to polskie, jak angielskie, to angielskie. Miks wygląda jakoś dziwnie.