Det är ingen överdrift att säga att objektorienterad systemutveckling är det helt dominerande synsättet för programkonstruktion, och det har det också varit de senaste åren. Men utvecklingen har inte stått stilla för det. Ett exempel på nya hjälpmedel i OO-verktygslådan är designmönster vilka syftar till att ge utvecklaren en katalog med generella lösningar på vanligt återkommande designproblem.

Ett annat sådant hjälpmedel, som ännu inte fått lika mycket uppmärksamhet som designmönster, men som i min åsikt har ännu större potential, är aspektorienterad systemutveckling (Aspect-Oriented Programming).

Denna artikel är även publicerad i Datormagazin nr 8 2002. Här finns exempelkoden som ZIP-fil.

Innehåll

1. Inledning
2. Problemområden
3. Aspekter
4. En enkel men komplett aspekt i AspectJ
5. Användningsområden för AOP och AspectJ
6. Något om verktygsstöd
7. Slutord
8. Länkar till externa AOP-resurser

1. Inledning

Det är ingen överdrift att säga att objektorienterad systemutveckling är det helt dominerande synsättet för programkonstruktion, och det har det också varit de senaste åren. Men utvecklingen har inte stått stilla för det. Ett exempel på nya hjälpmedel i OO-verktygslådan är designmönster vilka syftar till att ge utvecklaren en katalog med generella lösningar på vanligt återkommande designproblem.

Ett annat sådant hjälpmedel, som ännu inte fått lika mycket uppmärksamhet som designmönster, men som i min åsikt har ännu större potential, är aspektorienterad systemutveckling (Aspect-Oriented Programming). I den här artikeln beskriver jag vad AOP är och vad det kan komma att betyda för dig som systemutvecklare. Jag visar också på några exempel som använder sig av AspectJ – en aspektorienterad utvidgning av programspråket Java. Länkar till webbresurser för AOP och AspectJ är listade i slutet av artikeln. Läsaren förväntas ha grundläggande kunskaper om objektorientering och, i avsnitten som behandlar AspectJ, Java. Ibland refereras till gratistjänsten Fixafest.nu som exempelapplikation, men du behöver inte känna till något om den för att kunna tillgodogöra dig innehållet i artikeln.

1.1. Terminologi

Eftersom AOP är ett så nytt område finns det (mig veterligen) ännu ingen etablerad svensk terminologi. Jag har försökt använda mig av svenska termer som speglar syftet med de engelska termerna, istället för att försöka mig på direktöversättningar som kan bli konstiga. Jag hoppas kunna uppdatera den här artikeln när en etablerad svensk terminologi dyker upp. När jag introducerar nya begrepp anger jag den engelska termen i parentes efteråt.

2. Problemområden

AOP syftar till att hjälpa utvecklaren med uppdelning av problemområden (eng: separation of concerns). Vad menar vi då med detta?

När du designar ett (objektorienterat) system delar du upp problemet och lösningen i moduler/klasser som motsvarar lösningar av delproblem och samspelar på ett sätt som tillsammans uppfyller de krav som ställs på systemet. I en applikation som fixafest.nu återfinns, inte helt överraskande, klasser som till exempel User, Party, Invitation osv. Dessa är naturliga beståndsdelar av ett system som hanterar inbjudningar till fester och är förstås centrala för designen av systemet. Vi kan kalla dessa klasser och den del av lösningen som de representerar, för huvudproblemområdet (eng: core concern).

Ett komplett system som fixafest.nu måste dock uppfylla andra mer eller mindre implicita krav, som exempelvis:

  • Åtkomstkontroll, så att identifierade användare bara har åtkomst till sina egna fester, adressböcker, etc
  • Loggning – i olika nivåer under olika faser av projektet
  • Cachning, för att öka prestandan
  • Datalagring, som i fallet fixafest.nu innebär kommunikation via JDBC med relationsdatabasen MySQL.

Sådana typer av systemkrav är svårare att helt placera i egna moduler. Om vi tar loggning som exempel så finns det förstås klasser som hjälper till att utföra loggningen (till exempel Javas nya standardramverk för loggning) men kod som använder sig av dessa klasser finns utspridd över i stort sett hela systemet. Den här typen av problemområden kallar vi för tvärsnittsproblemområden (eng: cross-cutting concerns).

