Programowanie aspektowe
Jednym z niepożądanych zjawisk dość często pojawiających się przy tworzeniu aplikacji w oparciu o programowanie zorientowane obiektowo jest nadmierny rozrost metod. Najczęściej muszą one wykonać szereg dodatkowych operacji (sprawdzenie uprawnień, poprawności danych, logowanie czynności, itp.) zanim przystąpią do wykonywania głównego zadania. Często można sobie z tym poradzić uwzględniając takie ewentualności w projekcie aplikacji, zwykle nie jest to jednak idealne rozwiązanie. W tym celu powstała idea programowania zorientowanego aspektowo, które jest przystosowane właśnie do radzenia sobie z tego typu problemami.
Podstawowe założenia
Programowanie zorientowane aspektowo w żadnej swojej formie nie wyklucza programowania obiektowego. Właściwie to jest jego uzupełnieniem dostarczającym dodatkowe elementy pozwalające inaczej spojrzeć na projekt aplikacji. Aspektowe języki programowania pozwalają na wskazanie miejsc w kodzie obiektowym w których mają zostać wykonane dodatkowe operacje. Istnieje wiele implementacji tego, lecz zdecydowanie najbardziej popularny jest AspectJ stworzony dla Javy. Wprowadza on następujące elementy:
- pointcut – ogólny opis miejsca przecięcia kodu w którym mają zostać wykonane operacje definiowane przez aspekt. W większości sytuacji jest to wywołanie lub wykonanie kodu funkcji składowej, ale możliwe jest także zdefiniowanie pointcuta przy każdej operacji wykonanej przez metody w danej klasie. Pointcuty mogą także dotyczyć każdego dostępu do pola lub tworzeniu obiektu.
- joinpoint – konkretne miejsce przecięcia kodu w którym zostaną wstawione dodatkowe operacje definiowane przez dany aspekt.
- advice – definiuję operację jaka ma zostać wykonana w joinpointach. Kod opisany w advices jest najczęściej wstawiany przed lub po wywołaniu metody w klasie, ale także w przypadku rzucenia wyjątku lub oryginalna funkcja składowa zostaje całkowicie przez niego zastąpiona.
W przypadku AspectJ a także wielu innych języków aspekt jest traktowany jak klasa. Oprócz wyżej opisanych elementów może zawierać funkcje składowe lub pola. Mogą być tworzone ich instancje, ale najczęściej aspekt jest singletonem. Dodatkowo można utworzyć hierarchię aspektów korzystając z dziedziczenia. Wiele implementacji pozwala także na istnienie abstrakcyjnych aspektów oraz wirtualnych pointcutów.
Realizacja
W przypadku Javy i AspectJ aspekty są realizowane w czasie kompilacji z niewielkim wsparciem refleksyjności dzięki czemu nie wpływają w dużym stopniu na wydajność. W przypadku AspectC++ w związku brakiem wsparcia dynamicznego metaprogramowania ze strony C++ kod aspektowy (napisany w AspectC++) jest najpierw tłumaczony do kodu obiektowego (C++) i dopiero wtedy kompilowany. Bibliotek wprowadzająca aspekty w C# LOOM.NET wykonuje wszystkie operacje z tym związane podczas działania programu, podobnie rzecz się ma w przypadku modułu Perla Aspect. Generalnie sposób realizacji jest inny dla różnych bibliotek i także powinien być świadomym wyborem ponieważ, może w dość dużym stopniu wpłynąć na wydajność programu.
Zastosowanie
Najbardziej oczywistymi zastosowaniami programowania aspektowego to wszelkiej maści loggery i narzędzia diagnostyczne. Można także użyć ich do kontrolowania zgodności otrzymanych danych z kontraktami, ale należy pamiętać, że wiele zalet aspektów jest zauważalna dopiero wtedy gdy odnoszą się do tych samych operacji wykonywanych w wielu metodach. Przykładem może być realizacja wzorca Obserwator, gdzie każda zmiana stanu obiektu obserwowanego wymaga poinformowania o niej obiekty obserwujące. W tym przypadku najlepszym rozwiązaniem jest wstawienie po wywołaniu każdej metody (nie oznaczonej jako const) klasy obiektu obserwowanego wywołania metody uaktualniającej obiekty obserwujące.
Przykład
Warto przedstawić prosty przykład użycia aspektów w realizacji wzorca Obserwator. Zwykłe podejście wymagałoby czegoś podobnego do kodu poniżej:
class Obserwowany {
// ...
public void metoda1(int wartość) {
// ... operacje ...
uaktualnijObserwatorów();
}
public void metoda2(string tekst) {
// ... operacje ...
uaktualnijObserwatorów();
}
}
W przypadku AspectJ kod realizujący te same funkcje mógłby wyglądać następująco:
class Obserwowany {
// ...
public void metoda1(int wartość) {
// ... operacje ...
}
public void metoda2(string tekst) {
// ... operacje ...
}
}
aspect WzorzecObserwator {
pointcut operacja() : execution(public * Obserwowany.*(..));
after() : operacja() {
uaktualnijObserwatorów();
}
}
Ten przykład bardzo dobrze prezentuje główne założenie aspektów. Jest nim pozbycie się często powtarzanego kodu niezwiązanego z głównym przeznaczeniem metody z jej kodu. Taka izolacja ma na celu sprawienie kodu bardziej przejrzystym i ułatwienie wprowadzenia ewentualnych zmian.
Problemy
Głównym problemem związanym z programowaniem aspektowym jest fakt, że sam pomysł jest stosunkowo młody i w związku z tym wsparcie jest wciąż niewielkie. O ile ze znalezieniem odpowiednich narzędzi nie powinno być problemów to kwestia przyzwyczajenia programistów i to, że języki modelowania takie jak UML nie zostały stworzone z myślą o aspektach sprawiają, że wciąż nie zyskało dużej popularności.
Podsumowanie
Programowanie aspektowe, mimo że z niskopoziomowego punktu widzenia nie zmienia praktycznie niczego, to bez wątpienia jest w stanie wprowadzić pewne uporządkowanie w miejscu gdzie typowe programowanie obiektowe pozostawiło dużą swobodę. Rozwiązanie ciekawe, ale jest tylko jednym z wielu sposobów na ominięcie pewnych problemów co sprawia, że nie odnosi, jak na razie, spektakularnych sukcesów.