Strażnicy, czyli coś czego mi brakuje w Javie
W sumie brakuje mi dobrej implementacji tego rozwiązania, bo można taką funkcjonalność mieć z wykorzystaniem np. AspectJ czy Type Annotations. Jednak o co chodzi?
Guardians of Erlang
Erlang posiada bardzo fajną konstrukcję zwaną strażnikami (guardians). Cóż to jest? Otóż mając jakąś funkcję możemy walidować argumenty w deklaratywny sposób. Nie wywołujemy żadnego wywołania czy też nie odwołujemy się do żadnej dodatkowej biblioteki. Całość opiera się na konstrukcjach języka.
Przykładowo funkcja attack\_mod przyjmuje argument w postaci krotki, której drugi element musi być większy bądź równy 0. W Javie taki kod wyglądał by następująco:
Listing 1. Metoda attackMod
public int attackMod(UnitType type, int numberOf){
checkArgument(numberOf>=0, "Number of unit must be >= 0");
//...
}
Kod ten wykorzystuje klasę Preconditions z Guavy. Inną metodą w javie jest napisanie aspektu i użycie adnotacji @Min
Listing 2. Metoda attackMod z adnotacją
public int attackMod(UnitType type, @Min(0) int numberOf){
//...
}
Ten kod już dużo bardziej przypomina to co chcemy osiągnąć. Jest deklaratywny i stworzony w oparciu o meta dane. Problemem będzie uruchomienie go. Mianowicie trzeba napisać sobie aspekt w rodzaju @Around(„execution(public \* \* (.., @Min (\*), ..))”) oraz wpiąć go w trakcie uruchamiania.
Strasznie to upierdliwe, a w dodatku nie tak mocarne jak rozwiązanie w Erlangu. Tu implementacja będzie wyglądać następująco:
Listing 3. Funkcja attack\_mod
attack_mod({Unit_name, Number_of}) when Number_of >=0 ->
%%% ...
.
Strażnikiem nazywamy ciąg pomiędzy słowem when, a strzałką. Dlaczego rozwiązanie to jest znacznie fajniejsze niż te dostępne w Javie.
Strażnicy i dopasowania
Chyba najciekawszą cechą strażników jest możliwość ich wykorzystania w dopasowaniach (pattern matching). Wyobraźmy sobie, że o ile warunek nie mniejszy niż 0 będzie ok, dla wszystkich rodzajów UnitType, to już w zależności od rodzaju jednostki i liczebności inaczej będzie wyliczany modyfikator. Przykładowo niech orkowie jak będzie ich co najmniej 20 mają wyższy modyfikator (cecha „w kupie siła”), ale jednocześnie istnieje 10% szansy na to, że pokłócą się i modyfikator będzie wynosił 0 (cecha „Moja racja jest najmojsza”).
Wprowadzamy tu zatem dwa nowe elementy. Pierwszym jest dodatkowy zakres wartości, a drugim zmiana algorytmu w zależności od tego w którym zakresie wylądowaliśmy. Wyobraźmy sobie jak ten kod mógłby wyglądać w Javie. W najprostszym niezbyt obiektowym wykonaniu wyglądało by to mniej więcej tak:
Listing 4. Metoda attackMod po modyfikacjach
public int attackMod(UnitType type, int numberOf) {
checkArgument(numberOf >= 0);
switch (type) {
case Marine:
return 2 * numberOf;
break;
case Ork:
if (numberOf >= 20) {
return (int) (1.5 * numberOf * quarrel());
}
return (int) (1.3 * numberOf);
break;
case Human:
return 1 * numberOf;
break;
}
}
Metoda quarrel zwraca 0 albo 1 w zależności od warunku losowego. Strasznie to syfne. W wersji bardziej obiektowej i bardziej javowej switch zastąpiony by został mapą, w której kluczami były by wartości enuma, a wartościami fabryki (wzorzec ProblemFactory) zwracające odpowiedni algorytm (strategię) w zależności od parametru numberOf. Z lekka masakra. Osobiście nie mam nic przeciwko tworzeniu dużej ilości wyspecjalizowanych klas. Java to język taki, a nie inny i ma swoje „odchyły”, a jednym z nich jest konieczność tworzenia specjalizowanego kodu by uzyskać dużą elastyczność. Popatrzmy jak problem można rozwiązać w Erlangu.
Listing 5. Funkcja attack\_mod
-export([attack_mod/1]).
attack_mod({Unit_name, Number_of}) when Number_of >=0 ->
attack_mod(Unit_name, Number_of).
attack_mod(marine, Number_of) ->
2 * Number_of;
attack_mod(ork, Number_of) when Number_of
1.3 * Number_of;
attack_mod(ork, Number_of) when Number_of >= 20 ->
1.5 * Number_of * quarrel();
attack_mod(human, Number_of) ->
1 * Number_of.
quarrel() ->
Chns = random:uniform(10),
if Chns == 1 -> 0;
Chns > 1 -> 1
end.
Chyba wygląda to trochę lepiej? W tym przypadku VM-ka erlanga sama dopasuje odpowiednią wersję funkcji do argumentów. Jest to zdecydowanie lepsze rozwiązanie niż to w Javie.
Oddziały strażników
Czyli rzecz o tym jak składać warunki. Jeżeli chcemy nałożyć więcej niż jedno ograniczenie na nasze argumenty możemy połączyć strażników. W takim przypadku przecinek będzie działać jak logiczne AND, a średnik jak logiczne OR.
Więcej niż deklaracja funkcji
Strażników możemy używać nie tylko na poziomie deklaracji funkcji. Znacznie przyjemniej używa się ich na poziomie konstrukcji case, która jest podobna do tej ze Scali i służy do obsługi dopasowań. W takim wypadku powyższy kod można przerobić na coś takiego:
Listing 6. Funkcja attack\_mod z wykorzystaniem case
-export([attack_mod/2]).
attack_mod(Unit_name, Number_of) when Number_of >= 0 ->
Mod = case Unit_name of
marine -> 2;
ork when Number_of 1.3;
ork when Number_of >= 20 ->
1.5 * Number_of * quarrel();
human -> 1
end,
Mod * Number_of.
quarrel() ->
Chns = random:uniform(10),
if Chns == 1 -> 0;
Chns > 1 -> 1
end.
Całkiem ładne rozwiązanie. W dodatku bardziej zwięzłe niż to w Javie.
Podsumowanie
W Javie brakuje niestety takiego fajnego mechanizmu. Dodatkowym atutem Erlanga jest możliwość użycia atomów, które dodatkowo wprowadzają dodatkowe możliwości w przypadku dopasowań. Pozostając jednak przy deklaratywnym weryfikowaniu parametrów warto zapoznać się z możliwościami Javy w tym zakresie, bo choć użycie aspektów jest upierdliwe, to jest to całkiem efektywny sposób na pozbycie się nadmiarowego kodu.
Jeżeli podobał ci się ten wpis podziel się nim ze znajomymi korzystając z przycisków poniżej.