Hanteringen av tvärsnittsproblemområden i ett system kan få flera oönskade bieffekter:

  • Metoder i en klass kan ofta inte fokusera på ett problemområde, utan måste hantera flera olika problemområden, till exempel affärslogik, transaktionshantering och loggning
  • Eftersom flera problemområden blandas får också utvecklaren svårare att fokusera på en typ av problem i taget, vilket kan leda till lägre kvalitet
  • Hopblandningen av problemområden ökar också kopplingen mellan moduler och komponenter som egentligen inte är beroende av varandra. Det kan till exempel vara såväl tidsödande som tålamodsprövande att byta ut ett loggningsramverk mot ett annat i ett helt system, eftersom beroende till loggningskomponenterna kan finnas i så gott som alla klasser i systemet.

3. Aspekter

Med hjälp av verktyg som stödjer AOP (exempelvis AspectJ) kan vi designa också tvärsnittsproblemområden på ett rent modulerat sätt. Olika problemområden definieras som aspekter (eng: aspects) av systemet. Aspekter är också den modulariseringsenhet som vi arbetar med i AOP (precis som att klasser är den vanligaste modulariseringsenheten i objektorienterad systemutveckling).

En aspekt i AOP har två typer av beståndsdelar, punktsnitt (eng: point cuts) och direktiv (eng: advice).

3.1. Kodpunkter och punktsnitt

En implementation av AOP för ett speciellt programspråk definierar en mängd olika kodpunkter (eng: join points) för det programspråket. En kodpunkt är en specifik punkt i ett programs exekvering. Exempel på kodpunkter kan vara:

  • ett anrop av en metod
  • skapande av ett objekt
  • värdet av ett attribut sätts

För att sedan specificera exakt vilka kodpunkter som berör en viss aspekt används ett eller flera punktsnitt. Ett punktsnitt kan ses som ett ”mönster”, som kan passa in på en eller flera kodpunkter i ett system. Exempel på punktsnitt kan då vara:

  • anrop av publika metoder som har ett namn som börjar på ”findBy” och tar en textsträng som parameter
  • skapande av ett objekt som hör till klassen ”Invitation”
  • värdet förändras hos något av alla attribut som är av typen ”Connection” i alla objekt oavsett vilken klass objektet tillhör

3.2. Punktsnitt i AspectJ

För att bli lite mer konkreta så kan vi titta lite på hur en (ofullständig) aspekt med några punktsnitt ser ut i AspectJ-kod:

aspect IncompleteAspect {
    // anrop av publika metoder som har ett namn som börjar på
    // "findBy" och tar en textsträng som parameter
 
    pointcut findCall(): call(public * *.findBy*(String));
 
    // skapande av ett objekt som hör till klassen "Invitation"
    pointcut constructInvitation(): initialization(* Invitation(..));
 
    // värdet förändras hos något av alla attribut som är av typen "Connection"
 
    // i alla objekt oavsett vilken klass objektet tillhör
    pointcut connectionChanged(): set(* Connection *.*);
}

Exemplet ovan visar tre namngivna punktsnitt. Kom ihåg att punktsnitt kan betraktas som mönster som passar in på olika punkter i ett exekverande program (kodpunkter). AspectJ innehåller element för att skapa ett femtontal olika typer av punktsnitt. Dessa kan dessutom kombineras för att bygga mer avancerade punktsnitt för olika ändamål.

Ett punktsnitt i en aspekt markeras alltså med nyckelordet pointcut, följt av ett namn på punktsnittet som definieras. (Man kan även använda anonyma punktsnitt, men det kommer jag inte att behandla i den här artikeln.) Sedan kommer ett nyckelord som anger vilken typ av punktsnitt vi vill definiera. Här är några exempel:

  • call – anrop till en metod som specificeras av en signatur
  • set/get – värdet av ett attribut sätts eller läses av
  • within – anger ett punktsnitt som passar på alla kodpunkter i en angiven metod eller definierade i en angiven klass

