Dependency Injection patern i IoC container – Recept za lako održiv kod
Autor: Ivan Đurđevac |
Datum:
  • # PHP Science

Svi želimo da softver koji pišemo i aplikacije koje razvijamo budu visokog kvaliteta. Želimo da po završetku kreiranja aplikacije budemo zadovoljni kodom koji smo napisali. Nažalost ili na sreću, mi programeri nikada nismo zadovoljni svojim kodom. Sa druge strane, postoje softverske tehnike i paterni čijom ćemo primenom kod učiniti boljim, kvalitetnijim i lakšim za održavanje. Kao softverski inženjeri u obavezi smo da svakoga dana primenjujemo dokazane orijentisane principe u programiranju i da svoje znanje osvežavamo novim paternima, tehnikama i drugačijim pogledima na rešavanje problema.

Dizajniranje arhitekture aplikacije podrazumeva kreiranje raznih objekata. Kako složenost raste, raste broj objekata kao i broj njihovih međusobnih veza. Ako su veze između objekata čvrste (eng. tightly coupled) biće znatno teže menjati ili nadograđivati aplikaciju. Zato se moramo truditi da ove veze budu što labavije (eng. loosely coupled) kako bi spremnije dočekali izmene. Ovo je još važnije ako razvijamo aplikaciju koja će se razvijati i nadograđivati dugi niz godina. I sami znate koliko se često dešava da se zahtevi od strane klijenta menjaju i pre nego što sama funkcionalnost bude razvijena u potpunosti. Upravo zbog toga vi kao softverski inženjer morate da budete spremni na promene i da im se radujete.

Dependency injection

Čak i da niste čuli za izraz "Dependency Injection" to ne znači da ga niste koristili. Martin Fowler ga je definisao kao prosleđivanje objekata drugim objektima kojima su potrebni, umesto da ih oni sami kreiraju. To znači da ćemo klasi proslediti objekte koji su joj potrebni i smanjiti njenu odgovornost da ih sama kreira. Proslećivanje objekata klasi naziva se injektovanje.

Objekte možemo injektovati na nekoliko načina:

  • Direktno: preko konstruktora ili seter metode
  • Konfiruracijski: XML ili YAML fajlovi
  • Anotacije u komentarima

Injektovanje preko konstruktora i/ili seter metode

Sada ćemo primer iz realnog života da prenesemo u objektni svet PHP jezika. Porodica Ružić ima malo poljoprivredno gazdinstvo. Od poljopivrednih mašina poseduju dva stara stara IMT traktora. Pogledajmo klasu koja će opisati traktor.

class Tractor
{
    protected $engine;
    public function __construct($cubic)
    {
        $this->engine = new Engine($cubic);
    }
}

U primeru iznad objekat Tractor je kreirao objekat Engine koji mu je potreban za rad.

Možemo da pođemo od pretpostavke da ovo staro poljoprivredno gazdinstvo neće traktore zameniti novim sa jačim motorima i u tom slučuja ovaj kod možemo prihvatiti. Problem je što se u realnom svetu sve menja, pa tako i klasa iz našeg primera mora biti spremna na promene. Sin koji će preuzeti gazdinstvo zameniće dotrajale traktore novim, a naš kod više neće biti aktuelan. Ovo je primer čvrste veze između objekata koji bi trebalo izbegavati.

Šta bi se desilo ako osim zapremine klasa Motor treba da primi i informaciju da li se radi o dizel ili benzinskom motoru? Osim klase motora izmena bi se reflektovala na klasu Traktor tako i na svaki drugi objekat sa kojim je u čvrstoj vezi. Kada ste prinuđeni da zbog jedne izmene lančano menjate i prilagođavate druge objekte jednoj izmeni, znajte da ste loše ogranizovali arhitekturu aplikacije. Tada je važno da sagledate uzroke i da nakon iz ovoga izađete iskusniji i ne ponovite istu grešku.

class Tractor
{
    protected $engine;
    public function __construnct($engine)
    {
        $this->engine = $engine;
    }
    public function setEngine($engine)
    {
        $this->engine = $engine;
    }
}

U ovom slučaju smo primenili Dependency Injection jer smo objektu Tractor prosledili objekat Engine od kojeg zavisi. Injektovanje smo izvršili preko konstruktora klase Tractor.

Kada bi sada došlo do promene u konstruktoru klase Engine to se ne bi odrazilo na Tractor niti na bilo koju drugu poljoprivrednu mašinu koja koristi Engine. Kod koji koristi labave veze je lakše održavati.

