Optional jako pole i co mi zrobisz?

Od wprowadzenia klasy Optional w JDK 8 minęło już sporo czasu. Sporo też napisano o tym jaki i gdzie jej używać, co jest dobrą praktyką a co złą. Przykład tego można znaleźć w artykule Optional Anti-Patterns. Ale czy to wszystko ma sens?

Zgadzam się z wieloma postawionymi tam tezami. Chciałem jednak wejść w polemikę z zakazem używania Optional jako pola klasy i przekazywaniu ich jako parametrów konstruktora. Optional nie chroni nas przez NullPointerException, nic nie chroni nas przed głupotą, bo Java pozwala niezależnie od sygnatury zwrócić null. Tak więc Optional jest przydatny tylko w połączeniu ze spójną konwencją obejmującą cały projekt. Ta konwencja sprawia, że aby wiedzieć czy musimy zatroszczyć się o nulle nie musimy przeglądać kodu wywoływanej metody, czytać komentarzy lub dokumentacji. Jeśli widzimy, że w sygnaturze metody nie ma Optional możemy założyć, że metoda zawsze zwróci wartość, w przeciwnym razie musimy zatroszczyć się o brak wartości. Pięknie!

Optional jako pole klasy

W takim razie dlaczego nie użyć Optional jako pola klasy? Jest kilka podawanych argumentów. Po pierwsze IntelliJ IDEA sugeruje, że nie powinieneś tego robić, jest to też powszechną wykładnią.

Komunikat ten wskazuje nam drugi powód – Optional nie jest Serializable. Ten argument mnie osobiście nie przekonuje, ponieważ nie przypominam sobie sytuacji z ostatnich projektów w której potrzebowałbym implementować interfejs Serializable. Ponadto pisząc o Optionalu mam na myśli koncept, a nie konkretną implementację. W sytuacji, gdy potrzebujesz implementować interfejs Serializable, możesz użyć alternatywnej wersji z jakiejś biblioteki, jak na przykład Option z biblioteki vavr. Trzecim argumentem jest to, że “nie po to Optional został stworzony”. Komputer też nie został stworzony do rozrywki, a zechciej to powiedzieć milionom ludzi spędzających codziennie kilka godzin na graniu w gry komputerowe. Po czwarte spadek wydajności związany z opakowywaniem pól w Optional. Jednak we wszystkich projektach, które rozwijałem tego typu narzut nie były problemem, a optymalizacje związane z wywalaniem Optionala byłyby na szarym końcu listy wąskich gardeł. Piąty argument, jaki znalazłem, to spostrzeżenie, że w obiektach DTO słabo wygląda coś takiego

private Optional<Hashmap <String, Integer>> data;

Prawdopodobnie pojawia się to jeśli obiekty są zmienne i mają settery. Gdy tworzymy DTO jako obiekty niezmienne, w których wszystkie pola są ustawiane w konstruktorze, to tego typu sytuacje będą występować zdecydowanie rzadziej. Natomiast gdy już wystąpią, będzie to miało jakieś uzasadnienie semantyczne, gdy czym innym jest pusta mapa, a czym innym brak mapy. Niezależnie, jeśli moje pole jest typu Optional, to nie muszę przy nim stawiać żadnych adnotacji, komentarzy lub innego znacznika nullowalności pola. Odwołując się do pola od razu wiem czy muszę obsłużyć null’e. Jet to aa sama, dobrze sprawdzająca się zasada, co przy odwoływaniu się do metod publicznych.

Optional jako argument metody

No dobrze, a co z drugim tematem, czyli zakazem użycia Optionali w parametrach metod? Przyjmijmy, że mamy następującą klasę Client

class Client {
  String getFirstName() { ... }
  Optional<String> getSecondName() { ... }
  String getLastName() { ... }
  ...
}