Ofta ska en typ eller en signatur specificeras i punktsnittet. Det görs med hjälp av ett mönster som kan innehålla jokertecken. Mönstret ”* * .(..)” betyder ”alla metoder i alla klasser”. Det första jokertecknet anger vilket skydd metoden har (public/protected etc), det andra anger metodens returtyp, det tredje vilken klass som metoden är definierad i, och det fjärde anger metodens namn. Jokertecknet ”..” betyder ”hur många parametrar som helst, oavsett typ”. Om ”*” hade använts som jokertecken även för parametrar hade betydelsen varit ”en parameter oavsett typ”. Jokertecken behöver inte stå ensamt, utan kan även användas för att ange delar av namn.

Punktsnitt kan också ta parametrar, som kan användas för att ta fram värdet på element som passar in i punktsnittets mönster. Om vi till exempel vill använda findCall-punktsnittet ovan för att ta fram värdet på strängen som skickas in i anropen till findBy...-metoden så ser punktsnittet ut så här istället:

pointcut findCall(String s): call(public * *.findBy*(String s));

I nästa avsnitt ska vi se på hur vi kan använda punktsnitten och de parametervärden vi tittat på ovan.

3.3. Direktiv

Så – vad använder vi då punktsnitten till?

Punktsnitten används för att peka ut vilka delar av applikationen som berörs av den här aspekten. För att sedan ange på vilket sätt dessa delar påverkas av aspekten så använder vi direktiv (eng: advice). Direktiv kan vara dynamiska eller statiska.

Dynamiska direktiv innehåller kod som ska köras vid den kodpunkt som punktsnittet anger. Här är ett exempel på en aspekt som (med hjälp av en hjälpklass som används som räknare) håller koll på hur många gånger metoder som börjar på ”findBy” i klassen ”PartyHome” anropas:

aspect CountFinds {
    // Punktsnitt som passar på anrop av publika metoder i klassen "PartyHome",
    // som har ett namn som börjar på "findBy" oavsett vilka parametrar den har.
 
    // Vi döper detta kodsnitt till "findCall()"
    pointcut findCall(): call(public * PartyHome.findBy*(..));
 
    // Direktiv som exekverar en kodrad just innan kodpunkter som passar
    // på kodsnittet "findCall()" inträffar
 
    before(): findCall() {
        Counter.increaseFindCount();
    }
}

Den lilla aspekten ovan lägger alltså in en intressant mätning i ett befintligt program, utan att vi ändrar en enda kodrad i det befintliga programmet. Vi inför bara en ny aspekt som hanterar ett nytt problemområde.

Den andra typen av direktiv, statiska direktiv, ändrar på strukturen i ett program genom att lägga till nya medlemmar till klasser eller ändra på arvstrukturen för en klass. Vi kan illustrera det med en ny version av aspekten ovan, som lagrar en referens till räknar-hjälpklassen som en medlem i PartyHome – fortfarande utan att ändra något i den ursprungliga programkoden. Det kan se ut så här:

aspect CountFinds2 {
    // Vi börjar med en introduktion, som lägger till en privat
 
    // medlemsvariabel till klassen "PartyHome".
    // Den medlemsvariabeln kan vi senare använda i direktivet nedan.
    private Counter PartyHome.m_callCounter = Counter.getInstance("PartyHome");
 
    // Samma punktsnitt som i förra aspekten (ovan)
    // med tillägget att vi använder oss av en parameter och nyckelordet target
 
    // för att senare kunna använda objektet i direktivet
    pointcut findCall(PartyHome ph): call(public * PartyHome.findBy*(..)) && target(ph);
 
    // direktiv som exekverar en kodrad just innan kodpunkter som passar
 
    // på kodsnittet "findCall()" inträffar. Skillnaden mot förra aspekten är att
    // nu kan vi använda den introducerade medlemsvariablen "m_callCounter".
    before(PartyHome ph): findCall(ph) {
        ph.m_callCounter.increaseFindCount();
    }
}

4. En enkel men komplett aspekt i AspectJ

Nu kan vi ta en titt på en lite mer konkret aspekt. Vi har en applikation som sparar en representation av vissa verksamhetsobjekt ner till en relationsdatabas. Vid vissa tillfällen i logiken anropas metoden ”save()” för att spara ned aktuellt läge till databasen. När applikationen körs i drift visar det sig att det sker väldigt många anrop till ”save()” trots att attributen som sparas oftast är oförändrade. Kanske skulle applikationens prestanda öka väsentligt om man bara kommunicerade med databasen när något attribut faktiskt förändrats?

