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).
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.
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();
...
}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.
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.
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 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.
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 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();
}
}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:getXxx()) za svaki seter (setXxx()) (tj. kada je JavaBean read-write)
moguće je pročitati njegovo celokupno stanje, tj. konfiguracijuinit() metod, koji radi konkretno setovanje konfiguracije ili stanja objekta.init() metodom.Pravi odgovor bi, dakle, mogao biti da treba koristiti i jedno i drugo rešenje, zavisno od slučaja.
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.
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.