Dependency Injection

To co odróżnia programistę od dobrego programisty to umiejętność projektowania aplikacji, przewidywania drogi, jaką pójdzie rozwój projektu i pozostawienie sobie furtki do łatwych modyfikacji w przyszłości. Początkujący bardzo często mylą sobie naukę programowania z nauką tworzenia dobrego kodu. Szczególnie widoczne jest to wśród programistów PHP, gdzie do nauczenia się podstaw języka wystarczy kilka wieczorów. Dzisiaj chciałbym rozpocząć serię postów, które mam nadzieję pomogą przy tworzeniu takiego kodu, który będzie łatwo utrzymać i wykorzystać w przyszłych projektach.

Zakładam, że czytelnik zaznajomiony jest z ideą OOP. Wie, co to klasy, obiekty, metody. Wie, co powinna reprezentować sobą klasa, a co obiekt.

Podstawowym problemem podczas projektowania kodu dowolnych rozmiarów są zależności między obiektami. Bardzo często programista nie jest świadomy konsekwencji błędów związanych ze złym tworzeniem i utrzymywaniem powiązań między modułami skryptu. Przekreślają one możliwość szybkiego dostosowania komponentu pod warunki nowej wersji oprogramowania czy nowego projektu.

Rozwiązaniem jest Inversion of Control (ang. odwrócenie sterowania), którego celem jest przeniesienie poza komponent odpowiedzialności za kontrolę wybranych czynności. Jednym ze wzorców projektowych bezpośrednio związanych z IoC jest Dependency Injection (ang. wstrzykiwanie zależności), którym się zajmiemy.

Zacznijmy od prostych przykładów. Wyobraźmy sobie, że pracujemy nad własnym systemem szablonów (nigdy nie byłem dobry w wymyślaniu problemów do zaprezentowania – akurat mam doświadczenia po starciu z Templating Component + Twigiem, gdzie rzecz rozwiązana była wyśmienicie).

Naszą klasę zbudujmy najpierw w oparciu o „standardowe podejście”. Na przykład coś takiego:

class Project_View {
    public function render($file) {
        include $file;
    }
}

Cóż… dobrym pomysłem byłoby sprawdzić chociaż, czy plik istnieje, czy aby na pewno powinniśmy mieć mozliwość dołączenia go w ten sposób itp. itd. Utwórzmy więc klasę Project_View_Loader która zajmie się brudną robotą. Przystosujmy do niej nasz prosty system szablonów.

class Project_View {
    protected $loader;

    public function __construct() {
        $this->loader = new Project_View_Loader();
    }

    public function render($file) {
        $this->loader->load($file);
    }
}

Przyjmując, że wewnątrz Loadera filtrujemy nieco zapędy programisty, nasz kod wygląda już znacznie lepiej. W pewnym momencie rozwoju nasze założenia się nieco zmieniły (zdarza się). Postanowiliśmy wczytywać szablony z bazy danych. Podmieniamy więc naszą klasę Project_View_Loader na coś, co obsłuży nam nasze nowe wymagania.

Wydawać by się mogło, że rzecz jest całkiem elastyczna. Wystarczy podmienić jedną klasę i możemy wczytywać szablony ze zmiennych, plików, cache, bazy danych, zewnętrznego serwera, czy co tam sobie kto wymyśli. Pozory! Dobry kod, to kod który możemy później wykorzystać. A w różnych projektach możemy potrzebować wczytywania z różnych źródeł. Musimy jakoś dołączyć wszystkie strategie ładowania.

abstract class Project_View_Loader { /*...*/ }

class Project_View_Loader_Variable
  extends Project_View_Loader { /*...*/ }
class Project_View_Loader_File
  extends Project_View_Loader { /*...*/ }
class Project_View_Loader_Database
  extends Project_View_Loader { /*...*/ }

Wydzieliliśmy abstrakcyjną klasę, w której możemy napisać metody ogólnego użytku, a także kilka wyspecjalizowanych klas do realizacji poszczególnych strategii. Teraz wystarczy tylko zapisać odpowiednią klasę w zawartości konstruktora.

Według mnie jednak, dalej jest to złe rozwiązanie. Po pierwsze nasz kod jest prosty aż do bólu, połapać się można szybko. Co jednak z bardziej zaawansowanymi skryptami, gdzie dotarcie do miejsca wyboru sposobu obsługi zadania bywa bardzo trudne i często rozłożone jest w wielu różnych miejscach? Po drugie zakładamy stały rozwój projektu (jeżeli nie, to warto się zastanowić nad sensem jego rozpoczynania), możemy szykować się na różne aktualizacje, które nadpiszą nam nasz plik. Zmianę będzie trzeba dokonywać wraz z każdą wersją.

