Motivácia
Ako sa už spomínalo v úvode k predmetu, jedným zo stálych problémov, ktorým sa snažíme čeliť pri návrhu softvéru je oddelenie záujmov — separation of concerns. Objektovo-orientované programovanie poskytuje celkom dobré nástroje na realizáciu takéhoto oddelenia. Umožňuje dekomponovať systém na objekty komunikujúce medzi sebou cez definované rozhrania. Vďaka tomu by malo byť možné zapuzdrenie (encapsulation) záujmov a návrhových rozhodnutí vo vnútri objektu tak, že objekt sa musí meniť iba v prípade zmeny v danom záujme.
V skutočnosti nie je vždy možné izolovať záujmy do samostatných objektov a tried. Dochádza k súvisiacim javom — prepletaniu a rozptýleniu záujmov.
Prepletanie (tangling) je situácia, kedy v jednom module musíme riešiť viacero záujmov. Napríklad metóda okrem svojej hlavnej funkcionality musí riešiť aj otázky bezpečnosti, databázových transakcií, zaznamenávania (logging) a podobne. Pri zmene v ktoromkoľvek z týchto záujmov (napr. zmena knižnice pre logging) sa bude musieť zmeniť aj kód metódy.
Rozptýlenie (scattering) je druhá časť daného problému. Ide o to, že určitý záujem nie je riešený v jednom module, ale kód, ktorý ho rieši je rozptýlený v niekoľkých moduloch. Znovu môžeme uviesť ako príklad zaznamenávanie, kód ktorého je typicky rozptýlený v celom programe. Jeho prípadná zmena vyžaduje úpravy na mnohých miestách.
Záujmy, ktoré najčastejšie spôsobujú uvedené problémy sa nazývajú pretínajúce záujmy (crosscutting concerns). To sú záujmy, ktoré v zvolenej dekompozícii systému nemajú samostatný modul, ale ako keby pretínali viacero modulov naraz. Typickými príkladmi pretínajúcich záujmov sú práve logging, pooling, caching, perzistencia, autentifikácia, transakcie, bezpečnosť a podobne.
Je možné, že pri inom spôsobe dekompozície by bolo možné tieto záujmy vyčleniť do samostatného modulu, avšak v tom prípade by nejaké iné záujmy začali byť pretínajúcimi. Tomuto javu sa hovorí tyrania dominantnej dekompozície.
Daný problém je možné riešiť viacerými spôsobmi. Riešenie pretínajúcich záujmov môžu zobrať na seba aplikačné rámce, ktoré využívajú reflexiu na prispôsobenie sa konkrétnemu kódu. Je možné použiť generovanie alebo modifikáciu kódu na automatické doplnenie realizácie pretínajúcich záujmov. Mnohé návrhové vzory riešia možnosť rozdelenia záujmov, napríklad Observer, Chain of responsibility, Decorator, Proxy.
Aspektovo-orientované programovanie
Aspektovo-orientované programovanie (AOP) je metodológia, ktorá sa pokúša systematicky riešiť problém pretínajúcich záujmov. AOP umožňuje definovať doplnenie kódu tak, že kód samotný nie je nutné modifikovať.
Ukážme si to na príklade. Majme triedu Product, ktorá definuje vlastnosti a správanie produktov v internetovom obchode. Okrem iného táto trieda umožňuje zistiť a zmeniť cenu produktu.
public class Product {
private BigDecimal price;
...
public void setPrice(BigDecimal p) {
price = p;
}
public BigDecimal getPrice() {
return price;
}
}
Ak potrebujeme doplniť záujem zaznamenávania udalostí zmeny ceny, musíme upraviť kód našej triedy.
public abstract class Product {
private BigDecimal price;
private Logger logger = new Logger();
...
public void setPrice(BigDecimal p) {
logger.writeLog("Changed " + price + " to " + p);
price = p;
}
...
}
Ak však použijeme AOP, môžeme ponechať kód triedy Product nezmenený a samostatne v inom súbore definovať jej doplnenie. Určime pritom, že pred vykonaním metódy Product.setPrice(..) sa musí navyše zapísať správa do záznamu.
public aspect LoggingAspect {
private Logger logger;
public LoggingAspect() { logger = new Logger(); }
pointcut priceChange(Product product, BigDecimal price):
execution(* Product.setPrice(..)) && this(product) && args(price);
before(Product product, BigDecimal price): priceChange(product, price) {
logger.writeLog("Changed " + product.getPrice() + " to " + price);
}
}
Ako je možné vidieť na príklade, v AOP sa dekompozícia uskutočňuje dvomi spôsobmi:
- pomocou komponentov, ktoré predstavujú základný spôsob dekompozície definovaný programovacím jazykom;
- pomocou aspektov zapuzdrujúcich pretínajúce záujmy.
Aspekty definujú správanie pridávané do komponentov a tiež spôsob ako sa má toto správanie pridať. Následne dochádza k procesu, ktorý sa nazýva pretkávanie (weaving), pri ktorom sú komponenty modifikované na základe aspektov a spájaniu ich funkcionality. Pretkávanie môže byť statické (počas prekladu programu) alebo dynamické (počas načítavania programu alebo až počas behu).
Na príklade môžem ukázať aj ďalšie základné pojmy AOP:
- Bod spájania (joinpoint) — bod v procese vykonávania programu, ktorý môžeme ovplyvniť pomocou aspektu. Napríklad vykonávanie metódy, prístup k členskej premennej a podobne. Každý nástroj pre AOP môže používať rozdielnu sadu bodov spájania.
- Bodový prierez (pointcut) — výraz pre výber množiny bodov spájania. Napríklad každé vykonávanie metódy Product.setPrice.
- Odporučenie (advice) — funkcionalita vykonávaná vo vybraných bodoch spájania. Napríklad zaznamenanie do logu.
Koncept AOP bol predstavený v roku 1997 v článku Gregora Kiczalesa a jeho spolupracovníkov z Xerox PARC. V roku 2001 bola vytvorená implementácia AOP pre Javu — AspectJ. Všetky príklady v tejto prednáške boli implementované práve pomocou AspectJ.
Zo začiatku boli s AOP spájané veľmi veľké nádeje. Zdalo sa, že to môže byť nová prevratná metodológia, ktorá ovplyvní programovanie podobne, ako predtým objektovo-orientované programovanie. Tieto nádeje sa nenaplnili, ale pre AOP sa našli oblasti, v ktorých je užitočné. Predovšetkým je to použitie pri implementácii aplikačných rámcov. Napríklad rámec Spring využíva princípy AOP a dokonca má vlastnú implementáciu — SpringAOP, ktorá je čiastočne kompatibilná s AspectJ.
AspectJ
AspectJ je programovací jazyk, ktorý rozširuje jazyk Java o možnosť definovať aspekty. Kód v jazyku AspectJ používa príponu .aj
. Prekladač AspectJ — ajc
— zároveň slúži ako pretkávač (weaver).
AspectJ definuje možné typy bodov spájania, ktoré počas behu programu vieme ovplyvniť, a tiež zápis bodových prierezov, pomocou ktorých vyberáme konkrétne body spájania na základe určitých kritérií. Definuje tiež niekoľko typov odporučení, pomocou ktorých môžeme ovplyvniť správania v bodoch spájania. Okrem toho AspectJ podporuje tzv. statické pretínanie, ktoré mení nielen správanie, ale aj štruktúru komponentov, napríklad umožňuje pridať metódy do tried.
Poznámka
V tomto dokumente sú len stručne predstavené základné pojmy a konštrukcie AspectJ. Tento popis však nie je kompletný a detaily vždy nájdete v oficiálnej dokumentácii.
Body spájania (joinpoints)
AspectJ definuje niekoľko druhov bodov spájania, ktoré je možné ovplyvniť. Ide o volanie a vykonávanie metód a konštruktorov, čítanie a zápis do členských premenných, statická inicializácia triedy (jej statických premenných), inicializácia objektov, ošetrenie výnimiek v bloku catch
a vykonávanie odporučení definovaných aspektmi. Úplný prehľad bodov spájania nájdete v dokumentácii.
Toto sú teda tie udalosti počas behu programu, ktoré môžeme vybrať pomocou bodových prierezov a naviazať na ne odporučenia.
Bodové prierezy (pointcuts)
Bodové prierezy sú výrazy pre výber množiny bodov spájania na základe určitých podmienok. Bodové prierezy môžeme navzájom kombinovať pomocou logických operátorov: &&
(a súčasne), ||
(alebo), !
(nie).
Bodové prierezy sa delia na tri skupiny:
- Prierezy podľa druhu bodu spájania (kinded pointcuts),
- Prierezy pre určenie rozsahu (scoping pointcuts),
- Kontextové prierezy (contextual pointcuts).
Bodové prierezy môžeme pomenovať pomocou kľúčového slova pointcut
, alebo ich môžeme uviesť priamo na mieste použitia v definícii odporučenia. Podrobnejšie informácie nájdete v dokumentácii k bodovým prierezom.
Bodové prierezy podľa druhu bodu spájania
Pomocou týchto bodových prierezov vyberáme druh bodov spájania, ktoré chceme ovplyvniť. Zapisujú sa v tvare: «druh bodu spájania»(«vzor signatúry»)
, napríklad
execution(* Product.setPrice(..))
Kľúčové slová pre výber druhu bodu spájania sú napríklad execution a call pre vykonávanie a volanie metódy, get a set pre čítanie a zápis členskej premennej, alebo handler pre ošetrovanie výnimiek.
Vzory signatúr opisujú signatúru vyberaného elementu (metódy, premennej, typu) s tým, že môžu obsahovať aj zástupné symboly, ktoré umožňujú vybrať viac elementov naraz. Napríklad
*Account
vyberie všetky triedy, ktorých názov končí slovom „Account“,java..*
“ — všetky triedy v balíkujava
a jeho podbalíkoch,public void Account.set*(*)
— všetky verejné metódy triedy Account, ktorých názov začína slovom „set“ a majú práve jeden parameter,public * Account.*(..)
— všetky verejné metódy triedy Account,@Secured * *(..)
— všetky metódy ľubovoľnej triedy, ktoré majú anotáciu @Secured.
Zástupné symboly sú teda dva:
*
označuje ľubovoľnú postupnosť znakov okrem bodky a čiarky (oddeľovačov balíkov a parametrov metódy),..
— ľubovoľný znak vrátane bodky a čiarky.
Navyše je možné označiť, že vzoru budú vyhovovať aj všetky podtypy daného typu tak, že za jeho názov pridáme znak +
: Account+
. Ďalšie informácie nájdete v dokumentácii bodových prierezov a samostatnej dokumentácii pre použitie anotácií v bodových prierezov.
Kontextové bodové prierezy
Kontextové bodové prierezy umožňujú zúžiť výber bodov spájania na základe kontextu. Ide predovšetkým o výber na základe typov v čase vykonávania programu:
- this(«Typ»)
- target(«Typ»)
- args(«Typy»)
Tieto bodové prierezy majú dvojaký účel. Po prvé slúžia na obmedzenie výberu bodov spájania. Typ aktuálneho objektu (this), cieľového objektu volania (target) alebo argumentov (args) musí zodpovedať typu uvedenému v priereze (prípustný je aj podtyp).
Po druhé tieto prierezy umožňujú zachytiť kontext ako parameter prierezu a využiť ho vo vnútri odporučenia. Napríklad v našom odporučení pre sledovanie zmeny ceny produktu zachytávame objekt produktu a argument metódy, aby sme tieto objekty využili vo výpise.
before(Product product, BigDecimal price):
execution(* Product.setPrice(..)) && this(product) && args(price) {
logger.writeLog("Changed " + product.getPrice() " to " + price);
}
Odporučenie (advise)
AspectJ poskytuje tri druhy odporučení:
- before, ktoré sa vykoná pred bodom spájania;
- after, ktoré sa vykoná za bodom spájania;
- around, ktoré umožňuje obklopiť vykonávanie bodu spájania.
Odporučenie around je zaujímavé tým, že umožňuje nielen doplniť nové správanie pred a za bodom spájania, ale aj ovplyvniť vykonávanie pôvodnej funkcionality. Napríklad môže vykonať pôvodnú funkcionalitu viac krát, alebo nevykonať vôbec.
Syntax a príklady jednotlivých druhov odporúčaní nájdete v oficiálnej dokumentácii.
Literatúra
- AspectJ Programming Guide
- AspectJ 5 Developes's Notebook — rozšírenia pridané vo verzii 5
- Ramnivas Laddad. AspectJ in Action, Second Edition (Manning 2009)