1. Aspektusok
Ahogy a számítógépes programok nőttek,
úgy vált egyre súlyosabb
problémává ezen programok
áttekinthetősége, karbantarthatósága
és újrafelhasználhatósága. A kezdeti
magas szintű nyelvek (pl. FORTRAN) az assembly nyelvek
szerkezetét másolták. A goto
utasításokkal teletűzdelt "spagetti kód"
érthetetlensége hozta a strukturális nyelveket,
amelyeknek legfontosabb élő tagja a C. Ezen nyelvek
használata nyílt, jól definiált
interfészeket és jól
körülhatárolt modulokat eredményezhet, amelyek
az olvashatóságot és az
újrahasználhatóságot nagyban
megnövelik. Problémájuk, hogy az
implementáció viszonylag nyílt a programozó
számára (nincs adat- és kódrejtés)
valamint a bővítéshez hozzá kell nyúlni a
modulhoz.
Az objektumorientált nyelvek mindkét
problémára választ adnak. Ezen nyelvek adat-
és kódrejtési szolgáltatásaival
pontosan meghatározhatjuk, mit teszünk
láthatóvá más modulok
számára, az öröklődési
szolgáltatásokkal pedig úgy
bővíthetjük az eredeti modulokat, hogy azok
implementációjához nem kell
hozzányúljunk.
Az objektumorientált programozás
széleskörű elterjedése mégse hozta meg a
modularizáció kívánt fokát. Ennek
oka több kutató szerint az
átmetsző követelmények (crosscutting
concerns) jelenléte. Átmetsző követelmény az,
ami a szoftver választott
modularizációjában nem fogható meg egy
modul által. Minthogy a modularizáció
általában a funkció függvénye, ezek
tipikusan nem funkcionális követelmények vagy pedig
olyan funkcionális követelmények, amelyek az eredeti
funkcionális dekompozíciónál nem voltak
ismertek és a jelenlegi modulszerkezetben nem helyezhetők el egy
modulként. Hipermetszeres ismereteinket felhasználva azt
is mondhatjuk, hogy az átmetsző követelmény
hipermetszete a domináns dekompozíció
hipermetszetére vetítve nem
modularizálható.
Tipikus átmetsző követelmény pl. a
naplózás (ahol a naplót a program sok
pontján kell írni), a biztonság (ahol a
biztonsági követelményeket sok helyen kell
ellenőrizni) vagy felhasználói felületek
általános viselkedése (ha rábökök
az ablak sarkára, az mindig bezárul,
függetlenül attól, milyen ablak).
A domináns dekompozíciót megváltoztatva az
átmetsző követelmény esetleg egyszerűen
modularizálhatóvá válik. Kiczales, az
aspektusorientált programozás atyja azt
állítja, hogy mindig található olyan
követelmény, ami az adott dekompozícióban
átmetszővé válik. A hiperteres
megközelítésben ez triviálisnak
látszik, mert egy hipermetszet vetítése a
domináns dekompozíció metszetére nem
feltétlenül eredményez jól
körülhatárolt programmodult. Az
állítás formális bizonyítása
mindenesetre még várat magára.
Az aspektus egy átmetsző követelmény
reprezentációja. Az aspektus hasonlít a
komponenshez abban, hogy a követelményt egy "csomagban"
írja le, de az aspektus hatása a rendszerben
elszórva jelentkezik. A pontos definíció a
következő:
- A komponest le tudjuk írni
általánosított
függvényhívás által
- Az aspektust nem lehet leírni
általánosított
függvényhívás által. Az aspektusok
általában nem a rendszer funkcionális
dekompozíciójából keletkeznek, hanem olyan
tulajdonságok, amelyek a rendszer komponenseinek
működését jól meghatározható
módon megváltoztatják.
Az aspektusorientált programozási nyelvek az aspektusok
modularizálását tűzik ki célul. A
funkcionális dekompozíció (ez a
dekompozíció domináns) céljára egy
alapnyelvet választanak, ezt a nyelvet komponensnyelvnek is
hívják. Az első aspektusorientált nyelvek
kivétel nélkül objektumorientált nyelvet
használtak komponensnyelvnek, ígéretes
kísérletek folynak a C nyelvvel. Az aspektusokat egy
másik nyelven, az aspektusnyelven írják le. Mint
látni fogjuk, az aspektusnyelv lehet éppenséggel
ugyanaz az objektumorientált nyelv is. Indulásként
adott egy program a komponensnyelven megírva, ez
általában a funkcionális
dekompozíció eredménye. Erre alkalmazzuk az
aspektusokat és az aspektusok által leírt
átmetsző követelmények megváltoztatják
a program működését. Ez a folyamat a
szövés (weaving). A
szövés során az aspektus programhelyeket
azonosít és ezeken a helyeken megváltoztatja a
kódot (tipikusan betold programrészt). Ezek a helyek a
programpontok (join point).
Az aspektus tehát önmagában nem létezhet,
csakis a módosított programrészletek
kontextusában. Ezt úgy is mondják, hogy az
objektum valami, az aspektus viszont valaminek a valamije. A hiperteres
megközelítéstől eltérően az
aspektusorientált modellben kötelezően van domináns
dekompozíció (a komponensnyelvben véghezvitt
funkcionális dekompozíció) és az aspektusok
nem fejleszthetők magukban, csakis a domináns
dekompozíció kontextusában.
Jelenleg a legnépszerűbb és legérettebb
aspektusorientált implementáció az AspectJ. Ezzel
kezdjük az ismerkedést.
2. Az AspectJ aspektusnyelv
Az aspektusnyelv programpontokat azonosít és ide
kóddarabokat szúr. Egy aspektust tehát két
dolog ír le:
- A programpontokat kiválasztó
kifejezés, ezt pontszűrőnek (pointcut) nevezik. A pontszűrő
határozza meg a programhelyekt, ahol az aspektust
alkalmazni kell.
- Maga a beszúrandó programrészlet, ill.
a beszúrási szabályok. Ezt tanácsnak
(advice) nevezzük.
Ezen két elemet egy szintaktikai egységbe fogják
össze. Az AspectJ az aspektusokat Jáva osztályokra
képezi le, habár ez a nyelv szintjén nem
derül ki (észrevehetjük viszont a .class
fájlokat a lefordított programban). Az ismerkedést
kezdjük egy egyszerű példával! Lássuk a
következő egyszerű programot!
Hello.java:
public class Hello
{
public
void saySomething() {
System.out.println( "Something" );
}
public
int saySomethingElse( String msg ) {
System.out.println( "Something else: "+msg );
return 0;
}
public
static void main( String args[] ) {
Hello h = new Hello();
h.saySomething();
h.saySomethingElse( "hello" );
}
}
Ezek után nézzünk egy aspektust, ami a This
szöveget írja ki minden say-jel kezdődő nevű metódus
végrehajtása előtt, a That szöveget pedig
utána,
TT1.java:
public aspect TT1 {
pointcut p1() : execution( * *.say*( .. ) );
before(): p1() {
System.out.println( "This" );
}
after(): p1() {
System.out.println( "That" );
}
}
Nézzük ezt részekben! Először is
definiáltunk egy pontszűrőt, aminek a p1 nevet adtuk. Maga a
kiválasztókifejezés ez: execution( * *.say*( .. )
). Ez azt mondja, hogy keressük az összes metódust
akármilyen visszatérési értékkel
(első *) bármilyen osztályban (*.) aminek neve say-jel
kezdődik (say*) és akárhány ill. akármilyen
típusú paraméterei vannak. ((..)). Ha ilyet
találunk, akkor a metódus végrehajtása maga
a kiválasztott programpont. A metódus
végrehajtása előtt (before() : p1() ) egy programdarabot
hajtunk végre, ami most kiírja a This szöveget,
utána (after() : p1()) pedig egy másikat, ami a That
szöveget írja ki. Ha a programot lefordítjuk (ajc
Hello.java TT1.java) majd végrehajtjuk (java Hello) a kimenet
nem meglepő:
This
Something
That
This
Something
else: hello
That
Ha a pontszűrőt csak egy tanácsnál akarjuk
használni, a pontszűrőt nem kell elnevezni,
egyszerűsített szintakszis is használható. A
következő példában nincs after() tanács,
tehát egyszerűsíthetjük a szintaxist.
TT1short.java:
public aspect
TT1short {
before() : execution( * *.say*( .. ) ) {
System.out.println( "This" );
}
}
Érdemes az execution() programpont-kiválasztóra
visszatérni. Van egy hasonló
programpont-kiválasztó, a call(). Az execution
magának a kijelölt metódusnak a
végrehajtását kapja el, tehát a
kijelölt metódus törzsébe szúrja a
tanácsot. A call() a metódus hívását
kapja el, tehát a tanácsot a metódus
hívása elé és utána szúrja
be. Normálisan ennek nincs jelentősége, de
speciális esetekben fontos lehet. A Jáva
alkalmazás main() metódusát pl. nem lehet call()
kiválasztóval elkapni, mert ez a hívás
nincs a Jáva programban. Ugyancsak fontos lehet a
különbség konstruktorok
hívásánál.
Térjünk vissza a metódus
kiválasztására, mert ez az AspectJ-nek
különös erőssége. A metódus nevén
kívül annak akármelyik tulajdonságára
kereshetünk. Például az int *.say*( String ) olyan
metódusokat keres, amelyek visszatérési
értéke int, a neve say-jel kezdődik és egy String
paramétere van. A módosítókra is
kereshetünk, a static * *(..) pl. kiválasztja az
összes statikus metódust visszatérési
értéktől, deklaráló
osztálytól, névtől és paraméterektől
függetlenül. A konstruktor hívása is
elkapható, pl. execution( Hello.new() ); elkapja a Hello
osztály paraméter nélküli
konstruktorának végrehajtását. Az
osztálykijelölésnél az osztály neve
lehet interface név is, pl. MyInterface.new() elkapja az
összes MyInterface interfészt implementáló
osztály konstruktorának
végrehajtását. Sőt, a
leszármazást is kezelni lehet, pl. * Hello+.method()
elkapja a Hello osztály és leszármazottjai
method() metódusainak hívásait (akármilyen
visszatérési értékük is legyen).
Ezen felül még további feltételeket adhatunk
meg a pontszűrőben. Íme néhány
közülük:
- handler( ArrayOutOfBoundsException ) - ha az megadott
kivétel kezelője fut
- this( SomeType ) - ha this bizonyos típusú
- target( SomeType ) - ha a célobjektum (pl. a
metódushívás célobjektuma) bizonyos
típusú
- within( SomeClass ) - ha a végrehajtódó
kód a SomeClass osztályhoz tartozik.
- withincode( * say*() ) - illeszkedik a say* szűrőre illeszkedő
nevű, tetszőleges visszatérési értékű
metódus törzsében levő utasításokra.
- cflow( call( void saySomething() ) - ha a programpont a cflow
paraméterében levő programpont végrehajtási
útjában fekszik, vagyis a saySomething
végrehajtása elkezdődött. A példa
kiválasztja a saySomething belsejét, valamint a belőle
hívott összes metódust, ha hozzájuk a
hívás a saySomething-en keresztül jutott el.
- cflowbelow( call( void saySomething ) ) - hasonló az
előzőhöz, de ez csak a saySoemthing-ból hívott
metódusokat választja ki.
- set( int T.x ) - A T osztály x
egyedváltozójának (int típusú)
értékadó műveleteinek elkapása. Több
mezőre és módosítókra is kereshetünk:
set( public * * ) az összes osztály mindegyik
publikus egyedváltozójának
írását elkapja.
Osztályváltozó (static) is elkapható a
set-tel.
- get( T.x ) - Mint az előző, de egyedváltozók
elérését keresi.
Feltételek gazdagon kombinálhatók a
!,||,&& operátorokkal. Pl. handler(
ArrayOutOfBoundsException ) && within( MyClass ) csak a MyClass
metódusaiban levő ArrayOutOfBounds kivételkezelőket
választja ki. Ennél még ravaszabbat is
tehetünk: pl. az execution( public !static * *( .. ) ) minden
publikus, nem statikus metódus
végrehajtását kiválasztja.
A pontszűrőnek paraméterei lehetnek, ez lehetővé teszi a
tanács kódjának, hogy hozzáférhessen
a programpont információihoz. A következő
példa a saySomethingElse paraméterét írja
ki.
TT4.java:
public aspect TT4 {
pointcut p1( String parms ) : execution( int saySomethingElse( String )
) && args( parms );
before( String parms ): p1( parms ) {
System.out.println( "saySomethingElse called with parameter "+parms );
}
}
Az args() szűrő elkap minden hívást, ahol az argumentumok
típusa megfelel az args
paraméterlistájának. Pl.:
pointcut intArg(int i): args(i);
Ez elkapja az összes hívást, ahol csak egy int
paraméter van. Az args-nak azonban mellékhatása is
van: ha a pontszűrőnek paramétere is van, akkor az args()
segítségével kiszűrt paraméterek a
tanácsba juttathatók.
Ugyanez a trükk a target() feltétellel
felhasználható a célobjektum
referenciájának az aspektusba juttatására.
A célobjektum egy metódushívás vagy
változómanipuláció célja lehet.
TT5.java:
public aspect TT5 {
pointcut p1( Hello h ) : execution( int saySomethingElse( String ) )
&& target( h );
before( Hello h ): p1( h ) {
System.out.println( "calling saySomething before saySomethingElse ..."
);
h.saySomething();
}
}
A this(
azonosító )
szűrő hasonlóképpen felhasználható a this()
típusa szerinti szűrésre és illeszkedés
esetén a this referencia tanácsba
juttatására.
A set() az args()-sal kombinálva lehetővé teszi az
értékadó művelet jobb oldalának
vizsgálatát az értékadás előtt. A
set() csak osztály és egyedváltozókra
működik, lokális változókra nem, így
Hello programunkat egy kicsit módosítani kell.
static
Hello h = null;
...
h =
new Hello();
Ekkor az aspektus:
TT10.java:
public aspect TT10
{
pointcut p1( Hello h ) : set( Hello h ) && args( h );
before( Hello h ) : p1( h ) {
System.out.println( "Hello object is set: "+h );
}
}
És a kiírás:
Hello object is
set: null
Hello object
is set: Hello@1a16869
Something
Something
else: hello
Tehát a h mindkét értékadását
elkapta.
Eddig megismertük a before() és after() tanácsot,
ami a kiválasztott programpont előtt és mögött
tud kódot végrehajtani. Az after() tanács
változatai meg tudják különböztetni, ha a
metódus rendben tért vissza, vagy ha kivételt
dobott. A példa kedvéért módosítsuk
a Hello.java-t:
Hello2.java:
public class
Hello2 {
public
int saySomethingElse( String msg ) {
if( msg.equals( "hello" ) )
throw new IllegalArgumentException();
System.out.println( "Something else: "+msg );
return 0;
}
public
static void main( String args[] ) {
Hello2 h = new Hello2();
h.saySomethingElse( "hallo" );
h.saySomethingElse( "hello" );
}
}
Az új saySomethingElse kivételt dob, ha "hello"
paraméterrel hívják meg. Kezeljük le a
két különböző visszatérési
lehetőséget!
TT1_2.java:
public aspect
TT1_2 {
pointcut p1() : execution( int saySomethingElse( String ) );
after() returning: p1() {
System.out.println( "saySomethingElse returned" );
}
after() throwing: p1() {
System.out.println( "saySomethingElse threw exception" );
}
}
A kiírási kép megmutatja, hogy az after() throwing
előbb lefut, majd a kivétel rendben továbbterjesztődik a
hívónak:
Something else:
hallo
saySomethingElse
returned
saySomethingElse
threw exception
Exception in
thread "main" java.lang.IllegalArgumentException
at Hello2.saySomethingElse(Hello2.java:5)
at Hello2.main(Hello2.java:13)
A kiválasztott pontot
teljesen kihelyettesíthatjük az around() tanáccsal.
A következő példa a saySomethingElse metódust egy
új implementációval helyettesíti
(újra visszatérünk a Hello.java-ra).
TT6.java:
public aspect TT6 {
pointcut p1( String s ) : execution( int saySomethingElse( String ) )
&& args( s );
int
around( String s ) : p1( s ) {
System.out.println( "New implementation printing "+s );
return 1;
}
}
Az eredeti kód a proceed speciális
metódushívással hívható,
tehát a példánkban proceed( s ) meghívta
volan az eredeti saySomethingElse() metódust.
Az AspectJ lehetővé teszi az aspektusnak, hogy
megváltoztassa az osztályok
struktúráját. Az aspektus új
leszármazási hierarchiát hozhat létre,
új interfészek implementálását
írhatja elő, új változókat és
metódusokat szúrhat az osztályokba. Lássunk
egy példát, ami kedvenc Hello osztályunkba egy
új statikus változót szúr be és ezt
a változót felhasználja a say-jel kezdődő nevű
metódusok végrehajtásainak
számlálására.
TT7.java:
public aspect TT7 {
public
static int Hello.callCtr = 0;
pointcut p1() : execution( * *.say*( .. ) );
before() : p1() {
++Hello.callCtr;
System.out.println( "say* method were executed "+Hello.callCtr+" times"
);
}
}
Az aspektus megváltoztathatja az osztály
leszármazási hierarchiáját is. Az
alábbi példában Hello
alkalmazásunkból appletet csinálunk
olymódon, hogy a java.applet.Applet-et tesszük az
ősosztályává és implementálunk egy
init() metódust.
TT8.java:
public aspect TT8 {
declare parents: Hello extends java.applet.Applet;
public void Hello.init() {
saySomething();
saySomethingElse( "hello" );
}
}
A lehetőségek érzékeltetésére az
AspectJ kézikönyvből kimásoltam egy
példát, amely a Point, Line és Square nevű
osztályokkal implementáltatja a HasName nevű
interfészt, amit ugyancsak az aspektus definiál és
a metódusainak implementációit is megadja.
A.java:
public aspect A {
private interface HasName {}
declare parents: (Point || Line || Square) implements HasName;
private String HasName.name;
public String HasName.getName() { return name; }
}
Az AspectJ a legkiforrottabb aspektusorientált nyelv,
fejlődése során jelentősen megváltozott, a kezdeti
változatok biztonsági és elvi
problémáit kiküszöbölték. Nem lehet
azonban látni azt, hogy az AspectJ
eszközkészletét elég hasznosnak
találják-e a programozók valódi feladatok
megoldására vagy hogy az aspektusorientált
megközelítés kiállja-e az idő
próbáját.
Ajánlott irodalom:
- T.J. Highley, M. Lack, P. Myers: Aspect-oriented programming - a
critical analysis of a new programming paradigm.
- G. Kiczales, J. Lamping, A. Mendhekar, C. Maeda, C. V. Lopes, J-M
Loingtier, J. Irwin: Aspect-Oriented programming, European Conference on Object-Oriented
Programming (ECOOP), Finland, June 1997
- AspectJ honlap
- Ramnivas
Laddad: I want my AOP!