Używamy cookies, żeby zwiększyć Twoje doświadczenia na stronie
CodeWorlds

Unikanie głębokiego zagnieżdżania - Early Returns

Dr. Ian Malcolm, siedząc w centrum kontroli Parku Jurajskiego, obserwuje kod napisany przez zespół IT. "Spójrzcie na to," mówi, wskazując na ekran. "To tak jakby chodzić przez labirynt pełen dinozaurów - każdy if to kolejny zakręt, każdy else to ślepa uliczka. Chaos znajduje drogę... ale my nie powinniśmy jej utrudniać!"

Problem: Arrow Anti-Pattern

Jednym z najczęstszych problemów w kodzie początkujących programistów jest tzw. "Arrow Anti-Pattern" lub "Pyramid of Doom" - kod, który zagnieżdża się coraz głębiej, tworząc charakterystyczny kształt strzałki:

1// ❌ ŹLE: Arrow Anti-Pattern - kod zagnieżdżony jak piramida
2function checkDinosaurSafety(dinosaur) {
3  if (dinosaur) {
4    if (dinosaur.isAlive) {
5      if (dinosaur.enclosure) {
6        if (dinosaur.enclosure.fence) {
7          if (dinosaur.enclosure.fence.isActive) {
8            if (dinosaur.health > 50) {
9              if (dinosaur.aggressionLevel < 7) {
10                if (dinosaur.lastFed) {
11                  const hoursSinceFeeding = (Date.now() - dinosaur.lastFed) / 3600000;
12                  if (hoursSinceFeeding < 12) {
13                    return 'Bezpieczny';
14                  } else {
15                    return 'Głodny - ostrożnie!';
16                  }
17                } else {
18                  return 'Brak danych o karmieniu';
19                }
20              } else {
21                return 'Wysoki poziom agresji!';
22              }
23            } else {
24              return 'Niski poziom zdrowia';
25            }
26          } else {
27            return 'ALARM: Ogrodzenie nieaktywne!';
28          }
29        } else {
30          return 'BŁĄD: Brak ogrodzenia';
31        }
32      } else {
33        return 'BŁĄD: Brak przypisanego wybiegu';
34      }
35    } else {
36      return 'Dinozaur martwy';
37    }
38  } else {
39    return 'Brak danych o dinozaurze';
40  }
41}
42
43// Ten kod jest:
44// - Trudny do przeczytania (musisz liczyć nawiasy)
45// - Trudny do zrozumienia (logika jest rozproszona)
46// - Trudny do modyfikacji (dodanie warunku wymaga reorganizacji)
47// - Podatny na błędy (łatwo się zgubić w zagnieżdżeniach)

Rozwiązanie: Early Returns (Guard Clauses)

Zamiast zagnieżdżać kod, używamy early returns - sprawdzamy warunki błędów na początku i od razu wychodzimy z funkcji. To nazywa się także "guard clauses" (klauzule strażnicze).

1// ✅ DOBRZE: Early Returns - czytelny kod liniowy
2function checkDinosaurSafety(dinosaur) {
3  // Sprawdzamy wszystkie warunki błędów od razu
4  if (!dinosaur) {
5    return 'Brak danych o dinozaurze';
6  }
7
8  if (!dinosaur.isAlive) {
9    return 'Dinozaur martwy';
10  }
11
12  if (!dinosaur.enclosure) {
13    return 'BŁĄD: Brak przypisanego wybiegu';
14  }
15
16  if (!dinosaur.enclosure.fence) {
17    return 'BŁĄD: Brak ogrodzenia';
18  }
19
20  if (!dinosaur.enclosure.fence.isActive) {
21    return 'ALARM: Ogrodzenie nieaktywne!';
22  }
23
24  if (dinosaur.health <= 50) {
25    return 'Niski poziom zdrowia';
26  }
27
28  if (dinosaur.aggressionLevel >= 7) {
29    return 'Wysoki poziom agresji!';
30  }
31
32  if (!dinosaur.lastFed) {
33    return 'Brak danych o karmieniu';
34  }
35
36  // Główna logika na końcu
37  const hoursSinceFeeding = (Date.now() - dinosaur.lastFed) / 3600000;
38  
39  if (hoursSinceFeeding >= 12) {
40    return 'Głodny - ostrożnie!';
41  }
42
43  return 'Bezpieczny';
44}
45
46// Ten kod jest:
47// ✓ Łatwy do przeczytania (liniowy flow)
48// ✓ Łatwy do zrozumienia (każdy warunek jest osobno)
49// ✓ Łatwy do modyfikacji (dodanie warunku = jedna linijka)
50// ✓ Mniej podatny na błędy (brak zagnieżdżeń)

