Dependency Injection (DI)
Detta inlägg ingår i serien Spring från början och kommer att förklara designmönstren Inversion of Control (IoC) och Dependency Injection (DI).
Inversion of Control
Inversion of Control (IoC) är egentligen ett samlingsnamn på en mängd mönster. Dessa mönster uppträder ofta i ramverk. Huvudpoängen är att det inte är den egna applikationskoden som styr programflödet utan att detta sköts av ramverket. IoC är också lite informellt känt som ”Hollywood Principle” – ”don’t call us, We’ll call you.”
Traditionellt i imperativa programmeringsspråk så har applikationskoden explicit styrt programflödet. Programflödet defineras av att applikationskoden styr hela flödet som antingen är egen kod eller anrop till bibliotekskod. Bibliotekskoden kan anropa annan bibliotekskod, men typiskt aldrig den egna koden. Inversion of Control uppstår alltså när bibliotekskoden anropar den egna koden.
Exempel på IoC
-
Händelsestyrd programmering (event-driven programming). En *main-loop* definerar ett tydligt flöde i vilket den egna applikationskoden kan kopplas in. (exempelvis Swing)
-
JUnit. När du skriver ett testfall enligt JUnit 3.8+ så skapar du en testklass som ärver ramverksklassen `TestCase`. Du utvecklar en eller flera testmetoder `testSomething` och `testSomethingElse`. Ramverket tillhandahåller `setUp`– och `tearDown`-metoder där du kan lägga in initiering och uppstädningskod. Men det är inte du som kör testet utan detta gör ramverket.
IoC och beroenden
Definitionen på IoC ovan är betydligt bredare än vad man nu för tiden menar när man använder begreppet IoC. Vanligtvis talar man om att externt, via en container, hantera skapandet av komponenter och dess beroenden.
IoC kan grovt delas in i två kategorier nämligen Dependency Lookup och Dependency Injection (DI) vilka båda beskrivs nedan.
Traditionellt sett har skapande av komponenter och dess beroenden skett i de faktiska komponenterna såsom i detta exempel där klassen Foo
skapar en instans av typen Bar
och dessutom exekverar det logiska programflödet via metoden executeSomething
.
Figur 1: Foo på klassiskt sätt
public class Foo {
public void doSomething() {
Bar bar = new BarImpl();
bar.executeSomething();
}
}
I Figur 1 visas hur en instans av typ BarImpl
skapas och därigenom så har klassen Foo
ett hårt beroende till en viss typ av implementation av Bar
-objekt.
Syftet med att använda IoC för att hantera skapandet av komponenter är att:
- Minimera ”glue code” dvs kod som endast finns för att ”limma ihop” applikationen.
- Skapa komponenter på ett enhetligt och kontrollerat sätt.
- Hantera beroenden externt så att de inte behöver vara kända av den faktiska javakoden.
- Förbättra testbarhet. Genom att skapandet av objekt hanteras externt så kan implementationer av olika objekt enkelt bytas ut vid testning med hjälp av mock objects.
- Tvinga fram en bra applikationsdesign.
Dependency Lookup
Dependency lookup är en form av IoC som har funnits länge och använts på många olika sätt. En fabrik eller ett register (factory/registry) erbjuder en applikation att koppla ihop en mängd objekt/komponenter utan att exponera för mycket om hur dessa objekt sitter ihop eller vilka beroenden de ingående objekten har. I stället för att sprida denna ”create kod” i hela applikationen så kan skapandet modulariseras. Klienten anropar helt enkelt en metod på en fabrik för att få ett färdigihopkopplat och konfigurerat objekt. Klientkoden har endast beroende till fabriken och det objekt som skapas.
Det finns nackdelar med att använda fabriker varav en är att man måste skriva sin fabrikskod. Det kan tyckas trivialt men det blir många klasser som skrivs i onödan och det finns alltid risk för fel. Om vi tittar på ett typiskt användande av en fabrik.
Figur 2: Användning av fabrik
Bar ins = (Bar) MyObjectFactory.getInstance("myBar");
Värt att notera är att vi har en hård referens till fabriken vilket är lite tråkigt. Att mocka fabriken är svårt då vi använder en statisk metod. Att byta implementerande klass i sina testfall kan vara olika svårt beroende på hur man implementerat fabriken. Har vi gjort fabriken lite smart så att den tex väljer vilken klass den ska instansiera utifrån en XML-fil kan man byta implementation utan en egen fabriksimplementation för test.
I detta fall måste vi aktivt välja i koden vilken instans (”myBar”) vi vill ha (och stava rätt), kanske kontrollera att den inte är null och att akta oss för eventuella ClassCastExceptions
. Även komplexiteten i detta inte är så stor vore det mycket bättre om vi slapp den för att undvika ev fel och boiler plate kod. Den som har gjort JNDI-uppslag kan notera att mönstret är detsamma, båda är exempel på Dependency Pull, en variant av Dependency Lookup.
Ett annat typfall på hur man gjort innan DI är användande av fabriksmetoder som i Singleton pattern (även känt under synonymet Evil singleton pattern). Ett av huvudproblemen i användningen av detta mönster är den dåliga testbarheten pga att statiska metoder används som är svåra att byta ut under test.
Dependency Injection
Dependency injection (DI) är den andra formen av IoC gällande att skapa och hantera komponenter. Med DI är det ramverket som styr algoritmen för hur beroenden mellan ingående delar löses upp.
Med DI skriver man mindre kod och det finns nästan inga hårda beroenden till ramverket. När det gäller Spring kan man förenklat säga att ramverket redan tillhandahåller all den fabriks- och lookup-kod du behöver.
DI-containern (exempelvis Spring) kommer att vilja injicera instanser antingen i en konstruktor eller med getter/setter metoder till din klass vilket betyder att den lilla kod man skriver är mycket enkel och kan automatgenereras av en bra IDE. Med en DI-container kan man till skillnad från i Dependency Lookup fallen ovan utveckla/testa utan beroenden till en extern container. Nedanstående exempel visar hur klassen Foo
exponerar en setter-metod som kommer att anropas av DI-containern där Bar
-objektet sätts. Observera att klassen Foo
nu inte har någon kunskap om hur man skapar Bar
-objekt och den hårda kopplingen till BarImpl
är borta. Detta hanteras nu istället av DI-containern.
Figur 3: Foo med setter-metod
public class Foo {
private Bar bar;
// setter method invoked by the DI-container
public void setBar(Bar bar) {
this.bar = bar;
}
public void doSomething() {
bar.executeSomething();
}
}
Nästa del i denna artikelserie visar exempel på hur man kan använda Spring som DI-container.
Nedanstående exempel visar hur enkelt det är att testa klassen Foo
med en enkel testvariant av Bar
-interfacet. På rad 5 används en form a DI direkt i koden. Om ett fabriksmönster eller direkt instansiering hade använts av klassen Foo
hade det blivit betydligt mer komplicerat att mocka objektet Bar
.
Figur 4: JUnit-test och DI
public class FooTest extends TestCase {
public void testSomething() {
Foo foo = new Foo();
TestBar testBar = new TestBar();
foo.setBar(testBar);
foo.doSomething();
assertTrue(testBar.count > 0);
}
public static class TestBar implements Bar {
int count = 0;
public void executeSomething() {
count++;
}
}
}
Dependency Lookup eller Dependency Injection
Så vad ska man då välja, Dependency Lookup eller DI? Om man använder Dependency Lookup så finns det hårda beroenden till det ramverk som man använder eller de fabriker som måste skapas. Det blir större mängd ”glue code” och koden blir inte lika enkel att testa. Därför bör DI användas om möjligt.
Typiska exempel på vad en DI-container hanterar åt dig är:
- Hur vi får tag i referenser till andra objekt eller komponenter
- Åtkomst till systemresurser som exempelvis filsystem
- Hur ett objekt får sin konfiguration eller sina initieringsparametrar
- Livscykelhantering, dvs när och vem som ”startar” och ”stoppar” ett objekt
- Eftersom DI-containern skapar objekt kan proxies, aspekter, fasader och dylikt konfigureras dynamiskt. I denna serie kommer vi senare att blogga om aspekter i artikeln Introduktion till Spring AOP.
Allt detta är saker du inte vill och inte behöver hantera i din applikationskod!
Källor
- Martin Fowler om Inversion of Control – http://martinfowler.com/articles/injection.html
Av: Per Rasmussen, Johan Risén och Tommy Wassgren.