Witaj, Gościu O nas | Kontakt | Mapa
Wortal Forum PHPEdia.pl Planeta Kubek IRC Przetestuj się!

Wzorce projektowe w akcji - rozwiązania znanych problemów w praktyce

Zbudujmy sobie framework

Weźmy przykład - część aplikacji WWW, odpowiedzialną za przetwarzanie parametrów przesyłanych w zapytaniu HTTP jako wynik wywołania URL. Większość z nas wielokrotnie pisała taki fragment kodu, rozwiązując ten sam problem trochę inaczej. Ponieważ jest to artykuł o wzorcach projektowych, w dalszych przykładach wykorzystamy wzorzec architektoniczny Front Controller. Front Controller jest jednym, centralnym miejscem aplikacji, przez który przechodzą wszystkie wywołania HTTP. Dzięki takiej fasadzie możemy w jednym miejscu skupić funkcjonalność wspólną dla wszystkich akcji (np. sprawdzanie praw użytkowników do wykonania danej akcji, logowanie czasy renderowania stron itd.). W szczególności w tym głównym kontrolerze będziemy umieszczali logikę związaną z interpretacją przesłanych parametrów. Na podstawie wyniku interpretacji parametrów, Front Controller przekazuje sterowanie do konkretnej akcji, modułu aplikacji. W tych oddzielnych akcjach znajduje się funkcjonalność poszczególnych modułów, np.: zarządzanie kontami użytkowników, obsługa newsów czy forum. Głównemu kontrolerowi pozostawiamy zadania wspólne dla wszystkich akcji. Dzięki takiej separacji zadań programista dopisujący nowy moduł nie musi martwić się o usługi, które muszą być zapewnione dla każdej akcji. Tym zajmuje się już Front Controller. Więcej o tym wzorcu przeczytacie w artykule Frameworki dla PHP, z numeru 2/2005.

Rysunek 1. UML-owy diagram klas dla omawianego fragmentu frameworka

Listing 1. Front Controller z ustalonym na stałe sposobem wyszukiwania akcji

interface MVCAction {
   public function doAction(HttpRequest $request);
}

class HttpRequest {

   private $_requestParams = array ();

   public function __construct() {
      $this->_requestParams = array_merge($_GET, $_POST);
   }
   public function getParam($paramName) {
      return $this->_requestParams[$paramName];
   }
}

interface FrontController {
   public function doService(HttpRequest $request);
}
/**
 * Najprostsza implementacja interfejsu FrontController.
 * Wszystkie parametry sterujące pracą głównego kontrolera są zapisane na stałe w kodzie.*/
class FrontControllerImpl implements FrontController {
   
   public function doService(HttpRequest $request){
      
      //na stałe zapisana nazwa parametru wywołania HTTP,
      //przez co trudno jest zmienić nazwę parametru odpowiedzialnego
      //za przekierowanie przetwarzania do konkretnej akcji
      $actionName = $request->getParam('action');
      
      if ($actionName != ''){
         //na stałe zapisany katalog i nazewnictwo plików zawierających akcje,
         //wprowadzenie innej strategii odnajdywania klas z implementacją akcji
         //wymaga modyfikacji w tym fragmencie kodu
         $actionClassFileName = dirname(__FILE__).'/'.$actionName.'.php';
         
         if (!class_exists($actionName)){  
            if (file_exists($actionClassFileName)){
               require_once($actionClassFileName);
            } else {
               throw new RuntimeException("Brak na dysku pliku z definicją akcji '$actionClassFileName'");
            }
         }
         //powołanie do życia odnalezionej klasy dla akcji
         $actionClass = new $actionName();
         
         //właściwe wywołanie akcji
         //wszystkie akcje muszą implementować interfejs MVCAction
         $actionClass->doAction($request);
      } else {
         throw new RuntimeException('Nie wyspecyfikowano akcji do wywołania!');
      }         
   }      
}
$fc = new FrontControllerImpl();
$fc->doService(new HttpRequest());

class sayhello implements MVCAction {
   public function doAction(HttpRequest $request){
      echo "Hello World!";
   }
}