Isti efekat ćemo dobiti ako objekat Engine injectujemo preko stter metode. To ćemo uraditi ovako:

class Tractor
{
    protected $engine;
    public function setEngine($engine)
    {
        $this->engine = $engine;
    }
}

Injektovanje korišćenjem  XML ili YAML fajlova

Zavisnosti možete opisati u XML ili YAML fajlovima i na taj način registraciju izmestiti iz koda same aplikacije. U ovom slučaju neophodno je paziti na veličinu fajla jer će performanse opasti ako aplikacija mora da parsira suviše veliki fajl. Tada možete da zavsinosti razdvojite po modulima aplikacije ili primenite lazy loading pristup kako aplikacija ne bi učitavala nepotrebne zavisnosti.

use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\Loader\XmlFileLoader;
 
$container = new ContainerBuilder();
$loader = new XmlFileLoader($container, new FileLocator(__DIR__));
$loader->load('services.xml');

 

< !-- src/Acme/HelloBundle/Resources/config/services.xml -->

    < !-- ... -->
    sendmail

 

    
        %mailer.transport%       
    

Injektovanje korišćenjem anotacija

Anotacije u komentarima su odlično mesto za opisivanje zavisnosti između objekata. Ovo je najelegantniji način za stvaranje veza između objekata. Ipak, nekima se ovo neće dopasti jer smatraju da će parsiranje komentara biti prevelika cena za eleganciju koju dobijamo.

Projekat PHP-DI jeste Dependency Injection biblioteka koja koristi anotacije. Korišćenje ove biblioteke je više nego jednostavno.

use DI\Annotations\Inject;
 
class Foo {
    /**
    * @Inject
    * @var Bar
    */
    private $bar;
 
    public function __construct() {
        // The dependency is injected
        return $this->bar->sayHello();
    }
}

Šta je Inversion Of Control ?

Inversion of control je programerska tehnika koja će obezbediti da svaka klasa u toku izvršenja aplikacije dobije sve objekte koji su joj neophodni. IoC container je objekat koji se brine o odnosima i vezama između objekata. Ono što će nam IoC container obezbediti jeste da u bilo kom trenutku objekte koje prosleđujemo možemo zameniti sa objektom druge klase i aplikacija će nastaviti da funkcioniše nesmetano. U ovom slučaju mora se ispuniti uslov da objekte koje zamenjujemo imaju identičan interfejs. Kao što smo već spomenuli objekte možemo zameniti sa Mock objektima radi testiranja aplikacije ili sa objektom druge klase zato što je došlo do izmene u zahtevu od strane klijenta.

Uzmimo kao primer sistem za keširanje. Ako u startu keširanje radimo preko klase koja radi sa fajlovima možemo je kasnije vrlo lako zameniti sa drugom klasom za keširanje koja koristi APC. Jednostavnom nadogradnjom aplikacije možemo omogućiti korisnicima da konfiguracijom biraju koju vrstu keširanja će aplikacija koristiti. Uloga IoC containera će biti da u zavisnosti od podešavanja aplikaciji isporuči potreban objekat za keširanje. To znači da će zapravo korisnik birati klasu objekta koji će IoC container dostaviti našoj aplikaciji. Ovo je samo jedan scenario koji predstavlja mogućnosti i lakoću održavanja koju nudi IoC container.

Snaga IoC containera se vidi u složenim aplikacijama u kojima su odnosi između objekata lančani. Primer koji sledi će vas ubediti da treba da koristite IoC container u svojoj aplikaciji.

// Bez Ioc Containera
$shipping_service = newShippingService(new ProductLocator(),new PricingService(),new InventoryService(),new TrackingRepository(new ConfigProvider()),new Logger(new EmailLogger(new ConfigProvider())));

// Sa Ioc Containerom
$shipping_service = $container->get('services.shipping');

 U daljem tekstu ćemo razjasniti kako zaista funkcioniše IoC container.

Symfony komponenta - Dependency injection

Predstaviću vam symfony komponentu koja je potpuno nezavisna i može se integrisati sa bilo kojom PHP aplikacijom ili framework koji koristite. Za rad su mu potrebni paketi symfony yaml i symfony config ako ćete sa njima konfigurisati zavisnosti između objekata. Komponentu možete povući sa composera ili github respozitorijuma.

Kreiranje objekta klase Traktor sa Symfony komponentom:

use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
 
// Create IoC container
$container = new ContainerBuilder();
 
// Register Engine object and parameter for constructor
$container->register('my.engine', '\\Model\\Engine')
->addArgument(1800);
 