Idziemy dalej, zmodyfikujmy nieco nasz konstruktor:

public function __construct($strategy) {
    $class = 'Project_View_Loader_' . $strategy;
    $this->loader = new $class();
}

Pozornie zlikwidowaliśmy wszystkie problemy. Aktualizacje nie zmuszą nas do ciągłych poprawek, mamy kontrolę nad wybraną czynnością komponentu. Czy aby na pewno? Dobry zwyczaj nakazuje oddzielać gotowe biblioteki od swojego kodu przestrzeniami nazw. Ktoś z zewnątrz może potrzebować strategii ładowania, która najpierw sprawdzi cache, potem bazę danych a na koniec poszuka pliku. Swoją klasę nazwie powiedzmy Custom_View_Loader_Chain. I co teraz?

Dochodzimy do sedna Dependency Injection. Zamiast tworzyć zależności wewnątrz klasy, spróbujmy je wstrzyknąć z zewnątrz. Dzięki temu aplikacja zyskuje kontrolę nad wybranymi czynnościami komponentu.

Nasz finalny projekt będzie wyglądał mniej więcej tak:

class Project_View {
    protected $loader;
    protected $renderer;

    public function __construct(Project_View_Loader $loder) {
        $this->loader = $loader
    }

    public function render($template) {
        $this->loader->load($template);
    }
}

$loader = new Custom_View_Loader_Chain(array(
    new Project_View_Loader_Cache(),
    new Project_View_Loader_Database(),
    new Project_View_Loader_Filesystem()
));

$view = new Project_View($loader);

Zwróćmy uwagę, że proces przygotowywania naszego loadera jest również zgodna z ideą Dependency Injection. Poszczególne punkty sprawdzania istnienia szablonu przekazujemy z zewnątrz, zamiast sztywno zakodować wewnątrz klasy.

Na zakończenie trochę teorii. Dependency Injection jest sposobem osiągnięcia luźnych powiązań między komponentami. Szczytem w tej dziedzinie jest chyba Zend Framework, gdzie programiści dali nam możliwość kontroli niemal wszystkiego. I według mnie trochę przesadzili. Zostawiając jednak frameworka firmy od PHP i skupiając się na własnych dziełach: nie próbujmy używać wzorców w każdej możliwej sytuacji, na siłę. Stosujmy je wtedy, gdy faktycznie są one rozwiązaniem pewnego problemu. Humorystycznie problem oddaje Hello World we wzorcach projektowych

Wstrzykiwanie zależności możemy wykonać na kilka sposobów. Poprzez konstruktor (jak w przykładzie powyżej), poprzez settery i gettery (setLoader() i getLoader()), lub po prostu przez pola obiektu. I tu praktyczna wskazówka ode mnie: wymaganych obiektów oczekujmy w konstruktorze, opcjonalnych poprzez settery i gettery. Dodatkowo czasem warto zapewnić możliwość zmiany decyzji poprzez settery i gettery dla zależności wymaganych. Bezpośredniego dostępu do pól obiektu nie polecam. Chyba, że __set() i __get() będą odwoływać się potem do odpowiednich metod.

Ten wpis został opublikowany w kategorii Dobre nawyki, PHP, Projektowanie, Teoria, Wzorce projektowe. Dodaj zakładkę do bezpośredniego odnośnika.

4 odpowiedzi na „Dependency Injection

  1. Jako, że przeoczyłem w głównym nurcie tekstu, to dopiszę jeszcze tutaj:

    Luźne powiązanie komponentów to nie tylko łatwe sterowanie pracą aplikacji. To także ogromne ułatwienie podczas testów jednostkowych, gdzie możemy wstrzyknąć makietę obiektu, która zbada czy wszystkie operacje wykonywane są prawidłowo. Więcej na ten temat w przyszłym wpisie dotyczącym testowania.

    A tak poza tym to zapraszam do komentowania. Nie piszę przecież dla siebie. Jakie są Wasze przemyślenia, doświadczenia na ten temat? Nic tak nie motywuje jak dobre słowo i konstruktywna krytyka. :)

  2. …o czym, zgodnie z wcześniejszą zapowiedzią, będę pisał w następnym odcinku.

  3. prohol pisze:

    Super art. Czytalem juz wczesniej o DI m.in na stronie podanej przez wojtek ale dopiero po tym arcie zrozumialem do konca sensownosc tego rozwiazania.

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *

*

Możesz użyć następujących tagów oraz atrybutów HTML-a: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>