Spójrzmy na Listing 1 i Rysunek 1, gdzie możemy zobaczyć prosty schemat UML omówionej części aplikacji oraz reprezentację tego modelu w kodzie. Jak widać na przedstawionym wydruku, na początku zdecydowaliśmy się na dość prosty sposób mapowania URL na konkretną klasę dostarczającą funkcjonalności. Zakładając, że URL miał postać http://[host]/[katalog]/index.php?action=sayhello, w przedstawionym przykładzie będziemy poszukiwali klasy o nazwie sayhello umieszczonej w głównym katalogu. Oczywiście taki sposób mapowania wywołań na implementację jest dość prymitywny i w praktyce różne osoby będą miały odmienne pomysły na nazewnictwo klas, ich położenie w strukturze folderów, wymagania co do bezpieczeństwa itd. Może zdarzyć się nawet, że my sami w różnych projektach będziemy chcieli stosować jakieś warianty podstawowego rozwiązania. W chwili obecnej każda taka zmiana wymaga modyfikacji klasy głównego kontrolera. Możemy próbować przewidywać, jakie funkcje będą w przyszłości potrzebne i odpowiednio sparametryzować Front Controller (Listing 2).

Listing 2. Front Controller po sparametryzowaniu

/**
 * Kolejna próba implementacji FrontControllera, w której
 * pojawiają się parametry dla wartości, które do tej pory były
 * zapisane na stałe.
 */
class FrontControllerImpl implements FrontController {
   
   private $_actionRequestParamName;
   private $_actionFileNamePrefix;
   private $_actionFileNameSufix;
   
   /**
    * Konstruktor FrontControllera, dzięki któremu możemy 
    * parametryzować działanie metody doService.
    * 
    * @param String $actionRequestParamName
    * @param String $actionFileNamePrefix
    * @param String $actionFileNameSufix
    */
   public function __construct($actionRequestParamName, 
     $actionFileNamePrefix, $actionFileNameSufix){
      $this->_actionRequestParamName = $actionRequestParamName;
      $this->_actionFileNamePrefix = $actionFileNamePrefix;
      $this->_actionFileNameSufix = $actionFileNameSufix;      
   }
   