Co, jeśli chcemy przepisać tą klasę w inną? Po cóż mielibyśmy to robić? Otóż jeśli troszczymy się o izolacje modułów to, żeby ograniczyć ich sprzężenie a w szczególności żeby nie było cykli, przepisywanie jednych klas w drugie jest dość częstą praktyką. Podobnie jeśli robimy fasadę na jakieś API. Oczywiście zazwyczaj nie przepisujemy wszystkiego jeden do jeden. Czasem pomijamy jakieś pola, bierzemy wartości z kilku obiektów lub robimy jakąś transformację. W tym przypadku, aby nie komplikować przykładu, ograniczę się do prostego przeniesienia pól.

Mam nadzieję, że teraz wszyscy widzą zasadność przepisywania danych z jednej klasy do drugiej. W takim razie jak będzie to wyglądało, gdy nie ma optionali w argumentach? Wyjścia są dwa. Najpierw możemy spróbować przekazać null:

new User(client.getFirstName(), 
         client.getSecondName().orElse(null),
         client.getLastName())

Ten orElse(null) wygląda dla mnie strasznie! Taki potwór może w nocy się przyśnić. Drugim podejściem może być przygotowanie dwóch konstruktorów:

if (client.getSecondName().isPresent()) {
  new User(client.getFirstName(),
           client.getSecondName().get(),
           client.getLastName())
} else {
  new User(client.getFirstName(), client.getLastName())
}

lub w zapisie płynącego API bez Optional::get():

client.getSecondName()
  .map(secoundName -> 
       new User(client.getFirstName(),
                secoundName,
                client.getLastName()))
  .orElse(new User(client.getFirstName(), client.getLastName()))

Niezależnie od formy zapisu oba podejścia straszą. W takim razie spróbujmy nie rezygnować z Optional w argumentach metod, jak by to wyglądało?

new User(client.getFirstName(),
         client.getSecondName(),
         client.getLastName())

Nie mam wątpliwości, że to wygląda najlepiej.

Przekaz na dziś

Na koniec pojawia się pytanie czy powinienem użyć Optionali wszędzie? Użyj ich wszędzie, gdzie to ma sens. Jeśli hierarchia klas ma sens to zrób hierarchię, ale nie rób jej tylko dlatego, że nie chcesz mieć pola typu Optional. Jeśli dwa konstruktory sprawią, że API klasy będzie przyjemniejsze w użyciu, to zrób dwa konstruktory, ale nie rób ich tylko dlatego, że nie chcesz mieć argumentu typu Optional. Nie łykaj haseł “tu tak, a tam nie” bez zastanowienia. Miej twarde argumenty dlaczego Optional w konkretnym miejscu nie powinien wystąpić. Dla mnie konwencja, że typ danych zawsze niesie ze sobą informację o braku wartości jest czystą, spójną konwencją, która minimalizuje wystąpienie błędów związanych z NullPointerException.


Autor Tomek Fijałkowski

Tomek jest pasjonatem zwinnych metodyk, TDD i DDD. W swojej karierze pełnił przeróżne role. Był programistą, scrum masterem, liderem, architektem, managerem. Aktualnie wrócił do tego co lubi najbardziej i jest programistą w point72.

3 thoughts on “Optional jako pole i co mi zrobisz?

  1. Odgrzewany kartofel w dodatku z tragicznym rozumowaniem.
    Próba uzasadnienia Optional<Hashmap > seriously?
    Konstruktor z Optional’ami? Seriously?
    Autor moze i starszy programista w Allegro ale o prawdziwym programowaniu Obiektowym za wiele pojecia chyba nie ma. Ciekaw jestem jak zareagowali na to „coworkersi” autora np. Tomasz Nurkiewicz czy Jakub Nabrdalik …

  2. A ja się zgadzam totalnie. Reguły są dobrym punktem wyjścia do dyskusji, ale zawsze trzeba je aplikować pragmatycznie.
    Argumenty w stylu „Optional w api jest zły bo jest zły” sugerują skupianie się na mało ważnych detalach podczas gdy architektura całej aplikacji pewnie cierpi na dużo poważniejsze przypadłości

Leave a Reply