JavaSvet - otvorena java zajednica

 
glavna stranica arr2javasvet  english version arr2java.net

Inversion of Control (IoC)

Igor Spasić
26 Avg 2004

Definicija

Inversion of Control (IoC) je generalni princip kojim se otklanja međusobna zavisnost komponenti na nivou implementacije, čime se ostvaruje tkzv. izolacija komponenti.

IoC princip je poznat i po sledećim sinonimima: Dependency Inversion (DIP), Hollywood principle (don't call us, we'll call you).

Objašnjenje

IoC princip je prvi put predstavljen je u projektu Avalon (Stefano Mazzocchi, 1998), mada postoje reference na neke ranije definicije (Michael Mattson, 1996). Nedavno je ovaj princip naglo dobio na popularnosti, zbog pojave raznih programerskih okvira (framework) i kontejnera koji se koriste ovim principom. Od tada se ovaj princip često pogrešno naziva i paternom, ali i indetifikuje sa kontejnerima i okvirima (framework). Zato treba napomenuti da se ovaj članak bavi isključivo samim IoC principom.

Kao što je rečeno u definiciji, IoC ostvaruje izolaciju komponenti, tj. razdvajanje na nivou implementacije. Time entiteti modela zaista postaju "komponente": celine koje su konfigrabilne, razdvojene i nezavisne jedna od druge.

IoC princip nije težak za realizaciju. Međutim, podjednako je važno razumeti zašto i kada se on može primeniti. U tu svrhu poslužiće jednostavan primer koji će biti modelovan na više načina, bez i sa primenom IoC principa. Napomena: modele koji slede treba posmatrati samo kao primer, a ne kao deo nekog realnog sistema, pošto bi takav sistem zahtevao nešto složeniju strukturu.

Primer

Model #1

U pitanju je jednostavan model koji se može predstaviti sledećim UML dijagramom:

Postoje 2 entiteta: FooService i SearchEngine. Korisnik ovog sistema koristi FooService radi pretrage. FooService sam po sebi 'ne zna' da radi pretragu, već interno koristi rezultate dobijene od jednog konkretnog pretraživača (SearchEngine). Digresija: radi jednostavnosti primera, ovde FooService samo delegira zahtev za pretragom do SearchEngine. U realnosti bi FooService.doSearch() bio složeniji i mogao bi, na primer, da dodatno obrađuje/filtrira rezultate pretrage dobijene od više entiteta za pretragu, a ne od samo jednog.

Ovako modelovani entiteti su čvrsto povezani i direktno zavisi jedan od drugog. Strogo govoreći, ovi entiteti nisu komponente: nisu konfigurabilni, a njihova zavisnost je hard-kodovana. Ovakav program nema mogućnost jednostavnog proširenja bez izmena baznih klasa, ili mogućnost zamene SearchEngine koji se koristi za pretragu. Razlog tome je to što FooService u samom sebi instancira i čuva SearchEngine:

public class FooService {
   
...
   
private SearchEngine searchEngine = new SearchEngine();
    ...
}

Model #2

Prethodni model se može izmeniti uvođenjem SearchManager. Ovaj manager može da služi da registruje i čuva u sebi reference na više različitih entiteta za pretragu, koji se na mestu upotrebe dobijaju na osnovu jedinstvenog ključa, a ne direktno instanciranjem. Time je instanciranje entiteta za pretragu 'izvučeno' iz prvobitnog entiteta u SearchManager:

Međutim, ovim nije dobijeno mnogo. Nedostatak je što manager vraća konkretan objekat, a ne interfejs. Drugi nedostatak, koji isto doprinosi postojanju zavisnosti između entiteta, je to što je manager hard-kodovan u FooService, te nije moguće upravljati njime spolja:

public class FooService {
   
...
   
private SearchManager manager = new SearchManager();
    ...
}

Pošto je ovde SearchManager 'zarobljen' u FooService, kontrola implementacija managera (koje to zahtevaju) bi se odvijala preko FooService.

Model #3