   public function doService(HttpRequest $request){
      
      $actionName = $request->getParam(
         $this->_actionRequestParamName);
      
      if ($actionName != ''){
         $actionClassFileName = $this->_actionFileNamePrefix.
            $actionName.$this->_actionFileNameSufix;
         
         if (!class_exists($actionName)){  
            if (file_exists($actionClassFileName)){
               require_once($actionClassFileName);
            } else {
               throw new RuntimeException(
                 "Brak na dysku pliku z definicją akcji
                   '$actionClassFileName'");
            }
         }
         
         $actionClass = new $actionName();
         $actionClass->doAction($request);
         
      } else {
         throw new RuntimeException(
          'Nie wyspecyfikowano akcji do wywołania!');
      }
         
   }
      
}
   
   
$fc = new FrontControllerImpl('action', 
   dirname(__FILE__).'/','.php');
$fc->doService(new HttpRequest());

Niestety, nasze przewidywania nie obejmują zbyt wielu przypadków i z pewnością znajdzie się projekt, w którym potrzebna będzie zupełnie odmienna strategia odnajdywania implementacji na podstawie URL. Czy oznacza to, że jesteśmy skazani na ciągłe zmiany Front Controllera?

Strategy

Listing 3 pokazuje, że najczęściej zmieniającą się część kodu możemy wydzielić do osobnej klasy, w ten sposób uniezależniając główny kontroler od pomysłów na mapowania URL. Listing 4 jest dowodem na to, że dzięki wprowadzonej zmianie można stosować zupełnie nowe strategie, np. posiłkując się implementacją konkretnych akcji przechowywaną w pamięci dzielonej. Słowo strategia pojawia się tutaj nieprzypadkowo, bowiem pokazana na Listingu 3 modyfikacja jest wzorcem projektowym Strategy. Dzięki niemu wyodrębniliśmy fragment programu, który może być łatwo podmieniany i dostosowany do konkretnych potrzeb. Możemy stosować różne strategie dla konkretnego kroku w większym algorytmie. Dopisujemy tylko tą część programu, która dostarcza nowej funkcjonalności i nie musimy modyfikować już istniejących klas. Idealne rozwiązanie.

Listing 3. Front Controller z wymienną strategią odnajdywania akcji

interface ActionResolvingStrategy {
   public function resolveAction(HttpRequest $request);
}
class FilePerActionResolvingStrategy implements 
  ActionResolvingStrategy {
  private $_actionRequestParamName;
  private $_actionFileNamePrefix;
  private $_actionFileNameSufix;
  public function __construct($actionRequestParamName, 
    $actionFileNamePrefix, $actionFileNameSufix) {
      $this->_actionRequestParamName = $actionRequestParamName;
      $this->_actionFileNamePrefix = $actionFileNamePrefix;
      $this->_actionFileNameSufix = $actionFileNameSufix;
  }
  public function resolveAction(HttpRequest $request) {
    $actionName = $request->getParam(
      $this->_actionRequestParamName);
    if ($actionName != '') {
      $actionClassFileName = $this->_actionFileNamePrefix.
        $actionName.$this->_actionFileNameSufix;
      if (!class_exists($actionName)) {
        if (file_exists($actionClassFileName)) {
          require_once ($actionClassFileName);
        } else {throw new RuntimeException(
            "Brak na dysku pliku z definicją akcji 
            '$actionClassFileName'");
            }
       }
    } else {throw new RuntimeException(
         'Nie wyspecyfikowano akcji do wywołania!');
    }
    $actionClass = new $actionName ();
    return $actionClass;
  }
}
class FrontControllerImpl implements FrontController {
  private $_actionResolvingStrategy;
  // Przy konstruowaniu FrontController-a ustalamy strategię,
  // według której będą odszukiwane akcje dla zapytania HTTP.
  public function __construct(ActionResolvingStrategy 
    $actionResolvingStrategy) {
    $this->_actionResolvingStrategy = $actionResolvingStrategy;
  }
  public function doService(HttpRequest $request) {
    // we FrontControllerze pozostaje jedynie wywołanie 
    // wcześniej ustalonej strategii. Cała logika związana z 
    // odnalezieniem akcji zawarta jest w klasie 
    // implementującej interfejs ActionResolvingStrategy
    $actionClass = $this->_actionResolvingStrategy->
       resolveAction($request);
    if (!is_null($actionClass)) {
       $actionClass->doAction($request);
    } else {throw new RuntimeException(
         'Nie znaleziono akcji do wykonania');
    }
  }
}
$fc = new FrontControllerImpl(
  new FilePerActionResolvingStrategy('action', 
  dirname(__FILE__).'/', '.php'));
$fc->doService(new HttpRequest());

Listing 4. Strategia odnajdywania akcji w cache

/**
 * Ta klasa korzysta z rozszerzenia MCache to przechowywania obiektów w
 * pamięci pomiędzy wywołaniami.
 * @link http://pecl.php.net/package/memcache*/
class MCacheActionResolver implements ActionResolvingStrategy {
   
   private $_actionRequestParamName;
   private $_memcache;
   
   public function __construct($actionRequestParamName, $memcacheHost, $memcachePort){
      $this->_actionRequestParamName = $actionRequestParamName;
      
      $memcache = new Memcache;
      $memcache->connect($memcacheHost, $memcachePort);
      $this->_memcache = $memcache;
   }
   public function resolveAction(HttpRequest $request){
      
      $actionName = $request->getParam($this->_actionRequestParamName);
      if ($actionName != ''){
         return $this->_memcache->get($actionName);         
      } else {
         throw new RuntimeException('Nie wyspecyfikowano akcji do wywołania!');
      }
   }
}
Composite

Bez problemu potrafimy już odszukiwać dowolne klasy zawierające implementację dla przesłanego parametru URL. Całe rozwiązanie działa bez najmniejszego zarzutu, ale ma jedno niedociągnięcie: implementacji możemy poszukiwać tylko w jednym miejscu. W dużej części praktycznych zastosowań to wystarczy, ale wyobraźmy sobie sytuację w której próbujemy najpierw odszukać potrzebną implementację w pamięci dzielonej, a w przypadku niepowodzenia - na dysku. Już wcześniej przygotowaliśmy oddzielne strategie przeszukiwania dysku i pamięci, teraz wystarczy tylko połączenie obu rozwiązań w jedną całość (Listing 5). Dodajmy jeszcze jedno wymaganie - jeśli potrzebna klasa nie zostanie odnaleziona w żadnym z wcześniej wskazanych miejsc, to obsługa wywołania jest delegowana do standardowej klasy obsługującej sytuacje wyjątkowe. Listing 6 pokazuje nasz przykładowy kod po wprowadzeniu zaproponowanych zmian. Analizując ten przykład łatwo zauważmy, że każdą kolejną strategię składamy z już gotowych elementów. Z punktu widzenia Front Controllera zupełnie nie ma znaczenia, czy potrzebna klasa jest poszukiwana w jednym, czy też w wielu miejscach. My natomiast zyskaliśmy nowe, potężne i elastyczne narzędzie - możliwość dowolnego łączenia podstawowych klocków w większe struktury. Zamiast od nowa pisać kod, posługujemy się kompozycją. Po raz kolejny okazuje się, że zmiany które właśnie wprowadziliśmy są bardzo często spotykane przy okazji różnych problemów programistycznych. Mamy więc standardowy problem i eleganckie rozwiązanie. Udało się nam zidentyfikować kolejny wzorzec projektowy - Composite. Jest to bardzo sprytny sposób na łączenie jednostkowych rozwiązań, pojedynczych funkcjonalności w zupełnie nowe, jeszcze potężniejsze moduły. Wzorzec ten znajduje bardzo szerokie zastosowanie, od obiektowej reprezentacji działań matematycznych po, jak przed chwilą widzieliśmy, budowę frameworków.

Listing 5. Kompozycja wielu strategii odnajdywania akcji

/**
 * Ta klasa nie dostarcza nowego sposobu odnajdywania akcji.
 * Zamiast tego, potrafi skorzystać z wielu przygotowanych wcześniej strategii.
 */
class CompositeActionResolver implements ActionResolvingStrategy {

   // W tej zmiennej przechowujemy zdefininowane strategie
   private $_definedStrategies = array ();

   public function __construct($definedStrategies) {
      $this->_definedStrategies = $definedStrategies;
   }
   public function resolveAction(HttpRequest $request) {

      // przeszukujemy po kolei wszystkie zdefiniowane strategie
      // i zwracamy pierwszą akcję znalezioną przez jakąś strategię
      foreach ($this->_definedStrategies as $strategy) {
         $actionFromStrategy = $strategy->resolveAction($request);
         if ($actionFromStrategy != null) {
            return $actionFromStrategy;
         }
      }
      return null;
   }
}
// do konstruktora Front Controller przekazujemy w dalszym ciągu tylko
// jedną strategię, wzorzec Composite ukrywa przed Front Controller fakt,
// iż teraz poszukujemy akcji na 2 różne sposoby
$fc = new FrontControllerImpl(new CompositeActionResolver(array (
      new MCacheActionResolver('action', 'localhost', 11211),
      new FilePerActionResolvingStrategy('action', dirname(__FILE__).'/', '.php'))));

$fc->doService(new HttpRequest());

Listing 6. Wzorzec Composite i strategia InstanceActionResolver

/**
 * Ta strategia zawsze zwraca konkretną instancję akcji.
 * Dzięki temu, że implementuje ona interfejs @see ActionResolvingStrategy,
 * może uczestniczyć w rozwiązywaniu akcji
 * przez @see CompositeActionResolver. 
 */
class InstanceActionResolver implements ActionResolvingStrategy {

   // W tej zmiennej przechowujemy konkretną instancję akcji.
   private $_action = null;

   public function __construct(MVCAction $action) {
      $this->_action = $action;
   }

   public function resolveAction(HttpRequest $request) {

      // niezależnie od parametrów wywołania zwracamy tą samą akcję
      return $this->_action; 
   }
}

// Klasa akcji dla sytuacji wyjątkowych.
class ErrorAction implements MVCAction {
   public function doAction(HttpRequest $request){
      echo "Wystąpił błąd!";
   }
}

$fc = new FrontControllerImpl(
   new CompositeActionResolver(array (
      new MCacheActionResolver('action', 'localhost', 11211),
      new FilePerActionResolvingStrategy('action', dirname(__FILE__).'/', '.php'),
      //CompositeActionResolver zawsze znajdzie tą akcje, jeśli powyższe 
      //metody zawiodą
      new InstanceActionResolver(new ErrorAction()))));

$fc->doService(new HttpRequest());
Dekorator

Biorąc pod uwagę wszystkie wspomniane ograniczenia, wydaje się, że trudno jest znaleźć miejsce dla fragmentu skryptu zliczającego statystyki. Na szczęście sytuacja nie jest beznadziejna, a wybawienie przychodzi ze strony kolejnego wzorca projektowego - dekorator (ang. Decorator). Po raz kolejny nazwa wzorca naprowadza nas na trop jego funkcjonalności: zamiast zmieniać istniejące klasy otoczmy je, udekorujmy nową funkcjonalnością. Cała idea stanie się oczywista, jeśli spojrzymy na Listing 7.

Listing 7. Dekorowanie strategii poszukiwania akcji

class StatisticsDecoratingActionResolver implements ActionResolvingStrategy {

   private $_decoratedStrategy = null;

   public function __construct(ActionResolvingStrategy $decoratedStrategy) {
      $this->_decoratedStrategy = $decoratedStrategy;
   }

   public function resolveAction(HttpRequest $request) {

      // najpierw wykonujemy oryginalny kod
      $returnValue = $this->_decoratedStrategy->resolveAction($request);

      // teraz zliczamy ilość konkretnych akcji
      //tu następuje wzbogacenie pierwotnej klasy o nową funkcjonalność
      if (!is_null($returnValue)) {
         $classActionName = get_class($returnValue);

         //tutaj umieszczamy kod zapisujący statystyki
         //dla akcji $classActionName
      }

      //zwracamy wartość przygotowaną przez oryginalny kod 
      return $returnValue;
   }
}

$fc = new FrontControllerImpl(
   //tutaj dekorator "opakowuje" oryginalną strategię odnajdywania akcji
   //zarówno dla Front Controllera jak i dla FilePerActionResolvingStrategy
   //to udekorowanie jest zupełnie przezroczyste
   new StatisticsDecoratingActionResolver(new FilePerActionResolvingStrategy('action', dirname(__FILE__).'/', '.php')));

$fc->doService(new HttpRequest());

Zabieg z wprowadzeniem dekoratora wykonujemy konstruując nową klasę, która implementuje dokładnie taki interfejs, jak klasa dekorowana. W samym dekoratorze możemy dodać nową funkcjonalność do dowolnie wybranych metod, wzbogacając niektóre o nową funkcjonalność, inne zaś pozostawiając bez zmian.

Po bliższym przyjrzeniu się Listingowi 7 zauważymy, że jeden obiekt może być udekorowany wielokrotnie. Bez problemu możemy wprowadzić inny dekorator, który umożliwia dostęp do aplikacji tylko w wyznaczonych godzinach. Wszystkie opisane modyfikacje są możliwe bez zmiany choćby jednej linijki kodu! Sterowanie funkcjonalnością głównego kontrolera odbywa się przez jego odpowiednie skonfigurowanie (przekazanie do konstruktora wybranej implementacji interfejsu ActionResolvingStrategy).

Informacje na podobny temat:
Wasze opinie
Wszystkie opinie użytkowników: (2)
diagram uml
Sobota 19 Lipiec 2008 9:15:48 pm - szefoski

czy tylko ja nie moge obejrzec w wiekszych rozmiarach diagramu UML?

Wzorce
Niedziela 09 Wrzesień 2007 3:05:40 pm - sp_

W artykuie zaproponowano podział wzorców projektowych na architektoniczne i programistyczne. Myślę, że lepsze byłoby wydzielenie wzorców typowo programistycznych z wzorców projektowych. Powstałby wtedy podział na wzorce projektowe i wzroce programistyczne, co byłoby chyba bardziej naturalne.

Mentax.pl    NQ.pl- serwery z dodatkiem świętego spokoju...   
O nas | Kontakt | Mapa serwisu
Copyright (c) 2003-2024 php.pl    Wszystkie prawa zastrzeżone    Powered by eZ publish Content Management System eZ publish Content Management System