Cechy dobrego projektu - 4. Zasada open / close

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?

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.

Cechy dobrego projektu - 3. Minimum złożoności

kwiecień 14th, 2009

Jak radzić sobie z dużymi problemami lub skomplikowanymi wymaganiami funkcjonalnymi naszego projektu?

  • Duże problemy dziel na mniejsze “podproblemy”, tak aby można było je rozwiązać niezależnie od siebie (zasada dekompozycji).
  • Eliminuj mało istotne szczegóły problemu, wyodrębniaj cechy wspólne (zasada abstrakcji).

podproblemy-abstr

Przykład:

Tworzona przez nas aplikacja ma za zadanie wczytywać katalog produktów do bazy danych. Program powinien mieć możliwość wczytania danych do katalogu z pliku Excel’a znajdującego się na lokalnym dysku lub połączyć się z serwerem FTP i pobrać plik w formacie CSV.

Złe podejście:

Wydaje nam się, że problem jest na tyle prosty, że całość możemy zapisać w jednej metodzie, np.:

void import(bool downloadFromFtp) {
    if(downloadFromFtp) {
       // ...    
    } else {
       // ...
    }
}

Metoda import() w zależności od wartości parametru downoadFromFtp otworzy plik CSV i wczyta dane do naszej bazy danych lub pobierze plik w formacie .xls przez ftp i wczyta produkty do bazy.

Jednak co w przypadku, gdy klient zamówi dodatkową funkcjonalność - możliwość pobrania katalogu produktów w formacie XML przez web service? Musimy znowu zagłębiać się w szczegóły napisanej wcześniej metody import(). Musimy zmieniać argumenty przekazywane do tej metody i wszystkie miejsca w których metoda ta była używana. Oprócz tego, że piszemy procedurę odczytu z pliku XML, musimy napisać jeszcze raz procedurę zapisu do bazy produktów w sklepie.
Metoda import() robi się co raz większa i co raz mniej czytelna.
Ponadto - jeżeli zmieni się struktura naszej bazy produktów w sklepie, będziemy musieli zmienić kod w 3 miejscach - przy implementacji zapisu danych pobranych z pliku Excel, CSV i XML.

Spróbujmy więc rozwiązać problem inaczej…

Dobre podejście: (zgodne z zasadą dekompozycji i abstrakcji).

W pierwszym kroku, zamiast zajmować się szczegółami odczytu plików Excel, CSV i połączenia z FTP próbujemy wydzielić część wspólną. Wiemy, że nasz program powinien pobierać katalog produktów z różnych źródeł i zapisywać go do naszej bazy. Dlatego możemy utworzyć następujący interfejs (Java):

public interface ProductImporter {
    public Products importProducts();
}

Gdy mamy już wspólny interfejs, możemy przejść do szczegółów. Implementujemy 2 niezależne od siebie importery - jeden z lokalnego pliku Excel’a, a drugi z pliku CSV znajdującego się na serwerze:

public class CsvProductImporter implements ProductImporter {
    public Products importProducts() {
         // ....implementacja
    }
}

a osobno importer z pliku Excel na zdalnym serwerze ftp

class FtpXlsProductImporter implements ProductImporter {
    public Products importProducts() {
         // ....implementacja
    }
}

Nasze importery zwracają jednakowy format danych - kolekcję produktów. Teraz implementujemy operację zapisu tych danych do naszej bazy, np.

class ProductManager {
    public store(Products products) {
        // ...
    }
}

Zalety takiego podejścia:

  • Aby umożliwić wczytywanie danych z innego źródła (np. z pliku XML), nie musimy przerabiać istniejącego kodu - wystarczy, że zaimplementujemy nową klasę importera, np. XmlProductImporter
  • Importery są niezależne od struktury danych w bazie. Gdy zmieni nam się struktura danych w bazie, nie musimy modyfikować poszczególnych importerów
  • Kompozycja klas i interfejsów jest bliska naturalnemu opisowi problemu, przez to bardziej czytelna i łatwiejsza w zrozumieniu.

Nie tylko refaktoring

kwiecień 14th, 2009

Polecam ciekawą lekturę Mariusza Sieraczkiewicza o refactoringu, pisaniu ładnego, czytelnego kodu - “i nie tylko”: http://www.bnsit.pl/files/nie_tylko_refaktoring.pdf

Cechy dobrego projektu - 2. Oszczędność (KISS, DRY)

kwiecień 13th, 2009

„Książka jest skończona nie wtedy, gdy nie można do niej nic dodać, ale wtedy gdy nie można z niej nic usunąć”.

Nie komplikuj kodu (zasada KISS - z ang. Keep It Simple, Stupid). Staraj się zaprojektować możliwie prostą strukturę aplikacji. Unikaj niepotrzebnych elementów.

Dbaj o dobrą hermetyzację klas. Używaj najbardziej restrykcyjnych modyfikatorów dostępu do właściwości klas, na ile to możliwe (private, protected).

Zamiast używać wielu podobnych mechanizmów, spróbuj zaprojektować jeden, którego będziesz mógł użyć w każdym wypadku.

Gdy coś napiszesz, zastanów się, czy nie dałoby się tego zapisać prościej.

Nawet w tak banalnym przykładzie (PHP):

if( $condition ) {
    return true;
} else {
   return false;
}

warto się zastanowić - czy nie lepiej użyć prostszego zapisu, np tak:

return ( bool ) $condition;

Nie powtarzaj się (zasada DRY- Dont’ Repeat Yourself). “kopiuj i wklej” to operacja, której trzeba się wystrzegać pisania kodu. Jeżeli kusi Cię możliwość szybkiego skopiowania kodu z innego fragmentu projektu, pomyśl o przeniesieniu go do osobnej funkcji, klasy bazowej.

Reguła Rule Of Three dopuszcza co prawda jednokrotne skopiowanie kodu, ale trzeci raz nie powinno kopiować się powielać tego samego kodu.

Cechy dobrego projektu - 1. Czytelność

kwiecień 13th, 2009

Tworząc kod pamiętaj o tym, że w przyszłości Ty sam lub inny programista będzie chciał go zrozumieć. Warto również pamiętać, że w projektach informatycznych przyszłość nadchodzi szybciej niż nam się wydaje :)

Dlatego:

Korzystaj ze sprawdzonych, uniwersalnych schematów.

Unikaj programistycznej prowizorki, rozwiązań tymczasowych, obejść “na skróty” (tzw. workaround).

Trzymaj się ustalonych, jednolitych standardów kodowania i nazewnictwa (np. nazwy klasy z dużej litery, prywatne składowe poprzedzane podkreśleniem “_”, nazwy interfejsów od litery I).

Pisz samodokumentujący się kod, dbaj o komentarze na etapie tworzenia kodu.

Cechy dobrego projektu

kwiecień 12th, 2009

Próbowałeś wielokrotnie wykorzystać swój kod? Gdy projekt rozrastał, miałeś ochotę napisać wszystko od początku?
Jeżeli tak, to być może znajdziesz na tym blogu coś dla siebie.

Na studiach uczono nas o “cechach dobrego projektu” - było to kilka prostych reguł nie popartych przykładami. Dopiero podczas pracy przy komercyjnych projektach te proste regułki okazały się “żelaznymi” zasadami. Nie stosowanie się do nich wiąże się z trudnościami w rozwijaniu i utrzymaniu projektu.

Będzie trochę o zasadach i o sprawdzonych praktykach w inżynierii oprogramowania (w szczególności aplikacji webowych). Jeżeli o czymś będę pisał, to na podstawie własnych doświadczeń. Staram się popierać teorię mniej lub bardziej sensownymi przykładami.