Zasady stosowania Early Returns

1. Sprawdzaj najpierw edge cases i błędy

1// ❌ ŹLE: Główna logika ukryta w zagnieżdżeniach
2function processVisitorEntry(visitor, ticket) {
3  if (visitor && ticket) {
4    if (ticket.isValid) {
5      if (ticket.hasAdultSupervision || visitor.age >= 18) {
6        if (visitor.hasHealthClearance) {
7          // Główna logika głęboko zagnieżdżona
8          registerEntry(visitor);
9          return true;
10        }
11      }
12    }
13  }
14  return false;
15}
16
17// ✅ DOBRZE: Edge cases na początku
18function processVisitorEntry(visitor, ticket) {
19  // Walidacja danych wejściowych
20  if (!visitor || !ticket) {
21    throw new Error('Brak danych odwiedzającego lub biletu');
22  }
23
24  // Sprawdzanie warunków od najbardziej podstawowych
25  if (!ticket.isValid) {
26    throw new Error('Bilet nieważny');
27  }
28
29  if (visitor.age < 18 && !ticket.hasAdultSupervision) {
30    throw new Error('Niepełnoletni wymaga opieki osoby dorosłej');
31  }
32
33  if (!visitor.hasHealthClearance) {
34    throw new Error('Brak zezwolenia zdrowotnego');
35  }
36
37  // Główna logika na końcu - teraz wiadomo że wszystko jest OK
38  registerEntry(visitor);
39  return true;
40}

2. Używaj negacji dla warunków błędnych

1// ❌ ŹLE: Pozytywne warunki prowadzą do zagnieżdżeń
2function feedDinosaur(dinosaurId, foodAmount) {
3  if (dinosaurs.has(dinosaurId)) {
4    const dino = dinosaurs.get(dinosaurId);
5    if (dino.isAlive) {
6      if (foodAmount > 0) {
7        if (foodAmount <= MAX_FOOD_PORTION) {
8          // Logika karmienia
9          dino.feed(foodAmount);
10          return true;
11        }
12      }
13    }
14  }
15  return false;
16}
17
18// ✅ DOBRZE: Negacja warunków pozwala na early return
19function feedDinosaur(dinosaurId, foodAmount) {
20  // Sprawdzamy wszystkie warunki BŁĘDNE i od razu wychodzimy
21  if (!dinosaurs.has(dinosaurId)) {
22    throw new Error(`Dinozaur ${dinosaurId} nie istnieje`);
23  }
24
25  const dino = dinosaurs.get(dinosaurId);
26
27  if (!dino.isAlive) {
28    throw new Error('Nie można karmić martwego dinozaura');
29  }
30
31  if (foodAmount <= 0) {
32    throw new Error('Ilość jedzenia musi być większa niż 0');
33  }
34
35  if (foodAmount > MAX_FOOD_PORTION) {
36    throw new Error(`Maksymalna porcja to ${MAX_FOOD_PORTION}kg`);
37  }
38
39  // Główna logika - wiemy że wszystko jest OK
40  dino.feed(foodAmount);
41  return true;
42}

3. Wynoś złożoną logikę do osobnych funkcji

