fbpx

Pozbądź się if-else

Pozbądź się if-else

Im więcej if-else masz w kodzie, tym bardziej jest skomplikowany, tym trudniej go zrozumieć, tym więcej ścieżek masz do przetestowania.
Dużo łatwiej czyta się kod, z góry na dół, bez rozgałęzień i opcjonalnych dodatkowych zachowań, warto do tego dążyć.

Tylko jak sobie radzić w takim razie?
Oto kilka scenariuszy, z którymi możesz się spotkać.

Bazowanie na Stanie

Przypadek jest bardzo prosty. Twój obiekt może być w jakimś stanie, np. Zamówienie – może być nowe, w realizacji, wysłane itp.
W zależności od tego w jakim stanie obecnie znajduje się zamówienie, metoda popychająca je „Dalej” wykona inne akcje.

Czyli mamy taki kod:

public void advanceOrder(Order order) {
  if (order.state == "new") {
    // TODO: Validate New Order
    order.state = "received";
  } else if (order.state == "received") {
    // TODO: Send to Warehouse Team
    order.state = "processing";
  } else if (order.state == "processing") {
    // TODO: Send Shipped Email to Customer
    order.state = "shipped";
  } else if (order.state == "complete") {
    // TODO: Update Order Information and Send Received Email to Customer
    order.state = "complete";
  } else if (order.state == "cancelled") {
    throw new OrderCancelledException("...");
  }

  saveOrder(order);
}

W tym przypadkiem bardzo dobrym rozwiązaniem będzie wprowadzenie maszyny stanów. Masz do wykonania tak naprawdę 3 kroki:

  1. Wprowadzasz interfejs dla Stanu: OrderState
  2. Implementujesz każdy z możliwych stanów: NewState, ReceivedState, ProcessingState itd.
  3. W zamówieniu zamieniasz stan z postaci String na nowy interfejs OrderState.

Po takich zmianach logika przechodzenia pomiędzy stanami jest zamknięta w implementacji każdego z nich. Stan New wie jaki jest następny stan, wie też jak się zachować w przypadku anulowania zamówienia oraz wszelkich innych operacji, jakie na zamówieniu można wykonać.

A sama metoda wykonująca operację, jest teraz bardzo prosta:

public void advanceOrder(Order order) {
  order.state = order.state.advanceOrder(order);
  saveOrder(order);
}

Natomiast implementacja stanu to tylko:

public class NewState implements OrderState {
  public OrderState advanceOrder(Order order) {
    return new ReceivedState();
  }
}

Proste sprawdzenia warunków

Kolejna kategoria ifologii to proste sprawdzenia warunków, nullchecki i walidacja parametrów wejściowych w metodzie.

Najczęstszym przypadkiem, który na pewno znasz, jest sprawdzenie, czy jakiś argument nie jest nullem, po czym wykonujemy naszą robotę, natomiast jeżeli jest nullem, zwracamy błędy walidacji.

Czyli coś takiego:

void validateRequest(Model request, ValidationErrors validationErrors) {
    if (request != null) {
        if (StringUtils.isBlank(request.name)) {
            validationErrors.propertyRequired("Name");
        }
        if (StringUtils.isBlank(request.description)) {
            validationErrors.propertyRequired("Description");
        }
    } else {
        validationErrors.argumentRequired("request");
    }
}

Najlepszym rozwiązaniem, jest pozbycie się else, poprzez odwrócenie pierwszego warunku z != na == i jak najszybsze wyjście z metody poprzez return;. Tym sposobem, jej kod robi się dużo bardziej czytelny.

void validateRequest(Model request, ValidationErrors validationErrors) {
    if (request == null) {
        validationErrors.argumentRequired("request");
        return;
    }

    if (StringUtils.isBlank(request.name)) {
        validationErrors.propertyRequired("Name");
    }
    if (StringUtils.isBlank(request.description)) {
        validationErrors.propertyRequired("Description");
    }
}