//Register Tractor object and Engine object for his constructor
$container->register('my.tractor', '\\Model\\Tractor')
->addArgument(new Reference('my.engine'));
 
// IoC container will return Tractor object
$tractor = $container->get('my.tractor');

Symfony Dependency injection

Prvo smo kreirali IoC container koji je zadužen za registrovanje i dostavljanje objekata. Registrovali smo objekat klase Engine sa alijasom "my.engine" metodom register. Metodom addArgument() smo container-u rekli da će se prilikom kreiranja objekta sa alijasom "my.engine" kao parametar u konstruktoru proslediti broj 1800 koji predstavlja broj kubika motora. Registrovali smo i objekat klase Tractor sa alijasom "my.tractor". Kao parametar za konstruktor postavili smo referencu na "my.engine". Container je kao prvi parametar prosledio objekat koji je definisan alijasom "my.tractor".

Kada container-u kažemo da nam dostavi objekat sa alijasom "my.tractor" on će uraditi sledeće:

  • Među registrovanim objektima pronaći će klasu koja predstavlja "my.tractor"
  • Kreiraće prvi parametar koji treba da prosledi Traktor klasi ćiji je alijas "my.engine"
  • Kreiraće objekat klase Engine i proslediti mu parametar koji je definisan prilikom registracije alijasa
  • Objekat Engine će proslediti konstruktoru klase Tractor
  • Vratiće nam objekat Tractor koji u sebi ima objekte od kojih je zavisan

Ovo je osnovna upotreba moćne komponente symfony dependency injection koja može da odgovori mnogo zahtevnijim scenarijima.

Injektovanje objekta seter metodom

Ukoliko objektu klase Tractor objekat klase Engine prosleđujemo sa seter metodom, a ne preko konstruktora onda ćemo za konfigurisanje objekata koristiti i metodu addMethodCall(). Ovom metodom ćemo containeru da kažemo da kada zatražimo da nam dostavi objekat koji je registrovan sa alijasom "my.tractor" on odmah nakon kreiranja objekta pozove setter metodu i prosledi joj definisane parametre.

use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
 
class Tractor
{
    /**
    * Engine
    *
    * @var \Model\Engine
    */
    private $engine;
    
    /**
    * Seter method for Engine
    *
    * @param \Model\Engine $engine
    */
    public function setEngine(\Model\Engine $engine)
    {
        $this->engine = $engine;
    }
}
 
// Register Engine class and set parameter for constructor
$container = new ContainerBuilder();
 
// Register Tractor object and define param with Engine class for serEngine method
$container->register('my.tractor', '\\Model\\Tractor')
->addMethodCall('setEngine', array(new Reference('my.engine')));	
 
// Register param for Engine constructor
$container->register('my.engine', '\\Model\\Engine')
->addArgument(1800);
 
// IoC container will return object registered as my.tractor
$traktor = $container->get('my.tractor');

Registracija objekata i njihovih zavisnosti u praksi ne bi trebalo da bude razbacana po kodu već sistematizovana na centralizovanom mestu u aplikaciji. Jedna od ideja jeste da kreirate posebne RegisterDependencies klase koje će u metodi registerClasses() registrovati zavisnosti u određenom modulu aplikacije. Ovo je jedan način a vi ćete svoju aplikaciju organizovati u skladu sa potrebama i zahtevima koje imate ispred sebe.

Dependency Injection će vam pomoći da smanjite zavisnosti između objekata što će vaš kod učiniti lakšim za održavanje i otvorenim za nadogradnju. IoC container je zadužen da vodi računa o zavisnostima u vašoj aplikaciji i da svaki objekat snabde sa objektima koji su mu potrebni za funkcionisanje.

Zend, Symfony i Laravel imaju dependency injection komponente u svojim najnovijim verzijama. FuelPHP ima u planu razvoj dependency injection komponente. Očigledno je da je PHP zajednica postala svesna potrebe za primenom dependency injection paterna. Ako želite da vaš kod učinite lakšim za održavanje i nadogradnju, ovo je dokazana programerska praksa koja će vam u tome pomoći. Ako planirate razvoj velikog projekta koji će se dugo razvijati i nadograđivati onda je IoC container komponenta nešto o čemu bi trebalo da razmišljate kada projektujete arhitekturu aplikacije.

Ukoliko do sada niste koristili DI i IoC preporučujem vam da ih upotrebit u prvom sledećem projektu. Ukoliko framework koji koristite nema DI komponentu predlažem vam da počnete sa Symfony DependencyInjection komponentom.