Translate »
  • English
  • German
  • Polish

Kiedy wyjątek to za dużo, a null jest złem, czyli opcje w Guava

W poprzednim wpisie dotyczącym Guavy omówiliśmy klasę Preconditions, która mówiąc najprościej dostarcza zamienników dla często występujących idiomów, które służą do “twardego” sprawdzania parametrów metody. “Twarde” oznacza, że w przypadku niespełnienia warunku jest rzucany wyjątek.

Dziś przyjrzymy się sytuacji gdzie pomimo braku parametru, wartość null, możemy kontynuować pracę ponieważ potrafimy sobie poradzić z takim brakiem. Nie jest to sytuacja rzadka czy nietypowa. Bardzo często w przypadku braku parametru tworzymy kod w rodzaju:

Listing 1. Kod wykorzystujący wartości domyślne

public void method(Object p1){
    if(p1 == null)
       p1 = DEFAULT_VALUE;
    //...
}

Co więcej, miejsc w kodzie, gdzie powstają takie konstrukcje jest zazwyczaj od groma i ciut ciut. Szczególnie często występują one tam gdzie korzystamy ze wzorców “łańcuchowych” (Chain of Constructors/Methods) wołając coraz to kolejne wersje i przekazując im null. Co zabawne w ostatniej, najbardziej rozbudowanej wersji metody zazwyczaj następuje ifologia zastępująca null wartościami domyślnymi. W sumie i tak ta ifologia musi się tam znajdować zatem nie ma tu lepszego rozwiązania… no może poza przekazywaniem wartości domyślnych ze wcześniejszych wywołań i tym samym jakiejś tam pseudo optymalizacji omijającej w takim przypadku nieużywane IF-y… nieee…. fuuj…

Klasa Optional pozwala na pozbycie się tego jakże szkodliwego kodu jednocześnie dostarczając metodę (jedną biedaczkę), która w razie czego zachowa się jak Preconditions.chechNotNull i walnie błędem.
Samo działanie klasy, w tej fajniejszej wersji, przypomina trochę to jak działa Option ze Scali, ale jak to w czystej javie bywa jest z nią więcej pieprzenia.

Tworzenie instancji Optional

Na zdrowy chłopski rozum klasa ta powinna dawać możliwość stworzenia się w dwóch wersjach. Pierwszej, która powstanie gdy przekażemy obiekt, który nie jest null i w drugiej dla null. I generalnie to tak działa. Klasa Optional jest jako taka klasą abstrakcyjną i posiada dwie podklasy – Present dla istniejących obiektów i Absent, która jest singletonem reprezentującym wartości null.

Proces tworzenia obiektu może zatem przebiegać bardzo prosto:

Listing 2. Tworzymy obiekt Optional

public Integer forceNPE(Integer param) {          
	Optional<Integer> oParam = of(param);   
	return oParam.get();
}

Taka konstrukcja tworzy pod spodem obiekt klasy Present, która może zostać użyta w dalszym kodzie. Rzecz w tym, że uruchomienie kodu takiego jak ten:

Listing 3. Tworzymy obiekt Optional i mamy NPE

@Test(expectedExceptions = NullPointerException.class)
public void testForceNPEThrowNPE() {                  
	assertThat(oe.forceNPE(null)).isEqualTo(5);          
}

ujawnia pewien mankament. Jak wspomniałem wcześniej klasa posiada jedną metodę, która zachowuje się jak Prconditions.chechNotNull i jest to właśnie metoda tworząca of. Czyli cały nasz plan można wywalić, bo po co Optional skoro mamy Preconditions.
Nie załamujmy się jednak. Wbrew pozorom to nie metoda or jest najważniejsza. Znacznie ważniejsza jest metoda fromNullable.
Zamieńmy nasz kod na następujący:

Listing 4. Tworzymy obiekt Optional drugie podejście

public Integer supressNPE(Integer param) {         
	Optional<Integer> oParam = fromNullable(param);   
	return oParam.or(DEFAULT_INT);                    
}

Jeżeli teraz odpalimy proste testy:

Listing 5. Tworzymy obiekt Optional i nie ma NPE

@Test                                                                       
public void testSupressNPE() throws Exception {                             
	assertThat(oe.supressNPE(5)).isEqualTo(5);                                 
	assertThat(oe.supressNPE(null)).isEqualTo(OptionalExamples.DEFAULT_INT);   
}