Wybór implementacji na podstawie typu

Czasami pojawiają się przypadki, kiedy na podstawie parametru musisz wybrać implementację algorytmu, który ma być wykorzystany.
Niestety, bardzo często kończy się to wielkim zestawem if-else, gdzie sprawdzamy parametr i wykonujemy logikę dla danej wartości.

Czyli coś takiego:

public class Tournament {
    // ... All other Tournament Code ...

    public SeedResult seed() {
        if (tournament.seedAlgorithm == "ordered") {
            // TODO: Run Ordered Algorithm
            return results;
        } else if (tournament.seedAlgorithm == "random") {
            // TODO: Run Random Algorithm
            return results;
        } else if (tournament.seedAlgorithm == "total-score") {
            // TODO: Run Total Score Algorithm
            return results;
        } else {
            throw new InvalidAlgorithmException("...");
        }
    }
}

Rozwiązanie jest bardzo podobne do maszyny stanów, z tą różnicą, że tworzymy klasy dla każdego algorytmu z osobna, plus oczywiście interfejs, który nam uwspólnia API.

Kolejnym krokiem będzie użycie odpowiedniej implementacji, no i tutaj też masz kilka opcji:

  1. Utworzyć explicite te implementacje które potrzebujesz i je wykorzystać.
  2. Wstrzyknąć konkretne implementacje za pomocą Dependency Injection.
  3. Stworzyć fabrykę, która na podstawie konfiguracji, będzie zwracała odpowiednią implementację. Oczywiście musisz jakoś wybrać odpowiednią, ale masz tutaj wiele opcji, od switch(...), przez wczytywanie po nazwie klasy, czy odpytanie każdej implementacji, czy obsługuje dany typ określony w konfiguracji. Taka fabryka ma jeden ogromny plus, jest to jedno miejsce w kodzie, gdzie podejmowanie takiej decyzji musisz zaimplementować.

Algorytmy

Ostatni przypadek, gdzie możesz natknąć się na if-else to implementacje złożonych algorytmów. No i tutaj spraw się komplikuje, ponieważ zazwyczaj, takie algorytmy są bardzo specyficzne, zoptymalizowane na różne sposoby i tak naprawdę masz małe pole do manewru.
Dodanie dodatkowych warstw abstrakcji wcale nie będzie pomocne w ich przypadku i lepiej je po prostu zostawić w spokoju.

Co nie zmienia faktu, że mimo tego, że w ich kodzie będzie potencjalnie sporo if-else, to miej na uwadze, aby ich wykorzystanie było jak najbardziej czytelne.

Podsumowując

Na pewno nie usuniesz z logiki rozgałęzień przepływów, z jakiegoś powodu one tam są i są potrzebne. One tam będą i nic z tym nie zrobisz, ale to, co możesz zrobić to:

  1. Poprawić czytelność – spraw aby te ścieżki były możliwie krótkie i bardzo czytelnie określone w kodzie, aby nie trzeba było wiele razy błądzić po nim, aby zrozumieć, co się dzieje. To jest bardzo ważne.
  2. Zadbać o utrzymywalność – im prostszy do zrozumienia jest kod, tym łatwiej go utrzymać, tym mniej błędów się w nim znajdzie.

Źródla


Ten artykuł znalazł się w Mailingu EffectiveDev

Wydanie #22

‍💫 Przekleństwo ifologii

  • 👉 Pozbądź się if-else
    Czyli jak inaczej podejmować decyzje w kodzie
  • 👉 GraphQL w Springu
    Czyli jak wystawić API w kilku krokach
  • 👉 Event-Driven Architecture
    Czyli jak zmniejszyć coupling
  • 👉 Każdy system w chmurze
    Czyli co ciekawego na Twitterze

Dołącz TUTAJ
Co tydzień, podobne artykuły otrzymasz prosto do swojej skrzynki mailowej!

Awatar Łukasz Monkiewicz

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *