Blog: Jezyki programowania i srodowiska programistyczne | Java

Optionals – antywzorce

Optionals – antywzorce
  • 1 004 views

Dzień pracy programisty(-tki) obfituje w pułapki i zagrożenia. Gdy zapomnimy zablokować komputer, może się okazać, że niechcący zaprosiliśmy cały zespół na pączki, nieoczekiwanie przegraliśmy grę lub spowodowaliśmy krytyczny błąd w aplikacji, która w dodatku w logach pokaże nam NullPointerException (NPE).

Udostępnij!

API w Javie 8 nie podarowało nam narzędzi do radzenia sobie z pączkami albo z przegraną w grę (trzymajmy kciuki za kolejną wersję Javy), ale dostarczyło sposób na radzenie sobie z NPE – klasę Optional.

Dostarczone rozwiązanie, zainspirowane Google Guava, ułatwia życie programisty(-tki) i w znacznym stopniu eliminuje ryzyko wystąpienia NPE.  Drobne potknięcia w projektowaniu API klasy Optional w połączeniu z nieumiejętnym jego użyciem mogą jednak prowadzić do nieprzewidzianych rezultatów, z NPE włącznie.

Co może pójść nie tak?
1. Nadużywanie nowego API.
2. Niepoprawne użycie jeszcze nieznanego API.

Nadużywanie nowego API

Kupując nowy gadżet, zazwyczaj  szukamy okazji aby go użyć. Tak jak po zakupie nowego telefonu zastanawiamy się jak mogliśmy żyć do tej pory bez wszystkich tych funkcjonalności, tak samo z nowym API – stary kod wyda się passé, a nasz mózg zacznie rozglądać się za kolejnymi okazjami do używania najnowszych funkcjonalności języka.

Jak sobie radzić z tym problemem? Przede wszystkim, należy odpowiedzieć sobie na pytanie, dlaczego chcemy coś zmienić. Czy nowy sposób jest czytelniejszy, bezpieczniejszy, wydajniejszy? W poniższym przykładzie użycie nowego API nie tylko niepotrzebnie utrudnia odczytanie kodu, ale także zmniejsza jego wydajność – każde wejście do metody isEmptyJava8 powoduje utworzenie nowego, dodatkowego obiektu.

class CustomUtil {
public static boolean isEmptyJava7(String text) {
return text == null || text.isEmpty();
}
 public static boolean isEmptyJava8(String text) {
return !Optional.ofNullable(text).isPresent();
}
}

Nieumiejętne użycie nieznanego API

Mamy nowe narzędzia, których działanie nie jest dla nas w pełni zrozumiałe. Próbujemy odnaleźć się w tej sytuacji, stosując dotychczasowe doświadczenia.

Jak radzić sobie z tym problemem? Poza przeczesywaniem StackOverflow w poszukiwaniu gotowych rozwiązań, dobrze zajrzeć do dokumentacji metody, z którą się zaprzyjaźniamy (zachęcam do przejrzenia Javadoc metod get, orElse i orElseGet z klasy Optional). Dobrym sposobem na uniknięcie nieprzyjemnych niespodzianek jest również czytanie ciekawych artykułów o antywzorcach…

Poniżej kilka przykładów, jak nie używać Optional.

#1 Pole klasy typu Optional.

Dlaczego nie:

1.    Optional nie implementuje Serializable – nasze pole nie będzie widoczne dla mechanizmu serializacji.

class KlasaKtoraChceSerializowac implements Serializable {
 private Optional<Person> unikam;
private Person preferuje;
//...
}

2. Kod staje się mniej przejrzysty – mamy klasę opakowującą, która niewiele nam pomaga, ponieważ nie możemy powstrzymać wyobraźni programistów(-stek) korzystających z naszego API przed przekazaniem null do setter’a.

class Person {
private Optional<Car> car;
 public Optional<Car> getCar() {
return car;
}
public void setCar(Optional<Car> car) {
  this.car = car;
}
}

Zamiast pola użyjmy Optional tylko w getterze:

