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:
- Wprowadzasz interfejs dla Stanu:
OrderState
- Implementujesz każdy z możliwych stanów:
NewState
,ReceivedState
,ProcessingState
itd. - W zamówieniu zamieniasz stan z postaci
String
na nowy interfejsOrderState
.
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:
- Utworzyć explicite te implementacje które potrzebujesz i je wykorzystać.
- Wstrzyknąć konkretne implementacje za pomocą Dependency Injection.
- 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:
- 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.
- 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!
Dodaj komentarz