För att prova detta gör vi en aspekt som håller koll på om något av attributen i klassen förändrats. Om så är fallet så kör vi ”save()” som vanligt, annars ignorerar vi anropen till den metoden. Vi fortsätter att använda fixafest.nu som exempel, så klassen vi vill prova detta på heter ”Party”. Det kan se ut så här:

aspect CheckDirtyOnSave {
    private boolean Party.m_isDirty = false;
 
    pointcut updateAttribute(Party p): call(* Party.set*(*)) && target(p);
 
    after(Party p): updateAttribute(p) {
        p.m_isDirty = true;
    }
 
    pointcut saveCall(Party p): call(public void Party.save()) && target(p);
 
    void around(Party p): saveCall(p) {
        if( p.m_isDirty ) {
            proceed(p);
            p.m_isDirty = false;
        }
    }
}

Vi använder en ny typ av direktiv i aspekten ovan: around-direktivet, som körs i stället för koden som punktsnittet anger. I ett around-direktiv kan ett anrop till proceed göras där man vill att det riktiga anropet ska ske. I vår aspekt ovan görs alltså bara ett riktigt anrop till ”save()” om värdet på ”m_isDirty” är sant.

Om det visade sig att detta var ett bra sätt att lösa problemet med onödiga databasuppdateringar skulle man förstås vilja återanvända aspekten ovan till andra klasser i systemet, och kanske även i helt andra applikationer. Ett sätt att göra en aspekt mer generell är att göra den abstrakt. Jag har inte tänkt diskutera abstrakta aspekter på djupet i den här artikeln, men kortfattat handlar det om att du deklarerar abstrakta punktsnitt i aspekten, som du sedan använder i direktiven i aspekten. När du senare vill återanvända aspekten, ärver du från den abstrakta aspekten (motsvarande arv mellan klasser i Java) och specificerar ett konkret punktsnitt där du anger vid vilka kodpunkter i ditt program som direktiven ska gälla.

5. Användningsområden för AOP och AspectJ

Som jag berättade inledningsvis så är ju syftet med AOP och AspectJ att hjälpa en systemutvecklare med uppdelning av problemområden. De problemområden som mina exempel i artikeln har berört har ju uteslutande varit utvecklingsproblemområden, till exempel loggning och optimering. Ett systems logik och beteende förändras inte när man lägger till eller tar bort sådana aspekter. Därför kan den typen av aspekter vara det bästa sättet att prova på AspectJ, både eftersom det fungerar bra att introducera till befintliga (icke-AOP) program, och eftersom att det inte påverkar programmets funktion om man väljer att ta bort aspekterna senare.

Ett annat bra utvecklingsproblemområde att prova på AOP med är design by contract. Låt en aspekt kontrollera att ett objekts tillstånd är korrekt efter varje anrop, och att inparametrar till metoder stämmer överens med de förväntade. När programmet sedan fungerar och är ordentligt testat kan aspekten tas bort utan att påverka programmets funktion.

AOP kan också användas för att kontrollera att programmeringsregler är uppfyllda. Redan vid kompilering kan en aspekt signalera (med ett felmeddelande) att vissa typer av överenskommelser har blivit förbigångna. Exempel på sådana överenskommelser kan vara:

  • Objekt som hör till klassen X (eller dess underklasser) får bara skapas av klassen XFactory
  • Metoder som anropar ”java.sql.Connection.createStatement()” måste ligga i klasser vars namn slutar på ”DB”
  • Ingen direkt åtkomst till attribut får ske, utom i metoder som är definierade i samma klass som attributen och passar in på mönstret ”get*” respektive ”set*”

AOP kan dock även användas som ett verktyg för design av system, där aspekter spelar en central roll i programmets funktion. Exempel på sådana aspekter kan vara att implementera olika designlösningar som annars kanske skulle göras med hjälp av designmönster, till exempel Observer eller Visitor. (Båda dessa är välkända designmönster som beskrivs i den numera klassiska boken Design Patterns.)

6. Något om verktygsstöd

