Archiwum z maj, 2009

Cechy dobrego projektu - 4. Zasada open / close

Środa, maj 6th, 2009

software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification [Bertrand Meyer]

Nasz kod powinien być zamknięty na modyfikację i otwarty na rozszerzanie. Inaczej mówiąc: rozwijając projekt, nie powinniśmy zmieniać istniejącego kodu, a jedynie go rozszerzać.

Gdy zmieniamy coś w istniejącym kodzie, bardzo łatwo zepsuć inną funkcjonalność, która z tego fragmentu kodu korzysta.
Zgodnie z zasadą open/close należy unikać modyfikacji istniejącego kodu.

Przykład:
Piszemy prostą aplikację, która wypisuje zawartość pliku tekstowego na ekran numerując linie pliku.

Do dyspozycji mamy funkcję render_file() która ma za zadanie wypisać na wyjście zawartość pliku z ponumerowanymi liniami. Funkcja render_file() używa klasy SimpleSequence, która umożliwia wygenerowanie kolejnych liczb naturalnych.

 
class SimpleSequence
{
    protected $i=0;
    public function next() { return $i++; }
}
 
function render_file($file_name)
{
     $seq = new SimpleSequence();
     foreach( file($file_name) as $line)
     {
          printf("%s: %s", $seq->next(), $line);        
     }
}

index.php:

render_file("myfile.txt");

Założenia są spełnione. Nasz program działa prawidłowo.

Jednak co w przypadku, gdy będziemy musieli zmienić jego działanie i numerować linie pliku za pomocą liter “a” - “z”?

Będziemy musieli zmienić zawartość funkcji render_file(), a więc nie jest spełniona zasada open/close.

Jak zrobić to lepiej?

Zmodyfikujmy nieco nasz program przez dodanie interfejsu ISequence.

 
interface ISequence
{
    public function next();
}
 
class SimpleSequence implements ISequence
{
    protected $i=0;
    public function next() { return $this->i++; }
}
 
function render_file($file_name, ISequence $seq)
{
     foreach( file($file_name) as $line)
     {
          printf("%s: %s", $seq->next(), $line);        
     }
}

index.php:

render_file("myfile.txt", new SimpleSequence());

Co zyskujemy?
NIE MUSIMY zmieniać ani implementacji metody render_file(), ani klasy SimpleSequence.

Teraz aby wypisać zawartość pliku “numerując” linie kolejnymi literami alfabety wystarczy zaimplementować nową klasę implementującą interfejs ISequence, np. AlphaSequence. Klasa ta za pomocą metody next() będzie zwracać kolejne litery alfabetu.

index.php:

class AlphaSequence implements ISequence
{
    protected $chars = 'abcdefghijklmnopqrstuvwxyz';
    protected $i = 0;
    public function next() {
        return $this->chars[ $this->i++ % strlen($this->chars) ];
    }
}
render_file("myfile.txt", new AlphaSequence());

To tylko prosty przykład. Tak naprawdę konieczność stosowania tej zasady poznajemy, gdy pracujemy z setkami tysięcy linii kodu. W praktyce wygląda to tak, że każda zmiana istniejącego kodu może powodować nieoczekiwane zmiany w istniejących już elementach naszego projektu (tu bardzo ważna jest zasada “Low coupling, high cohesion” - minimalizacja zależności pomiędzy modułami, maksymalizacja spójności modułów).

OOP: Singleton - kiedy użyć tylko jednego obiektu klasy?

Wtorek, maj 5th, 2009

Potrzebujesz jednej instancji (obiektu) klasy w czasie uruchomienia aplikacji? Potrzebujesz, aby instancja klasy dostępnej z dowolnego miejsca w kodzie? Możesz użyć wzorca singleton.

Najprostsza klasa implementująca wzorzec singleton w PHP wygląda następująco:

class Singleton
{
    private static $instance = null;
    private __construct() {};
    public static getInstance()
    {
        if(self::$instance == null) 
            self::$instance = new Singleton();
        return self::$instance;
    }
}

Przykład użycia - tworzymy klasę, która udostępnia szczegóły zapytania HttpRequest:

// Request.class.php:
class Request
{
    private $params;
    private static $instance = null;
    private __construct() 
    {
        $this->params = array_merge($_GET, $_POST);
    };
    public static getInstance()
    {
        if(self::$instance == null) 
            self::$instance = new Singleton();
        return self::$instance;
    }
 
    public function get($key, $default=null)
    {
        return isset($this->params[$key]) 
            ? $this->params[$key] : $default;
    }
}

Teraz instancji klasy Request możemy używać w wielu miejscach w kodzie, np. w pliku kontrolera:

// index.php:
function showPage()
{
    echo 'Hello, ' 
        . Request::getInstance()->get("Name", 'World');    
}

jak również w innym, dowolnym miejscu naszego projektu:

// OtherClass.php
 
class OtherClass
{
    public processForm()
    {
       $request = Request::getInstance();
        if( ! $request->get("name") )
        { 
            throw new Exception("Invalid name");
        }
    }
}

Zalety:
- jednokrotna inicjalizacja (w powyższym przykładzie konstruktor jest wywoływany co najwyżej raz w czasie uruchomienia skryptu).
- możliwość przenoszenia danych pomiędzy warstwami (np. obiekt Request może być użyty zarówno w kontrolerze, jak i widoku).
- użycie Singletona jest lepsze od zmiennych globanych. Nie zaśmieca przestrzeni zmiennych globalnych. Za pomocą Singletona można zahermetyzować w jednej klasie powiązane ze sobą dane.

Wady:
- Wzorzec Singleton użyty niewłaściwie może wprowadzać chaos w kodzie (podobnie jak zmienne globalne).

Kiedy używać?

Wzorzec singleton jest często używany do implementacji:
- obiektów, przechowujących informacje o stanie aplikacji,
- obiektów które powinny istnieć przez cały czas trwania aplikacji,
- fabryk abstrakcyjnych (AbstractFactory)

Przykłady użycia:

Klasa Log - używana w wielu miejscach zapisuje logi do pliku:

Log::getInstance()->message("start...");

Fabryka abstrakcyjna - tworzy instancję określonej fabryki (o wzorcu fabryki postaram się napisać w kolejnych postach)

$type = "MyProductFactory";
AbstractFactory::getIstance()->createFactory($type);

Fasada. Wyobraźmy sobię klasę CheckoutFacade. Metoda placeOrder() wykonuje kilka logicznie powiązanych ze sobą operacji. Metoda ta zapisuje dane z koszyka i dane klienta do bazy danych z zamówieniami, oraz wysyła maila z potwierdzeniem zamówienia.

$facade = CheckoutFacade->getInstance();
$facade->placeOrder($CartData, $CustomerData);

Podany przykład użycia wzorca Singleton przy implementacji klasy Request ma pewne wady (o tym postaram się napisać w kolejnych postach). Jednak w prostych aplikacjach webowych się sprawdza.