1// ❌ ŹLE: Złożona logika zagnieżdżona
2function authorizeAccess(employee, sector) {
3  if (employee) {
4    if (employee.isActive) {
5      if (employee.clearanceLevel >= sector.requiredClearance) {
6        if (employee.department === sector.allowedDepartment || 
7            employee.role === 'Admin' ||
8            employee.temporaryAccess.includes(sector.id)) {
9          if (!employee.accessRestrictions.includes(sector.id)) {
10            if (isWithinAllowedHours(employee.schedule, sector.operatingHours)) {
11              return grantAccess(employee, sector);
12            }
13          }
14        }
15      }
16    }
17  }
18  return denyAccess(employee, sector);
19}
20
21// ✅ DOBRZE: Wydzielamy warunki do funkcji pomocniczych
22function authorizeAccess(employee, sector) {
23  if (!employee) {
24    return denyAccess(null, sector, 'Brak danych pracownika');
25  }
26
27  if (!employee.isActive) {
28    return denyAccess(employee, sector, 'Konto nieaktywne');
29  }
30
31  if (!hasSufficientClearance(employee, sector)) {
32    return denyAccess(employee, sector, 'Niewystarczający poziom dostępu');
33  }
34
35  if (!isDepartmentAuthorized(employee, sector)) {
36    return denyAccess(employee, sector, 'Nieautoryzowany dział');
37  }
38
39  if (hasAccessRestriction(employee, sector)) {
40    return denyAccess(employee, sector, 'Ograniczenie dostępu');
41  }
42
43  if (!isWithinAllowedHours(employee.schedule, sector.operatingHours)) {
44    return denyAccess(employee, sector, 'Poza godzinami dostępu');
45  }
46
47  return grantAccess(employee, sector);
48}
49
50// Funkcje pomocnicze - każda ma pojedynczą odpowiedzialność
51function hasSufficientClearance(employee, sector) {
52  return employee.clearanceLevel >= sector.requiredClearance;
53}
54
55function isDepartmentAuthorized(employee, sector) {
56  return employee.department === sector.allowedDepartment ||
57         employee.role === 'Admin' ||
58         employee.temporaryAccess.includes(sector.id);
59}
60
61function hasAccessRestriction(employee, sector) {
62  return employee.accessRestrictions.includes(sector.id);
63}

Praktyczne przykłady refaktoryzacji

Przykład 1: System alarmowy

1// ❌ PRZED: Zagnieżdżony kod trudny do czytania
2function triggerAlarm(sensorData) {
3  if (sensorData) {
4    if (sensorData.fence) {
5      if (sensorData.fence.voltage < MIN_VOLTAGE) {
6        if (emergencyProtocol.isActive) {
7          if (securityTeam.isAvailable) {
8            if (evacuationRoutes.areOpen) {
9              activateFullAlarm();
10              notifySecurityTeam();
11              return { status: 'active', level: 'critical' };
12            } else {
13              activatePartialAlarm();
14              return { status: 'active', level: 'warning' };
15            }
16          } else {
17            activateAutomatedResponse();
18            return { status: 'automated', level: 'critical' };
19          }
20        } else {
21          logIncident();
22          return { status: 'logged', level: 'info' };
23        }
24      } else {
25        return { status: 'normal', level: 'info' };
26      }
27    }
28  }
29  return { status: 'error', level: 'critical' };
30}
31
32// ✅ PO: Czytelny kod z early returns
33function triggerAlarm(sensorData) {
34  // Walidacja danych
35  if (!sensorData || !sensorData.fence) {
36    return { status: 'error', level: 'critical', message: 'Brak danych z czujnika' };
37  }
38
39  // Sprawdzenie głównego warunku
40  if (sensorData.fence.voltage >= MIN_VOLTAGE) {
41    return { status: 'normal', level: 'info', message: 'Napięcie prawidłowe' };
42  }
43
44  // Niskie napięcie wykryte - sprawdzamy protokoły
45  if (!emergencyProtocol.isActive) {
46    logIncident();
47    return { status: 'logged', level: 'info', message: 'Incydent zarejestrowany' };
48  }
49
50  // Protokół aktywny - sprawdzamy dostępność zespołu
51  if (!securityTeam.isAvailable) {
52    activateAutomatedResponse();
53    return { status: 'automated', level: 'critical', message: 'Odpowiedź automatyczna' };
54  }
55
56  // Zespół dostępny - sprawdzamy drogi ewakuacyjne
57  if (!evacuationRoutes.areOpen) {
58    activatePartialAlarm();
59    return { status: 'active', level: 'warning', message: 'Alarm częściowy' };
60  }
61
62  // Wszystko gotowe - pełny alarm
63  activateFullAlarm();
64  notifySecurityTeam();
65  return { status: 'active', level: 'critical', message: 'Pełny alarm aktywowany' };
66}