class Person {
private Person person;
public Optional<Person> getPerson() {
return Optional.ofNullable(person);
}
public void setPerson(Person person) {
 this.person = person;
}
}

#2 Optional w kolekcjach lub strumieniach.

Poniższe API nie jest zbyt przyjazne dla użytkownika(-iczki) – przechowywanie wartości null w kolekcji nie jest dobrym pomysłem. Należy zadbać o wysłanie konkretnych wartości w kolekcji/strumieniu lub pustej kolekcji/strumienia.

interface PersonRepository {
List<Optional<Person>> getPeopleTheWrongWay();
List<Person> getPeopleTheRightWay();
}

#3 Optional jako parametr metody/konstruktora

Ideą Optional było zabezpieczenie przed NPE. Używając go jako parametru, osiągniemy  trzy rzeczy:

  • nie zabezpieczymy się przed NPE
  • kod naszej klasy będzie mniej przejrzysty (bez Optional’a sprawdzamy  dwa scenariusze, z Optional’em sprawdzamytrzy )
  • wymusimy narzut pracy na użytkowniku(-iczce) naszego API (opakowanie przekazanej wartości w Optional).

class ClassWithIssues {
public void _3scenariosToCheck(Optional<Person> person) {
if (person == null || !person.isPresent()) {
// some logic
else {
// some logic
}
}


public void _2scenariosToCheck(Person person) {
if (person == null) {
// some logic
else {
// some logic
}
}
}

Mniej czytelne użycie API w użyciu, a nadal można przekazać null.

api._3scenariosToCheck(Optional.ofNullable(p));
api._2scenariosToCheck(p);

#4 Nowe narzędzie, stare nawyki: isPresent + get.

if(optional.isPresent()) {
uzyjPerson(optional.get());
}

Można zauważyć, że powyższy sposób praktycznie nie różni się od “tradycyjnego” sprawdzania wartości null (jeśli wywołamy get na Optional bez wartości, dostaniemy NoSuchElementException). Cóż, każdemu  zdarzają się gorsze dni w pracy – nawet autorom(-rkom ) API Javy.

Metoda get() w klasie Optional jest idealnym kandydatem dla adnotacji @Deprecated.Powyższy zapis można uprościć:

optional.ifPresent(Api::uzyjPerson);

Od Java 9 mamy dodatkową opcję:

optional.ifPresentOrElse(Api::uzyjPerson, Api::bezPerson);

#5 orElse zamiast orElseGet

Obie metody wyglądają bardzo podobnie, ale orElse można wykonać zawsze, bez względu na to, czy Optional przechowuje wartość czy nie. Metoda orElseGet zadziała natomiast tylko wtedy, gdy Optional nie będzie przechowywał wartości. Różnica może być bolesna kiedy o niej nie wiemy, a wywołana jest metoda, która obciąża zasoby lub zmienia stan aplikacji.

Person person1 = optional.orElse(Api.dlugieObliczenieLubZmianaStanu());
Person person2 = optional.orElseGet(Api::dlugieObliczenieLubZmianaStanu);

Podsumowanie:

1.    Przed użyciem nowych narzędzi zastanówmy się, czy na pewno stare są gorsze.
2.    Używaj Optional’a dla zaznaczenia możliwego scenariusza, w którym występuje brak zwracanej wartości (np. zwracana wartość z funkcji).
3.    Unikaj Optional w kolekcji lub strumieniu.
4.    Unikaj Optional jako parametru metody lub konstruktora.
5.    Zamiast Optional::isPresent + Optional::get używaj Optional:orElse lub Optional::orElseGet.
6.    W orElse unikaj wywołań, które zmieniają stan lub przeprowadzają skomplikowane obliczenia.

NullPointerException to jedno z najbardziej wstydliwych potknięć dla programisty(-tki). Po przeczytaniu tego artykułu zyskałeś więcej narzędzi do jego uniknięcia. Nawet jeśli zdarzy Ci się potknięcie, pamiętaj, że jest na świecie bardzo błyskotliwa osoba, która od wielu lat radzi sobie z tym wstydem na co dzień – Sir Charles Antony Richard Hoare i jego “billion dollar mistake”.