to naszym oczom ukaże się zielony pasek. Jest zatem całkiem nieźle. Użycie metody fromNullable powoduje, że biblioteka wybiera w zależności od parametru jaki dokładnie rodzaj Optional nam dostarczyć. Następnie wywołując metodę or może zajść jedna z dwóch ścieżek. Pierwsza to ta gdy oryginalny obiekt jest null, wtedy to po sprawdzeniu czy zastępnik też nie jest or zostaje on zwrócony. Druga to ta gdy oryginalny obiekt nie jest null. Wtedy metoda or zachowuje się jak inna metoda, get i zwraca oryginalny obiekt.

Na chwilę obecną klasa Optional nie przedstawia nam się zbyt zachęcająco. W sumie jej “wartość dodana” wynikająca z dołączenia jej do kodu jest ograniczona do eliminacji IF-ów. Przyjrzyjmy się jednak co więcej zawiera ta klasa, czyli czas na klasyczne omówienie API.

Metoda absent

Metoda statyczna zwracająca instancję klasy Absent. Nic ciekawego, ale zapewne znajdzie się dla niej zastosowanie wraz ze wzorcem null object.

Metoda asSet

Po drodze oryginalny obiekt jest opakowywany w niezmiennego Seta. Jeżeli przekażemy null to w efekcie otrzymamy pusty, niezmienny Set. Niby mało ciekawe, ale jeżeli zastanowić się nad tym dokładniej to w ten sposób otrzymujemy bardzo elegancki mechanizm do eliminacji efektów ubocznych w funkcjach, które stosujemy na zbiorach albo zwracających zbiory.

Metody equals, hashCode i toString

Metody te są zaimplementowane w intuicyjny sposób. Pierwsza z nich wywołuje metodę equals na referencjach do oryginalnych obiektów (o ile oczywiście trafi do niej Optional, a tak na prawdę Present). Druga wywołuje hashCode oryginalnego obiektu “domieszkując” go o swoje przesunięcie. Trzecia z metod też domieszkuje oryginalne wywołanie o informację, że jest to jednak Optional.

Metoda fromNullable

W zależności od przekazanego argumentu zwraca Present jeżeli nie jest on null albo Absent w przeciwnym wypadku.

Metoda get

Metoda ta zwraca referencję do obiektu. Jeżeli zostanie wywołana na obiekcie klasy Absent to rzuci IllegalStateException.

Metoda isPresent

Zwraca true…. a zresztą czy trzeba tłumaczyć?

Metoda of

Jeżeli jako argument przekażemy jej istniejący obiekt to otrzymamy Optional. Przekazując null otrzymamy NullPointerException. Metodę tą omówiłem już wcześniej. Generalnie może wprowadzić trochę zamieszania, bo zamiast eleganckiej obsługi null wali wyjątkiem.

Metoda or

Metoda ta występuje w trzech wersjach. Z pierwszą, przyjmującą domyślna wartość, już się spotkaliśmy. Druga wersja przyjmuje jako parametr inny obiekt Optional i ją zwraca. Trzecia wersja metody przyjmuje jako parametr Supplier i zwraca jego produkt.
Oczywiście dotyczy to tylko null. W przypadku normalnych obiektów zostanie zwrócony oryginał.

Metoda orNull

Metoda zwróci dokładnie to co jej przekażemy jako argument. Jak przekażemy null to dostaniemy null.

Metoda presentInstances

Parametrem tej metody jest coś po czym można się iterować. Z oryginalnego zbioru zostaną usunięte wszystkie wystąpienia obiektów Abasent. Zwrócony zbiór będzie zawierać tylko te wartości, które nie są wartościami domyślnymi.

Metoda transform

Jako argument przyjmuje funkcję, która o ile istnieje oryginalny obiekt, zostanie zaaplikowana i zwróci Optional przechowujący wynik jej działania. Jeżeli mamy do czynienia z null zostanie zwrócony Absent.

Podsumowanie

Przedstawione tu rozwiązanie nie jest najlepsze. Pozwala jednak na pójście “trzecią drogą” gdzieś pomiędzy wyrzucaniem wyjątku, a ręczną obsługą wartości null. Doskonale nadaje się też jako wsparcie w programowaniu funkcyjnym gdy trzeba jakoś eliminować niepożądane efekty uboczne wynikające z przekazywania do funkcji i predykatów wartości null.