Treći model primera je neznatno bolji: ovde se instanca SearchEngine prosleđuje servisnoj metodi FooService koja radi pretragu:

Osim što i dalje ne postoji interfejs za SearchEngine, čini se da je ovim razbijena čvrsta zavisnost ove dve komponente. Međutim, problem ovde je i to što bi se svakoj servisnoj metodi morala prosleđivati odgovarajuća instanca entiteta za pretragu koju koristi. Ne samo da to dovodi do nepotrebnog povećanja koda koga treba pisati, već se to može odraziti i na dizajn modela: potrebno je u programu voditi računa i o dostupnosti entiteta za pretragu, da bi ih mogli upotrebiti na mestu poziva servisne metode.

public class FooService {
   
public void doSearch(SearchEngine searchEngine) {
       
...
   
}
}

Ovim se najčešće razbija enkapsulacija koju FooService treba da ostvari: na mestu poziva servisne metode mora da bude dostupna i konkretna komponenta za pretragu.

Primena IoC na primer

IoC, kao što to nagoveštava sam naziv, pristupa problemu inverzno, polazeći od toga šta se želi postići: nezavisne komponente na nivou implementacije. U startu se, dakle, predpostavlja da već postoje 2 komponente koje su nezavisne. Pošto komponente rade jedna sa drugom, jasno je da nekakava zavisnost mora da postoji. Zavisnost može da bude 'jaka', na implementacionom nivou, kao što je to prikazano u prethodnim modelima. Međutim, zavisnost je moguće ostvariti i na nivou interfejsa. Takva zavisnost je 'laka', i čini da komponente ostanu međusobno nezavisne.

Komponente dizajnirane prema IoC principu ne dobavljaju same druge komponente koje su im potrebne. Umesto toga, ove zavisnosti se samo deklarišu, a konkretno se ostvaruju od spolja. Upravo je to inverzija kontrole, jer nije komponenta ta koja ostvaruje zavisnost, već se to ostvaruje spolja.

Zavisnost na nivou interfejsa je moguće ostvariti na više načina. Vremenom su uočena sledeća moguća rešenja, koji se danas nazivaju tipovima IoC (nazivi su dati na engleskom jer ih nema smisla posebno prevoditi):

U zagradama su dati nekadašnji nazivi za koje se danas smatra da su zastareli. Uočavaju se dve grupe rešenja. Prva grupa, Dependency Injection, zasniva rešenja na tome da se zavisnost spolja ubacuje (injektuje) u postojeće entitete. Sami entiteti ne utiču na kreiranje instanci drugih, a ono što se, u stvari, injektuje su reference na postojeće instance entiteta. Druga grupa, Dependency Lookup, zasniva rešenja na tome da se zavisne komponente dobavljaju traženjem (lookup) na mestu gde je potrebno njihovo korišćenje.

Ovako opisana IoC rešenja deluju prilično apstraktno, iako su u suštini jednostavna. Zato je najbolje pogledati kako se gornji primer modelira uz pomoć IoC principa. Ovde će biti dat opis tri danas najčešće korišćenih rešenja: Contextualized Dependency Lookup (Type 1), Setter Dependency Injection (Type 2) i Constructor Dependency Injection (Type 3).

Contextualized Dependency Lookup (Type 1)

Contextualized Dependency Lookup IoC tip polazi od gore opisanog modela #2. Zavisnost se ostvaruje očitavanjem i traženjem (lookup) željene komponente iz nekog za to predviđenog entiteta (manager, kontejner). Polazeći od gornjeg modela #2, SearchManager se izdvaja iz FooServices, a servisnim metodama se sada prosleđuje referenca na manager. Manager služi za dobavljanje reference na odgovarajuću instancu komponente koja se zahteva. Sam FooService implementira interfejs SearchService, čime se dodatno razdvajaju manager i konkretni SearchEngine.

SearchService 'zna' samo za SearchManager, a FooService je taj koji zna za i koristi konkretne entitete za pretragu koji su registrovani u manageru:

public class FooService implements SearchService {
   
public void doSearch(SearchManager manager) {
       
SearchEngine searchEngine = manager.fetch("SuperDuperSearcher");
        searchEngine.search
();
   
}
}