Przykład 2: Walidacja formularza

1// ❌ PRZED: Walidacja z wieloma poziomami zagnieżdżeń
2function validateVisitorRegistration(formData) {
3  if (formData.firstName) {
4    if (formData.lastName) {
5      if (formData.email) {
6        if (isValidEmail(formData.email)) {
7          if (formData.age) {
8            if (formData.age >= 5 && formData.age <= 120) {
9              if (formData.emergencyContact) {
10                if (formData.emergencyContact.phone) {
11                  if (isValidPhone(formData.emergencyContact.phone)) {
12                    if (formData.acceptedTerms) {
13                      return { valid: true, data: formData };
14                    } else {
15                      return { valid: false, error: 'Musisz zaakceptować regulamin' };
16                    }
17                  } else {
18                    return { valid: false, error: 'Nieprawidłowy numer telefonu awaryjnego' };
19                  }
20                } else {
21                  return { valid: false, error: 'Brak numeru telefonu awaryjnego' };
22                }
23              } else {
24                return { valid: false, error: 'Brak kontaktu awaryjnego' };
25              }
26            } else {
27              return { valid: false, error: 'Wiek musi być między 5 a 120' };
28            }
29          } else {
30            return { valid: false, error: 'Brak wieku' };
31          }
32        } else {
33          return { valid: false, error: 'Nieprawidłowy adres email' };
34        }
35      } else {
36        return { valid: false, error: 'Brak adresu email' };
37      }
38    } else {
39      return { valid: false, error: 'Brak nazwiska' };
40    }
41  } else {
42    return { valid: false, error: 'Brak imienia' };
43  }
44}
45
46// ✅ PO: Czytelna walidacja z early returns
47function validateVisitorRegistration(formData) {
48  // Walidacja wymaganych pól tekstowych
49  if (!formData.firstName) {
50    return { valid: false, error: 'Brak imienia' };
51  }
52
53  if (!formData.lastName) {
54    return { valid: false, error: 'Brak nazwiska' };
55  }
56
57  if (!formData.email) {
58    return { valid: false, error: 'Brak adresu email' };
59  }
60
61  if (!isValidEmail(formData.email)) {
62    return { valid: false, error: 'Nieprawidłowy adres email' };
63  }
64
65  // Walidacja wieku
66  if (!formData.age) {
67    return { valid: false, error: 'Brak wieku' };
68  }
69
70  if (formData.age < 5 || formData.age > 120) {
71    return { valid: false, error: 'Wiek musi być między 5 a 120' };
72  }
73
74  // Walidacja kontaktu awaryjnego
75  if (!formData.emergencyContact) {
76    return { valid: false, error: 'Brak kontaktu awaryjnego' };
77  }
78
79  if (!formData.emergencyContact.phone) {
80    return { valid: false, error: 'Brak numeru telefonu awaryjnego' };
81  }
82
83  if (!isValidPhone(formData.emergencyContact.phone)) {
84    return { valid: false, error: 'Nieprawidłowy numer telefonu awaryjnego' };
85  }
86
87  // Walidacja akceptacji regulaminu
88  if (!formData.acceptedTerms) {
89    return { valid: false, error: 'Musisz zaakceptować regulamin' };
90  }
91
92  // Wszystkie walidacje przeszły
93  return { valid: true, data: formData };
94}

Przykład 3: Przetwarzanie zamówienia