4 Responses to “Kiedy wyjątek to za dużo, a null jest złem, czyli opcje w Guava”

  1. Nowaker Says:

    To takie w sumie typowo Javowe, żeby komplikować naprawdę proste rzeczy. Prosty if na samym początku metody jest wystarczająco zrozumiały, choć jednak trochę rozlazły. Idealnym więc rozwiązaniem jest ObjectUtils.defaultIfNull(param, defaultValue) z Commons Lang lub własna metoda we własnym utilsie. Ale oczywiście świat Javy tak bardzo podniecił się Optionalami, że w Javie Ósmej to będzie już standard. Tymczasem w innych językach domyślne wartości parametrów już są (C#) albo będą za chwilę (JavaScript). I jak tu nie wierzyć, że Java zostanie następnym Cobolem. ;-)

  2. Koziolek Says:

    Własna metoda jest też rozwiązaniem, ale musimy ją przetestować. Co już dorzuca nam trochę pracy. Później takie utilsy gdzieś się zapodzieją i znowu coś tam piszemy.
    Nie twierdzę, że Optional są the best, bo tak nie jest, ale nie można też podchodzić do nich jak do COBOLa. Java ma, jako język, trochę wad i takie bądź konkurencyjne rozwiązania ułatwiają życie.

  3. airborn Says:

    Ciut się chyba w opisie orNull() machnąłeś. Tzn. intencje miałeś dobre, ale z tych słów których użyłeś wynika, że zwrócone zostanie to co przekażemy jako argument do notNull (który jest przecie bezargumentowy), a nie obiekt przekazany do Present.

  4. Marcin Kubala Says:

    @Nowaker Jeśli na Optional składałby się tylko isPresent oraz get, to podzielałbym twoje wątpliwości co do sensu jego istnienia.

    Domyślne wartości mogą być ok, pod warunkiem, że jesteś w stanie:
    – wprowadzić NullObjecty dla każdego typu który będzie używany w postaci opcjonalnego argumentu (rozpuchnięcie hierarchii klas, konieczność każdorazowej implementacji interfejsu),
    lub
    – jednoznacznie wyznaczyć domyślną wartość, jedną pasującą dla wszystkich przypadków użycia metody.

    Ponieważ w Javie jak sam zauważyłeś brakuje m.in. domyślnych wartości argumentów, trzeba będzie dodatkowo posiłkować się checknullem do podstawienia NullObjectu/jakiegoś defaulta.
    Mamy proste rozwiązanie, ale jednak tego if’a ze sprawdzaniem nulla będziesz musiał wielokrotnie powielać.. Także w językach gdzie defaulty dla argumentów są obsługiwane (C#, Scala i pewnie cała masa innych), gdy nie jesteś w stanie wyznaczyć ich ‘statycznie’, tylko dopiero w runtime.
    Raz zapomnisz i prędzej czy później masz wyjątek w runtime. Niedobrze.

    Jawny typ nie tylko dokumentuje (na poziomie interfejsu) opcjonalność argumentu/pola/etc. ale też zaprzęga system typów na etapie kompilacji do wyłapywania miejsc, gdzie bazując na checknullach dostałbyś NPE (mimo wszystko wciąż pozostaje możliwość, że ktoś zamiast Optional.absent() przekaże nulla..).

    Z Optional zyskujemy propagację i pewnego rodzaju kontrakt, egzekwowany przez systemu typów – jeśli jest metoda, która przyjmuje typ T i chcesz ją wywołać z poziomu metody przyjmującej Optional, to musisz albo wywołać ją z poziomu Optional.transform(), albo użyć Optional.or(..) i dostarczyć defaulta dla konkretnego przypadku.
    To nam pozwala zrobić małe odwrócenie sterowania dla polityki obchodzenia się z nullami.

    IMHO te 2 aspekty @Koziolek chyba przeoczył.

    Używając transform możesz zacząć budować flow oparty na potokach funkcji które zawsze otrzymują jakąś nie-pustą wartość, delegując odpowiedzialność za obsługę nulli na zewnątrz.

    Jeśli chodzi o wady Optional – to przede wszystkim straszne verbosity przy braku wyrażeń lambda / funkcji jako obiektów pierwszej kategorii (jak w JS czy Scali). Do tego type erasure na JVM może nas doprowadzić do łez, gdy zachce nam się przeciążać metody..

    Czy wspomniane korzyści są warte dodatkowej warstwy abstrakcji?
    Moim zdaniem tak, biorąc pod uwagę, że częściej czyta się cudzy/stary własny kod niż JavaDoci (patrz typy jako dokumentacja interfejsu) oraz to, że zawsze znajdują się ciekawsze rzeczy do roboty niż pisanie test-caseów sprawdzających czy coś się nie wywali na nullach ;)

    W sumie to tutaj chyba rozchodzi się nie tyle o samo Optional, ale raczej o “KISS” vs. “DRY, SRP i czerpanie z silnego typowania Javy”?

Leave a Reply