Aspektexempel: Inversion of Control
Den här artikeln visar på ett alternativt användningsområde för AOP: att ersätta IoC-ramverk som XWork, PicoContainer och Spring.
Först sätter vi scenen. Vi har gjort en enkel app som hanterar en adressbok. Den använder sig i sin tur av externa tjänster för att hämta in vissa uppgifter, t.ex. telefonnummer (Gula sidorna, kanske?). För att inte koppla ihop vår adressbok med de externa tjänsterna för hårt så definieras dessa med gränssnitt som fasader. Det medför dessutom att vi enkelt kan använda test-implementationer av dem när vi testar vår kod, så att vi slipper beroendet att bara kunna köra testerna när vi kommunicerar med de externa tjänsterna.
// Gränssnittet som definierar en extern tjänst som vår app använder sig av
interface PhoneServiceProvider {
String fetchPhoneNumber(String name);
}
// Testklass som implementerar gränssnittet så att man
// kan testa appen utan externa kopplingar
class TestServiceProvider implements PhoneServiceProvider {
public String fetchPhoneNumber(String name) {
return "kalle".equals(name) ? "08-123 45 67" : "0910-123 45";
}
}
// Den här klassen använder sig av PhoneServiceProvider, men
// vet inte (och vill inte veta) varifrån den kommer, eller vilken
// implementation som används!
class AddressBook {
private PhoneServiceProvider service;
public String lookupPhoneNumber(String name) {
return service.fetchPhoneNumber(name);
}
}
// Testklass. (Vi struntar i jUnit etc just nu för att minimera koden.)
public class TestAddressBook {
public static void testPhoneLookup() {
AddressBook book = new AddressBook();
String kallesNummer = book.lookupPhoneNumber("kalle");
assert kallesNummer.equals("08-123 45 67");
}
// Runs all test cases
public static void main(String[] args) {
testPhoneLookup();
}
}
Observera att koden ovan är helt komplett! Stoppa den i var sin java-fil och kör ”TestAddressBook”-klassen och kolla vad som händer! (Eller – varför inte – stoppa allt i samma fil, med namnet TestAddressBook.java, och kompilera den.)
Ok, det kanske inte är så svårt att räkna ut att det kommer att smälla… Vi har ju aldrig berättat för ”AddressBook” att den ska använda sig av TestServiceProvider (eller någon annan PhoneServiceProvider heller), så service-variabeln är fortfarande null.
Det är i det här läget som man numera vanligen blandar in ett IoC-ramverk för att knyta ihop implementationer av gränssnitt med de klasser som vill använda sig av dem. Detta görs typiskt i olika konfigureringsfiler i XML-format. Nu ska vi dock använda en annan lösning!
Det är den här aspekten som gör det magiska:
// Denna aspekt ser till att TestServiceProvider används av klasser
// som behöver en PhoneServiceProvider, men bara när testfallen körs!
aspect UseTestServiceProvider {
PhoneServiceProvider realService = new TestServiceProvider();
pointcut usingProvider():
get(PhoneServiceProvider *.*) &&
!within(UseTestServiceProvider);
pointcut inTestCase():
cflow(call (void TestAddressBook.test*()));
PhoneServiceProvider around():
usingProvider() && inTestCase()
{
return realService;
}
}
Aspekten UseTestServiceProvider plockar (i punktsnittet ”usingProvider”) fram alla tillfällen i appen där en medlemsvariabel av typen PhoneServiceProvider används, dvs där objektet som variabeln refererar till pekas ut, t.ex. vid ett metodanrop (som i vårt fall). När detta händer körs ett around-direktiv som helt enkelt ser till att koden använder sig av den TestServiceProvider som ligger i aspekten (refererad till av variabeln realService).
Ett par saker man bör notera angående lösningen ovan:
- Som standard är aspekter singletons, vilket innebär att även vår instans av TestServiceProvider är det (eftersom den är knuten till aspekten).
- Punktsnittet ”inTestCase()” ser till att TestServiceProvider bara används i kod som körs från en metod vars namn börjar på ”test” och ligger i klassen TestAddressBook. Ett naturligare villkor hade nog t.ex. varit att den skulle gälla för alla klasser som ligger i ett ”test”-paket.
- För alla andra användningar av PhoneServiceProvider (t.ex. i produktionskoden) så gör denna aspekt ingenting. Användandet för dessa fall läggs lämpligen i andra aspekter.
- Medlemsvariabeln ”service” i klassen AddressBook är alltid null. Vi ger den aldrig något värde, men använder den heller aldrig till någonting. Dess enda syfte är att tjäna som ett ”handtag” för oss att kunna flagga var i koden man behöver en PhoneServiceProvider som vi ska stoppa in. Det är alltid motsvarande variabel i aspekten som används istället.