Ipak, ovo nije sasvim korektan pristup. Jedan od razloga je to što se odluka koji se SearchEngine dobavlja i dalje nalazi u FooService, i na to se ne može uticati spolja. Ovaj problem se može rešiti tako što se manageru ne navodi šta se traži ("SuperDuperSearcher"), već ko traži ("FooService.doSearch"), ali to opet može da ima svojih problema.

Contextualized Dependency Lookup se, na primer, koristi i prilikom standardne realizacije EJB (bez korišćenja drugih frameworka): pomoću JNDI se dobavljaju drugi EJB-ovi i resursi. Realizacija je poznata: u konstruktoru EJB-a (radi keširanja) se pomoću InitialContext.lookup() dobijaju svi potrebni EJB-ovi.

Contextualized Dependency Lookup je složeniji od ostalih IoC izvedbi, što sa sobom uglavnom može da donese i druge probleme. Zato danas veća pažnja poklanja IoC rešenjima koji se baziraju na injektovanju zavisnosti, a čiji opis sledi.

Setter Dependency Injection (Type 2)

Jednostavnije rešenje je Setter Dependency Injection. Ideja je da se reference na potrebne komponente ubacuju kroz settere.

FooService je proširen setterom kojim se setuje koji se SearchEngine koristi. Ovde se SearchEngine konačno definiše kao interfejs.

public class FooService {
   
private SearchEngine searchEngine;
   
public void setSearchEngine(SearchEngine engine) {
       
this.searchEngine = engine;
   
}
   
public void doSearch() {
       
searchEngine.search();
   
}
}

Constructor Dependency Injection (Type 3)

Constructor Dependency Injection je sličan Setter Injection rešenju. Umesto da se zavisnost setuje kroz setter metode, u ovom rešenju se to radi kroz konstruktor:

Sve ostalo je isto kao i kod Setter Injectiona.

public class FooService {
   
private SearchEngine searchEngine;
   
public FooService(SearchEngine engine) {
       
this.searchEngine = engine;
   
}
   
public void doSearch() {
       
searchEngine.search();
   
}
}

Koji tip injektovanja izabrati?

Ovo pitanje nema pravog odgovora, jer oba rešenja u osnovi predstavljaju isti koncept. Evo nekih prenosti i mana oba ova IoC rešenja (prema Rodu Johnsonu).

Prednosti Setter Dependency Injection: Mane Setter Dependency Injection: Prednosti Constructor Dependency Injection: Mane Constructor Dependency Injection:

Pravi odgovor bi, dakle, mogao biti da treba koristiti i jedno i drugo rešenje, zavisno od slučaja.

Programiranje interfejsa

U svim prethodnim primerima primene IoC principa je donekle iskorišćeno izdvajanje interfejsa. Reč je o takođe jednom principu modelovanja, koji se naziva 'programiranje interfejsa'. Ako bi se dosledno poštovao ovaj princip, sve komponente iz prethodnih primera bi trebalo da budu modelovane sa izdvojenim interfejsima. To znači da bi SearchEngine i SearchService bili interfejsi, koje bi implementairale konkretne realizacije: SuperDuperSearch i FooService, respektivno.

IoC i kontejneri

Danas postoje programerski okviri (framework) koja koriste IoC princip i koja olakšavaju izradu aplikacija. U tom svetlu, Inversion of Control (IoC) princip doprinosi tome da se kontrola komponenti (pozivanje, životni ciklus, međuzavisnost, konfigurisanje) izvršava od strane kontejnera ili frameworka, a ne od strane same komponente. Komponente međusobno komuniciraju i pristupaju jedna drugoj samo na nivou interfejsa (ili 'servisa'), a ne na nivou implementacije. Time se ostvaruje izolacija komponenti.

Resursi

Celokupan sors svih primera
The Dependency Inversion Principle (autor: Robert Martin)
Inversion of Control Containers and the Dependency Injection pattern (autor: Martin Fowler)

Spring Framework
Pico Container