1// ❌ PRZED: Złożona logika biznesowa w zagnieżdżeniach
2function processFoodOrder(order) {
3  if (order && order.items && order.items.length > 0) {
4    if (inventory.checkAvailability(order.items)) {
5      const total = calculateTotal(order.items);
6      if (order.payment) {
7        if (order.payment.amount >= total) {
8          if (order.deliveryLocation) {
9            if (isDeliveryLocationSafe(order.deliveryLocation)) {
10              if (deliveryPersonnel.isAvailable()) {
11                const receipt = generateReceipt(order, total);
12                inventory.reduceStock(order.items);
13                scheduleDelivery(order);
14                return { success: true, receipt };
15              } else {
16                return { success: false, reason: 'Brak dostępnego personelu' };
17              }
18            } else {
19              return { success: false, reason: 'Niebezpieczna lokalizacja dostawy' };
20            }
21          } else {
22            return { success: false, reason: 'Brak lokalizacji dostawy' };
23          }
24        } else {
25          return { success: false, reason: 'Niewystarczająca kwota płatności' };
26        }
27      } else {
28        return { success: false, reason: 'Brak danych płatności' };
29      }
30    } else {
31      return { success: false, reason: 'Produkty niedostępne w magazynie' };
32    }
33  } else {
34    return { success: false, reason: 'Nieprawidłowe zamówienie' };
35  }
36}
37
38// ✅ PO: Logika biznesowa podzielona na kroki
39function processFoodOrder(order) {
40  // Krok 1: Walidacja zamówienia
41  const validationError = validateOrder(order);
42  if (validationError) {
43    return { success: false, reason: validationError };
44  }
45
46  // Krok 2: Sprawdzenie dostępności w magazynie
47  if (!inventory.checkAvailability(order.items)) {
48    return { success: false, reason: 'Produkty niedostępne w magazynie' };
49  }
50
51  // Krok 3: Weryfikacja płatności
52  const total = calculateTotal(order.items);
53  const paymentError = validatePayment(order.payment, total);
54  if (paymentError) {
55    return { success: false, reason: paymentError };
56  }
57
58  // Krok 4: Weryfikacja dostawy
59  const deliveryError = validateDelivery(order.deliveryLocation);
60  if (deliveryError) {
61    return { success: false, reason: deliveryError };
62  }
63
64  if (!deliveryPersonnel.isAvailable()) {
65    return { success: false, reason: 'Brak dostępnego personelu' };
66  }
67
68  // Krok 5: Realizacja zamówienia
69  const receipt = generateReceipt(order, total);
70  inventory.reduceStock(order.items);
71  scheduleDelivery(order);
72
73  return { success: true, receipt };
74}
75
76// Funkcje pomocnicze
77function validateOrder(order) {
78  if (!order) return 'Brak zamówienia';
79  if (!order.items || order.items.length === 0) return 'Brak produktów w zamówieniu';
80  return null;
81}
82
83function validatePayment(payment, requiredAmount) {
84  if (!payment) return 'Brak danych płatności';
85  if (payment.amount < requiredAmount) return 'Niewystarczająca kwota płatności';
86  return null;
87}
88
89function validateDelivery(location) {
90  if (!location) return 'Brak lokalizacji dostawy';
91  if (!isDeliveryLocationSafe(location)) return 'Niebezpieczna lokalizacja dostawy';
92  return null;
93}

Dodatkowe techniki

1. Używaj optional chaining i nullish coalescing

1// ❌ PRZED: Zagnieżdżone sprawdzanie null/undefined
2function getDinosaurLocation(dinosaurId) {
3  const dino = dinosaurs.get(dinosaurId);
4  if (dino) {
5    if (dino.enclosure) {
6      if (dino.enclosure.location) {
7        return dino.enclosure.location.coordinates;
8      }
9    }
10  }
11  return null;
12}
13
14// ✅ PO: Optional chaining
15function getDinosaurLocation(dinosaurId) {
16  const dino = dinosaurs.get(dinosaurId);
17  return dino?.enclosure?.location?.coordinates ?? null;
18}

