W każdym języku programowania jednym z ważniejszych wzorców jest sprawdzanie w metodzie warunków wejściowych. Nazywa się to „fail fast”, czyli możliwie szybkie i czytelne zwrócenie błędu przed wykonaniem właściwego ciała metody. Dzięki temu łatwiej się taki wyjątek analizuje i poprawia błąd.
W samym języku Java oraz z wykorzystaniem popularnych bibliotek można to zrobić na kilka sposobów. Przedstawie to na prostym przykładzie sprawdzenia czy argument liczbowy jest większy od 0.
1. Sprawdzenie warunku i rzucenie wyjątku
private void checkByExceptions(int arg1) { if(arg1 <= 0) { throw new IllegalArgumentException( "arg1 should be > 0, is: " + arg1); } }
Metoda znana od początku języka Java: sami sprawdzamy warunek i rzucamy wyjątek: IllegalArgumentException, IllegalStateException, itp.
Exception in thread "main" java.lang.IllegalArgumentException: arg1 should be > 0, is: -1 at checks.SimpleApp.checkByExceptions(SimpleApp.java:18) at checks.SimpleApp.main(SimpleApp.java:10)
Plusy:
– zawsze działa
Minusy:
– trzy linijki kodu zamiast jednej
– konieczne sklejanie napisów żeby komunikat zawierał dane
2. Asercje
private void checkByAssertions(int arg1) { assert arg1 > 0 : "arg1 should be > 0, is: " + arg1; }
Wprowadzone w Javie 1.4 dało ładny i czytelny zapis. Jednak rzucany obiekt to AssertionError który dziedziczy z Error, co jest mało „koszerne”. Przez długi czas nie przeszkadzało, jednak po prowadzeniu statycznej analizy kodu jest to wykazywane jako błąd i ciężko z tym dyskutować.
Drugi problem to fakt że asercje wymagają włączenia w JVM zmiennej -enableassertions (-ea), co łatwo zapewnić w serwerach aplikacyjnych ale wielokrotnie zdarza się zapomnieć przy uruchamianiu standalone’wych aplikacji czy testów, np. JUnit. A brak tej flagi sprawia że kod wykonuje się dalej i prowadzi do trudno analizowanych problemów, co powoduje frustracje.
Trzeci problem, związany z pierwszym, to obsługa takich błędów w testach jednostkowych – nieładnie jest łapać Throwable (klasa bazowa dla Exception i Error).
Exception in thread "main" java.lang.AssertionError: arg1 should be > 0, is: -1 at checks.SimpleApp.checkByAssertions(SimpleApp.java:24) at checks.SimpleApp.main(SimpleApp.java:11)
Plusy:
– zwięzły zapis
– działa bez dodatkowych bibliotek
– konieczne sklejanie napisów żeby komunikat zawierał dane
Minusy:
– rzuca Error a nie Exception
– wymaga ustawienia flagi -enableassertions
3. Biblioteka Apache commons-lang3
private void checkByApacheCommonsValidate(int arg1) { Validate.isTrue(arg1 > 0, "arg1 should be > 0, is: %s", arg1); }
Wykorzystując metody statyczne z klasy Validate z biblioteki commons-lang można uzyskać zwięzły zapis oraz umieszczać parametry w opisie problemu.
Exception in thread "main" java.lang.IllegalArgumentException: arg1 should be > 0, is: -1 at org.apache.commons.lang3.Validate.isTrue(Validate.java:106) at checks.SimpleApp.checkByApacheCommonsValidate(SimpleApp.java:28) at checks.SimpleApp.main(SimpleApp.java:12)
Plusy:
– zwięzły zapis
– możliwość użycia parametrów w opisie problemu
– rzuca wyjątki dziedziczące z Exception
Minusy:
– wymaga dodatkowej biblioteki
4. Biblioteka Google Guava
private void checkByGuavaPreconditions(int arg1) { Preconditions.checkArgument(arg1 > 0, "arg1 should be > 0, is: %s", arg1); }
Biblioteka o funkcji analogicznej co commons-lang, tylko bardziej „trendy”.
Exception in thread "main" java.lang.IllegalArgumentException: arg1 should be > 0, is: -1 at com.google.common.base.Preconditions.checkArgument(Preconditions.java:145) at checks.SimpleApp.checkByGuavaPreconditions(SimpleApp.java:32) at checks.SimpleApp.main(SimpleApp.java:13)
Plusy:
– zwięzły zapis
– możliwość użycia parametrów w opisie problemu
– rzuca wyjątki dziedziczące z Exception
Minusy:
– wymaga dodatkowej biblioteki
Podsumowanie
Jeżeli już używamy Guavy do innych celów, to powinniśmy z dnia na dzień zacząć używać Preconditions. Jeżeli nie, to powinniśmy zastanowić się czy jej nie zacząć używać :-).
Migracja z asercji do Google Guava
Najprościej:
- sed’em albo w swoim IDE zastąpić napis „assert (.*):(.*);” na „Preconditions.checkArgument($1, $2);”
- sed’em albo w swoim IDE zastąpić napis „assert (.*);” na
„Preconditions.checkArgument($1);” - przejść się po klasach które przestały się kompilować i uzupełnić import klasy: „import com.google.common.base.Preconditions;”
- sprawdzić pozostałe wystąpienia słowa „assert ” i zastąpić na „Preconditions.checkArgument(” – ten problem występuje w wystąpieniach wieloliniowych.
Przy wykorzystywaniu Guavy (commons-lang zresztą też) należy się w każdym wypadku zastanowić jaką klasę wyjątku chcemy rzucać, bo dostępnych jest kilka:
- Preconditions.checkArgument -> IllegalArgumentException – rzucamy w sytuacji gdy argument jest niezgodny z oczekiwanym
- Preconditions.checkState -> IllegalStateException – rzucamy w sytuacji gdy warunek oznacza wejście w nieobsługiwany przypadek
- Preconditions.checkNotNull -> NullPointerException – rzucamy w sytuacji gdy zmienna okaże się nullowa
- Preconditions.checkElementIndex -> IndexOutOfBoundsException – rzucamy gdybyśmy próbowali odwołać się do nieistniejącego typu tablicy.
W praktyce powinniśmy używać tylko tych dwóch pierwszych metod.