AOP är en ung teknik som just börjar ta steget från den akademiska världen ut i industrin. Trots att AspectJ är det AOP-verktyg som kommit längst och är det mognaste i mängden, så är fortfarande verktygsutbudet begränsat. I skrivande stund finns dock tillägg till Emacs, JBuilder och Forte, som låter dig hålla koll på vilka aspekter som berör vilka klasser i ditt program. Dessutom finns bra stöd till Jakarta Ant, så att du kan automatisera byggprocessen även med aspekter. På AspectJs mailinglistor diskuteras också tillägg till Eclipse, vilket åtminstone jag personligen ser fram emot.

7. Slutord

Varje gång ett nytt synsätt kommer fram och ”hotar” den etablerade världen kommer det fram kommentarer som ”det där är inget nytt”, ”så där har jag alltid gjort”, ”det där krånglar bara till det jämfört med hur vi gör nu”. Så var det när objektorienteringen kom och så kommer det helt säkert att bli när AOP börjar bli allmänt känt. Och precis som förut ligger det något i dessa kommentarer – visst finns det likheter med hur många designar system idag, och visst finns det befintlig teknik som kan lösa samma problem som AOP. Men, precis som att objektorientering kräver en del vana för att se den ”stora nyttan”, så har också AOP potential att vara något mycket större.

Några av dagens sätt att dela upp problemområden på ett generellt sätt är till exempel med hjälp av designmönster, applikationsramverk, eller med en applikationsserver. (Ofta används alla dessa hjälpmedel tillsammans.) Dessa verktyg är till stor hjälp för att hantera tvärsnittsproblemområden, som de som jag nämnde inledningsvis (säkerhet, datalagring, etc). Nackdelen med dessa hjälpmedel är dock att även om de avlastar utvecklaren mycket så krävs det också att han/hon anpassar även lösningen av huvudproblemområdet till det verktyg han/hon valt för att hantera tvärsnittsproblemområdena. En annan begränsning är att ramverk och applikationsservrar är designade för att lösa en viss typ av problemområden. Detta kanske inte riktigt passar in på vad applikationen egentligen kräver.

Om vi tar en J2EE-kompatibel applikationsserver som exempel, så hanterar den flera vanliga tvärsnittsproblemområden åt applikationen:

  • Transaktionshantering
  • Säkerhet
  • Distribuerade anrop
  • Persistens till databas

En applikation som håller sig till J2EE-specifikationen är lätt flyttbar mellan olika leverantörers J2EE-applikationsservrar. Detta är förstås till stor hjälp och ett väldigt kraftfullt verktyg. Men det ställer också stora krav på att applikationen är designad och byggd för att köras i en J2EE-server. Dessutom klarar J2EE förstås bara av att hjälpa till med de problemområden som det är förberett för i applikationsservern. Om problemområdena hade hanterats med aspekter hade det (i bästa fall) bara varit att välja och vraka vilka tvärsnittsproblemområden som är relevanta för den applikationen.

Som jag nämnde tidigare är AOP en ny teknik som är långt ifrån etablerad i industrin ännu. Mina kollegor och jag experimenterar med tekniken, men som konsulter är vi oftast hänvisade till befintliga lösningar som designmönster och ramverk/applikationsservrar när vi genomför ”riktiga” projekt.

Missförstå mig inte: objektorienterad systemutveckling med designmönster och andra av dagens hjälpmedel är en mogen och kraftfull teknik, och vi använder den gärna. Men gör inte misstaget att tro att utvecklingen tar slut där! AOP ersätter inte objektorientering, AOP är inte heller beroende av objektorientering, men AOP kompletterar objektorienterad systemutveckling och tillåter en utvecklare att modularisera problemområden som tidigare helt enkelt inte varit möjliga. När AOP har blivit jämförelsevis lika etablerad kommer vi att se väldigt intressanta systemlösningar som vi kanske har lite svårt att föreställa oss idag.

Minimala testprogram som använder exempelaspekterna från den här artikeln finns överst på denna sida eller läsas från DMZ-CD:n. Jag diskuterar gärna mer om AOP och AspectJ i mån av tid, och kan nås på robert.buren@bluefish.se.

AOP är coolt! AOP kommer att bli superstort! Kom ihåg var du läste om det först… :-)

8. Länkar till externa AOP-resurser