2. Wydzielaj złożone warunki do zmiennych

1// ❌ PRZED: Trudny do zrozumienia warunek
2function shouldEvacuate(park) {
3  if ((park.fenceStatus === 'breached' && park.dangerousDinosaurs > 0) ||
4      (park.powerOutage && park.backupGeneratorFailed) ||
5      (park.weatherAlert === 'severe' && park.visitorCount > 100)) {
6    return true;
7  }
8  return false;
9}
10
11// ✅ PO: Nazwane warunki
12function shouldEvacuate(park) {
13  const hasDangerousBreach = park.fenceStatus === 'breached' && park.dangerousDinosaurs > 0;
14  const hasTotalPowerFailure = park.powerOutage && park.backupGeneratorFailed;
15  const hasWeatherEmergency = park.weatherAlert === 'severe' && park.visitorCount > 100;
16
17  return hasDangerousBreach || hasTotalPowerFailure || hasWeatherEmergency;
18}

3. Używaj strategii polimorfizmu zamiast switch/if-else

1// ❌ PRZED: Długi łańcuch if-else
2function handleDinosaurBehavior(dinosaur, behavior) {
3  if (behavior === 'aggressive') {
4    activateDefenses();
5    alertSecurity();
6    evacuateVisitors();
7  } else if (behavior === 'scared') {
8    reduceNoiseLevel();
9    increaseDistance();
10  } else if (behavior === 'hungry') {
11    prepareFoodration();
12    scheduleFeedingTime();
13  } else if (behavior === 'sleeping') {
14    reduceActivity();
15    dimLights();
16  } else if (behavior === 'playful') {
17    allowInteraction();
18    monitorClosely();
19  } else {
20    logUnknownBehavior(behavior);
21  }
22}
23
24// ✅ PO: Strategia z mapą funkcji
25const behaviorHandlers = {
26  aggressive: () => {
27    activateDefenses();
28    alertSecurity();
29    evacuateVisitors();
30  },
31  scared: () => {
32    reduceNoiseLevel();
33    increaseDistance();
34  },
35  hungry: () => {
36    prepareFoodRation();
37    scheduleFeedingTime();
38  },
39  sleeping: () => {
40    reduceActivity();
41    dimLights();
42  },
43  playful: () => {
44    allowInteraction();
45    monitorClosely();
46  }
47};
48
49function handleDinosaurBehavior(dinosaur, behavior) {
50  const handler = behaviorHandlers[behavior];
51  
52  if (!handler) {
53    logUnknownBehavior(behavior);
54    return;
55  }
56  
57  handler();
58}

Podsumowanie

"Życie... znajduje drogę," mówi Dr. Malcolm. "Ale kod nie powinien być labiryntem. Każda funkcja powinna mieć jasną, liniową ścieżkę - od walidacji na początku do logiki biznesowej na końcu."

Kluczowe zasady:

  1. Sprawdzaj błędy najpierw - używaj early returns dla edge cases
  2. Neguj warunki - zamiast
    if (x)
    użyj
    if (!x) return
  3. Unikaj zagnieżdżeń - maksymalnie 2-3 poziomy
  4. Wydzielaj logikę - złożone warunki → pomocnicze funkcje
  5. Nazywaj warunki - przypisuj złożone wyrażenia do zmiennych
  6. Używaj nowoczesnego JS - optional chaining, nullish coalescing

Kod z early returns jest:

  • Czytelniejszy - łatwiej zrozumieć flow
  • Łatwiejszy w utrzymaniu - łatwo dodać nowy warunek
  • Mniej podatny na błędy - mniej miejsc gdzie coś może pójść nie tak
  • Bardziej testowalny - każdy warunek można przetestować osobno

Pamiętaj: dobry kod to nie tylko kod, który działa - to kod, który jest łatwy do zrozumienia i modyfikacji. A w Parku Jurajskim, gdzie każdy błąd może mieć katastrofalne skutki, czytelność kodu może uratować życie!

Przejdź do CodeWorlds