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 < 20 ->
  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 < 20 -> 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.

5 myśli na temat “Strażnicy, czyli coś czego mi brakuje w Javie

  1. Oprócz preconditions i aspektów pozostają jeszcze kontrakty. O ile składnia zawsze jest trochę nienaturalna (np. całość opisana w komentarzu), to już takia https://github.com/nhatminhle/cofoja wygląda trochę lepiej. To oczywiście wciąż daleko od tego co jest w Erlangu, ale stanowi pewną alternatywę dla aspektów.

  2. Też fajne rozwiązanie, ale jak widzę korzysta z asm-a co też jest pewnego rodzaju hakiem. Trochę nawet bardziej 🙂 Ale rozwiązanie warte obadania.

  3. W erlangu teraz kodzisz?
    Jeśli chodzi o rozwiązanie problemu w Erlangu to można powiedzieć, że Strażnicy są pewnego rodzaju wbudowanym wzorcem Strategia w mechanizm języka?

  4. W erlangu to ja się dopiero uczę 🙂 Co do samego wzorca strategii to raczej nie. Można ich co prawda użyć jako mechanizmu wyboru strategii gdyż mogą uczestniczyć w pattern matchingu, ale ich głównym zadaniem jest zapewnienie walidacji argumentów.

Napisz odpowiedź

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *

To create code blocks or other preformatted text, indent by four spaces:

    This will be displayed in a monospaced font. The first four 
    spaces will be stripped off, but all other whitespace
    will be preserved.
    
    Markdown is turned off in code blocks:
     [This is not a link](http://example.com)

To create not a block, but an inline code span, use backticks:

Here is some inline `code`.

For more help see http://daringfireball.net/projects/markdown/syntax