Függőség injekció
Konstantinusz Kft. © 2010
1. Tartalomjegyzék 1. 2. 3. 4. 5.
Tartalomjegyzék ...................................................................................................................... 2 Bevezetés ................................................................................................................................ 3 Függőségek formái .................................................................................................................. 4 Függőség kezelés problémái ................................................................................................... 8 Megvalósítás............................................................................................................................ 9
2/16
2. Bevezetés Egy objektum modellben az objektumok több féle viszonyban lehetnek egymással. Az egyik ilyen viszony a funkcionális függőség, ami jelenti, hogy egyik objektumnak szüksége van a másikra, működési szempontból, vagyis használja azt az objektumot. A problémát az okozza, hogy egyik objektum hogyan tud hozzájutni a másik objektumnak egy példányához, hogy használhassa. Az alkalmazásfejlesztésnek ez egy komoly kihívása, és merőben eltérő alkalmazás szerkezetek léteznek ennek a megoldására. Hogy megérthessük, mi is itt a probléma, nézzük meg milyen lehetséges esetei vannak a függőségeknek, és milyen problémák merülhetnek fel ezzel kapcsolatban.
3/16
3. Függőségek formái A legegyszerűbb eset, ha egy objektum bekér egy másik objektumot paraméterként, amit a hívó fél kell hogy adjon. Ezt teheti a konstruktorában, vagy akármilyen metódusában. Nézzünk egy példát, egy C-szerű fiktív nyelven, ahol van egy Tablazat nevű osztály, aminek a feladata, hogy a konstruktorában megkapott tömbben szereplő adatokból táblázatot jelenítsen meg valamilyen módon.
class Tablazat { Array adatok;
Tablazat(Array adatok){ this.adatok=adatok; } void megjelenit(){ ... }
}
Ez így rendben is van. A hívó fél összeállít egy Array osztályú objektumot, és átadja a egy Tablazat objektumnak (amit akár ő maga példányosít). Hívjuk ezt a szituációt lokális függőségnek, mivel a hívó fél adja át az adatok tömböt a táblázatnak.
4/16
A második függőség éppen a táblázatot használó kód oldaláról keletkezik, a táblázat felé.
class RaktarKeszletOldal{ void megjelenit(){ Array adatok; . . tablazat=new Tablazat(adatok); tablazat.megjelenit(); } } A függőség itt abból adódik, hogy a RaktarKeszletOldal közvetlenül hivatkozik a Tablazat osztályra, mivel példányosítja azt. Ez legyen a konstruált függőség.
Végül pedig jöjjön a legproblémásabb függőség, a globális függőség. Ez esetben egy objektumnak egy olyan másik objektumra van szüksége, aminek van egy kitűntetett példánya valahol, és ezt használja minden más objektum is. Ilyen dolog lehet például egy adatbázis objektum. A probléma itt az lesz, hogy hogyan is jutunk hozzá ehhez a kitűntetett példányhoz.
class Kereso{ void keres(){ Array adatok=adatbazis.query(“SELECT * FROM `keszlet`”); . . } }
5/16
Az egyik lehetőség, hogy van valahol egy példány a globális térben, ami akárhonnan elérhető. A gond ezzel annyi lehet, hogy nem egyértelmű, hogy mikor és hol lesz ez az objektum példányosítva. Amúgy sem jó ötlet a globális névtérbe rakni dolgokat, többek között például a hordozhatóság, és az információ elrejtés elve miatt. A másik lehetőség ami követi az OO filozófiát, az úgynevezett Singleton. A megoldás lényege, hogy az adott osztálytól kérdezzük le a példányt, egy osztálymetóduson keresztül. Ezt a metódust konvenció szerint getInstance()-nek szokás hívni. Ez a metódus pedig szükség szerint példányosíthatja az objektumot.
class Adatbazis { static private Adatbazis peldany;
static void Adatbatis getInstance(){ if (peldany==null) peldany=new Adatbazis(); return peldany; } }
class Kereso{ void keres(){ Adatbazis adatbazis=Adatbazis.getInstance();
Array adatok=adatbazis.query(“SELECT * FROM `keszlet`”); . . } }
6/16
Ezek a módszerek a függőségek kezelésének a hagyományos módjai. Alap esetekben nincs is velük semmi gond, viszont vannak olyan korlátaik, amik miatt komolyabb alkalmazásoknál már nem nyújtanak kielégítő megoldást. Ezek a korlátok főleg a bővíthetőség, módosíthatóság, flexibilitás területén jelentkeznek.
7/16
4. Függőség kezelés problémái A legkomolyabb probléma, hogy ezek a módszerek nem teszik lehetővé a polimorfizmust. Mivel az Adatbazis osztályra közvetlenül hivatkozok, illetve a konstruált függőségek esetén is a Tablazat osztályt is közvetlenül konstruálom, ezért nincs lehetőség arra, hogy a ezeknek az osztályoknak valamilyen alosztálya szerepeljen ott ahol ezeket az osztályokat használom. Azt sem tehetem meg, hogy a Kereso-t úgy használjam, hogy a program futásán belül egyszer egyik, egyszer másik adatbázissal dolgozzon. A második probléma, hogy ezek az osztályok nehezen tesztelhetőek. Tesztelés során ugyanis úgy szeretném kipróbálni egy-egy osztály működését, hogy csak az adott osztály működjön, a függőségei ne. Például nem szeretném azt, hogy egy adatbázist használó objektum tényleg belepiszkáljon az adatbázisba. Ezért jó lenne ha az adatbázis objektumot ki tudnám cserélni valamilyen teszt verzióra, ami nem csinál semmit. Statikus hivatkozás miatt viszont ez problémás. Azt sem tehetem meg, hogy az egész rendszerből kettőt futtassak egymás mellett, egy névtérben, mondjuk más adatbázissal, hiszen mindkét rendszer objektumai ugyanarra a statikus adatbázis példányra hivatkoznának. A függőségek statikus konstruálása pedig nem teszti lehetővé a pluginezhetőséget. Például a táblazat megjelenítő esetében nem tehetem meg, hogy valami új táblázat megjelenítőt feltelepítek, és beállítom hogy azt használja a rendszer, mivel explicit módon meg van adva, hogy a Tablazat osztályt példányosítsa. Ezek miatt az lenne a legjobb, ha globális függőségeket is megkaphatná paraméterként egy adott objektum, mint ahogy a lokális függőségeknél láttuk. Ennek persze semmi akadálya, a probléma viszont ezzel az, hogy ki adja át ezeket a paramétereket? A hívó fél? És a hívó fél honnan kapta? Mi van ha a hívó félnek nem is függősége az az objektum? Ha minden objektum csak paraméterben kaphat meg mindent, akkor minden létező globális objektumot végig kell adogatni az egész hívási láncon a főprogramtól egészen a legalsó objektumig? Szerencsére nem, viszont egészen új módon kell felépítenünk az alkalmazásunk szerkezetét.
8/16
5. Megvalósítás Az első dolog, hogy mindent paraméterben kérünk be. Legtöbbször ez a konstruktorban történik, ahol az ott megkapott függőségeket megjegyezzük attribútumokban.
class Kereso{ Adatbazis adatbazis;
Kereso(Adatbazis adatbazis){ this.adatbazis=adatbazis; }
void keres(){ Array adatok=adatbazis.query(“SELECT * FROM `keszlet`”); . . } }
A gondot az okozza, hogy a Kereso-t példányosító kód hogyan fogja konstruálni azt, anélkül, hogy át kellene neki adni a globális függőségeit. A trükk az lesz, hogy nem fogunk objektumokat közvetlenül példányosítani, hanem erre segéd osztályokat használunk. Az általános elv az, hogy el kell választani az objektumok konstruálását a viselkedési modelltől. Igazából egy objektumnak nem felelőssége az, hogy tudja konstruálni egy függőségét. Helyette factoryk fogják végezni ezt a feladatot. A factory egy olyan objektum, aminek az a feladata, hogy más objektumot vagy objektumokat példányosítson, és ami esetünkben lényegesebb, hogy “összekösse” őket a megfelelő függőségeikkel. A most következő példában ezt láthatjuk, viszont egyelőre itt még kihagyjuk azt hogyan a factoryhoz hogyan jutunk hozzá, és hogy a factory hogyan jut hozzá az adatbázishoz.
9/16
class KeresoFactory{
Kereso ujPeldany(){ Adatbazis adatbazis; . . return new Kereso(adatbazis); } }
class RaktarKeszletOldal{
void megjelenit(){ KeresoFactory keresofactory; . . kereso=keresofactory.ujPeldany();
Array adatok=kereso.keres(); . . } }
10/16
Láthatjuk, hogy itt a RaktarKeszletOldal nem példányosítja a Kereso-t közvetlenül, hanem a KeresoFactory-t kéri meg erre. Ezzel azt is elérjük, hogy a RaktarKeszletOldal-nek nem kell kezelnie a Kereso globális függőségeit, sőt nem is tudja hogy milyen globális függőségei vannak. Tehát a két kérdés ami nyitva maradt, hogy hogyan jutunk hozzá a factoryhoz, és a factory hogyan jut hozzá a az adatbázishoz? Ha tökéletes megoldást akarunk, ami megfelel az összes fentebb meghatározott igényünknek, akkor teljesen komolyan kell venni azt az elvet, hogy semmit de semmit nem szabad statikus módon elérni, mindent meg kell kapnia az objektumoknak közvetlenül. Ezen túl, két újabb segédobjektumra van szükségünk. Egyrészt, kell egy factory menedzser, amiből lekérhetjük a kívánt factoryt. Továbbá kell valami ami a globális objektumainkat tárolja, és majd a factoryk ezen keresztül jutnak hozzájuk. Ez a kettő lehet akár ugyanaz az objektum is, vagy akár a tároló lekérdezhető a factory menedzseren keresztül is. Végül pedig, ahhoz hogy tényleg semmit ne kelljen statikusan elérnünk, muszáj lesz a factory menedzsert végig egyik objektumról a másikra passzolgatni a teljes hívási láncon. Persze ez csak egyetlen objektum, és szerencsére ezt is a factoryk fogják átadogatni, tehát a valódi objektumoknak ezzel sem kell foglalkozniuk.
Ezek után valahogy így fog kinézni az előbbi kódunk:
abstract class Factory{ FactoryMenedzser factorymenedzser;
Factory(FactoryMenedzser factorymenedzser){ this.factorymenedzser=factorymenedzser; } }
class FactoryMenedzser{ Array globalis_objektumok;
Object getSingleton(string nev){
11/16
return globalis_objektumok[nev]; }
Factory getFactoryOsztalyhoz(string osztalynev){ /*osztálynév alapján példányosítás nyelvfüggő, ha ezt nem tudja a nyelv, máshogy kell */ return new osztalynev(this); }
}
class KeresoFactory{
Kereso ujPeldany(){ //A factorymenedzsertől elkérjük az globális adatbázis objektumot Adatbazis adatbazis=factorymenedzser.getSingleton(“Adatbazis”); . return new Kereso(adatbazis); } }
class RaktarKeszletOldal{ FactoryMenedzser factorymenedzser;
12/16
void megjelenit(){ //Lekérdezzük a factory-t; KeresoFactory keresofactory=factorymenedzser.getFactoryOsztalyhoz(“Kereso”); . . //A factorytól kérunk egy új példányt. kereso=keresofactory.ujPeldany();
Array adatok=kereso.keres();
//Lekérdezzük a táblazat factory-ját is, és példányt kérünk tőle. Tablazatfactory tablazatfactory=factorymenedzser.getFactoryOsztalyhoz(“Tablazat”); tablazat=tablazatfactory.ujPeldany(adatok);
tablazat.megjelenit(); . . }
}
Tehát innentől úgy épül fel az alkalmazásom, hogy a főprogramban példányosítom a globális objektumokat, a factory menedzsert, a globális tárolót, és a globális objektumokat elhelyezem benne. Ezután már a factory menedzseren keresztül példányosítom az első objektumokat.
13/16
void main(){ Adatbazis adatbazis=new Adatbazis(); FactoryMenedzser factorymenedzser=new FactoryMenedzser();
factorymenedzser.singletonHozzaad(“Adatbazis”,adatbazis);
factory=factorymenedzser.getFactoryOsztalyhoz(“RaktarKeszletOldal”);
RaktarKeszletOldal raktarkeszletoldal=factory.ujPeldany();
raktarkeszletoldal.megjelenit(); }
Ezzel a módszerrel megvalósul a polimorfizmus, mivel itt a főprogramban döntöm el, hogy milyen adatbázis objektumot példányosítok, és én adom át a rendszernek közvetlenül. Megvalósul a tesztelhetőség is, mivel minden osztály minden függőséget kívülről kap meg paraméterként, ezért minden osztály tesztelhető elkülönítve, csak be kell helyettesíteni a függőségeket teszt objektumokkal. Ezt még úgy is meg lehet tenni, ha egy névtérben vagyunk az élő rendszerrel. Az is megoldható, hogy akár két példányt futtassak a teljes rendszerből, csak annyit kell tennem hogy a main() metódusban látott objektumokból többet példányosítok. Még azzal is eljátszhatok, hogy bizonyos globális objektumok közösek legyenek, mások meg egyediek. Minden csak attól függ, hogy mivel töltöm fel a globális objektum tárolót. Még az objektumok konstruálását is lehet polimorfizálni, például ha azt szeretném, hogy a RaktarKeszletOldal tetszőleges osztályú táblázat objektumot példányosítson. Ezt meg lehet tenni úgy is, hogy olyan factory menedzsert hozok létre, ami a “Tablazat” osztályhoz olyan factoryt ad vissza, ami igazából annak egy alosztályát konstruálja, bár ezzel igazából becsapjuk a rendszert, szóval nem ajánlott, de meg lehet tenni, és jól mutatja mennyire laza a rendszer. A Jó megoldás az, hogy a RaktarKeszletOldal osztály valahonnan paraméterként kapja, vagy valami konfigurációs
14/16
állományból kiolvassa, hogy milyen osztályú táblázatot kell példányosítania, és akkor olyan osztályra kért factoryt a factory menedzsertől. Mindezt megtehetjük, hiszen az osztály nevét csak sima szövegként kezeljük.
class RaktarKeszletOldal { string tablazat_osztalynev;
//A konstruktorban kapjuk a táblazat osztálynevét. RaktarKeszletOldal(string tablazat_osztalynev){ this.tablazat_osztalynev=tablazat_osztalynev; }
void megjelenit(){ Tablazatfactory tablazatfactory= factorymenedzser.getFactoryOsztalyhoz(tablazat_osztalynev);
tablazat=tablazatfactory.ujPeldany(adatok);
tablazat.megjelenit(); } }
Még attól sem kell félnünk, hogy a legyártott objektumnak egyedi függőségei vannak, hiszen azokat mindet a megfelelő factory szerzi be és adja át az objektumnak, anélkül hogy a hívó fél bármit is tudna róla.
15/16
Az egyetlen szépséghiba, hogy itt muszáj vagyunk factory menedzsert minden objektumba beinjektálni. Ezt éppen meg lehet úszni azzal, ha a factory menedzsert egy hagyományos, statikusan elérhető singletonként valósítjuk meg, így mindenhonnan globálisan elérhető. Ezzel viszont elveszítünk néhány jó tulajdonságot. Nem fogunk tudni két külön rendszert egymás mellett futtatni, mivel mindkettő ugyanarra a globális tárolóra fog hivatkozni. Tesztelhetőség is sérül, mivel minden osztály statikusan hivatkozik a factory menedzserre, így nem tudom az általa létrehozott objektumok függőségit olyan nagy szabadsággal befolyásolni. Mindenesetre a statikus factory menedzser is egy jó megoldás és messze jobb flexibilitást ad, mintha minden globális objektum statikus singleton lenne. De ha a mindent tudó megoldást szeretnénk, akkor muszáj lesz legalább a factory menedzsert végigpasszolgatni az objektumainkon, viszont legalább ezt a factoryk intézik, mint minden más függőséget is, szóval nem nagy ár ez azért amit kapunk cserébe. Általában a legtöbb alkalmazás elműködik komoly függőség injekció nélkül, tisztán singletonokkal. Ám komolyabb alkalmazásoknál, ahol fontos a jó bővíthetőség, már megjelennek az ehhez hasonló függőség injekciós eszközök és megvalósítások. Ha más nem is, a tesztelhetőség igénye miatt már muszáj ebbe az irányba elmenni. Igazán nagy rendszereknél pedig, ahol már csak méretük és bonyolultságuk miatt is létfontosságú a bővíthetőség, flexibilitás, tesztelhetőség, egyenesen kötelező a függőség injekció, és factoryk alkalmazása.
16/16