Aspektowe warunkowanie algorytmu
W programach, z których korzysta wielu różnych użytkowników, moduł ustawień potrafi być bardziej skomplikowany niż sama aplikacja. Większość opcji przyjmuje postać wartości logicznych. W naszym zespole powstał nawet termin Single Checkbox Requirement (albo Single Checkbox Apocalypse), oznaczający funkcję programu, która może być włączona lub wyłączona i wpływa na wiele różnych elementów systemu (np. powiadomienia email).
Wprowadzenie jednego SCR do projektu generuje kilka(naście/dziesiąt) dodatkowych instrukcji warunkowych. Większa ilość takich wymagań powoduje, że kod źródłowy staje się całkowicie nieczytelny (niezależnie od zastosowanych zabiegów poprawiających jego jakość) – zwłaszcza, gdy pojawiają się one po pół roku (sic!) pisania aplikacji.
Programowanie aspektowe pozwoliło mi rozwiązać opisany problem szybko i w elegancki sposób.
W przykładzie wykorzystam plik pom.xml opracowany w jednym z wcześniejszych wpisów. Struktura projektu będzie wyglądać następująco:
- src
- main
- java
- pl.info.czerwinski.conditional
- java
- test
- java
- pl.info.czerwinski.conditional
- resources
- java
- main
- pom.xml
Aby w prosty sposób włączać i wyłączać wykonywanie określonych metod, oznaczę je adnotacją @Conditional:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
public @interface Conditional {
String value();
}
Wartością atrybutu value będzie nazwa parametru warunkującego wykonanie danej metody.
Aby ułatwić sobie zadanie, do testów wykorzystam dwie implementacje prostego algorytmu:
interface Calculation {
double calculate();
}
Metoda calculate będzie wykonywać pewne operacje arytmetyczne, po czym zwróci ich wynik.
Najpierw utworzę klasę FullCalculation, której wszystkie metody będą zawsze wykonywane:
class FullCalculation implements Calculation {
public double calculate() {
initValue();
multiply();
divide();
add();
subtract();
return value;
}
private void initValue() {
value = 1.0;
}
private void multiply() {
value *= 2.0;
}
private void divide() {
value /= 2.0;
}
private void add() {
value += 1.0;
}
private void subtract() {
value -= 1.0;
}
private double value;
}
Algorytm składa się z kilku atomowych operacji:
- ustawienie wartości na 1,
- pomnożenie wartości przez 2,
- podzielenie wartości przez 2,
- dodanie 1 do wartości,
- odjęcie 1 od wartości,
- zwrócenie wartości.
Jeżeli żaden etap obliczeń nie zostanie pominięty, wynik powinien wynosić 1.
Klasa ConditionalCalculation jest niemalże dokładną kopią pełnej implementacji. Tym razem metody multiply, divide, add i subtract oznaczone są adnotacją @Conditional z odpowiednimi parametrami:
class ConditionalCalculation implements Calculation {
public double calculate() {
initValue();
multiply();
divide();
add();
subtract();
return value;
}
private void initValue() {
value = 1.0;
}
@Conditional("MULTIPLY")
private void multiply() {
value *= 2.0;
}
@Conditional("DIVIDE")
private void divide() {
value /= 2.0;
}
@Conditional("ADD")
private void add() {
value += 1.0;
}
@Conditional("SUBTRACT")
private void subtract() {
value -= 1.0;
}
private double value;
}
Działanie przykładowego aspektu oprę na ustawieniach pobieranych z pliku parameters.properties. W typowej aplikacji będą to raczej wielkości pobierane z bazy danych.
Spośród obliczeń warunkowych włączę tylko mnożenie i dodawanie. Teraz rezultat powinien wynieść 3:
# ConditionalCalculation properties: MULTIPLY=true DIVIDE=false ADD=true SUBTRACT=false
Metody testowe dla obu algorytmów są raczej oczywiste:
public class CalculationTests {
@Test
public void testCalculateFull() {
Calculation calculation = new FullCalculation();
assertEquals(calculation.calculate(), 1.0,
"Full calculation value");
}
@Test
public void testCalculateParameterized() {
Calculation calculation = new ConditionalCalculation();
assertEquals(calculation.calculate(), 3.0,
"Conditional calculation value");
}
}
Aby testy się wykonywały, muszę jeszcze utworzyć plik testng.xml, wskazujący właściwy pakiet:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd">
<suite name="AspectJ" verbose="2">
<test name="Conditional">
<packages>
<package name="pl.info.czerwinski.conditional"/>
</packages>
</test>
</suite>
W tej chwili wywołanie mvn test powinno zakończyć się jedną porażką przy dwóch wykonanych testach. Nieprawidłowy rezultat wystąpi w metodzie testCalculateParameterized, gdzie zamiast oczekiwanej wartości 3.0 pojawi się 1.0.
Pora przejść do najważniejszego elementu całego przykładu, czyli do aspektu. Pojawia się tu kilka nowości:
@Aspect
public class ConditioningAspect {
@Around("call(@Conditional * *.*(..)) && @annotation(conditional)")
public Object condition(
ProceedingJoinPoint joinPoint,
Conditional conditional) throws Throwable {
String parameterName = conditional.value();
if (Boolean.parseBoolean(parameters.getString(parameterName))) {
return joinPoint.proceed();
}
return null;
}
private ResourceBundle parameters =
ResourceBundle.getBundle("parameters");
}
Poprzednio omówiłem adnotacje @Before oraz @After, wskazujące rady przed i po. Adnotacja @Around służy to utworzenia rady „otaczającej” punkt przecięcia, czyli takiej, która kontroluje dalsze wykonanie programu. Standardowo metoda taka pobiera parametr klasy ProceedingJoinPoint i zwraca dowolny obiekt. Aby rada pozostała bez wpływu na wykonanie programu, powinna składać się z jednej instrukcji:
return joinPoint.proceed();
Innymi słowy, brak tej instrukcji oznacza, że metoda, której rada dotyczy, nie zostanie w ogóle wykonana.
Definicja punktu przecięcia w przykładzie jest dość złożona:
call(@Conditional * *.*(..)) && @annotation(conditional)
Powyższy zapis oznacza wywołanie dowolnej metody oznaczonej adnotacją @Conditional. Rada przyjmuje tę adnotację jako parametr conditional, dzięki czemu można uzależnić dalsze działanie programu od atrybutu value.
Po dodaniu aspektu, wszystkie testy powinny zakończyć się sukcesem.
Opisane rozwiązanie daje możliwość szybkiego i łatwego warunkowania kolejnych metod, z zachowaniem czytelności kodu.
Przypuszczam, że do zaproponowanego przeze mnie aspektu można wprowadzić dalsze poprawki (czekam na konstruktywne komentarze). Mam tylko nadzieję, że duża ilość kodu nie zmniejszy czytelności niniejszego wpisu.




1 Trackback(s)