W programowaniu często powtarza się zasada: "przezorny zawsze ubezpieczony". Innymi słowy - lepiej dmuchać na zimne. Programowanie defensywne to podejście, w którym piszemy kod tak, aby był odporny na nieprzewidziane sytuacje i błędy. Nie zakładamy, że wszystko zadziała idealnie. Przeciwnie, przygotowujemy nasz kod na najgorsze scenariusze. Dzięki temu aplikacje działają stabilniej, łatwiej się je utrzymuje i rzadziej zaskakują nas awariami.
Czym jest programowanie defensywne?
Mówiąc najprościej, programowanie defensywne to sztuka pisania odpornego kodu.
Taki kod:
• Sprawdza poprawność danych wejściowych zanim zacznie je przetwarzać.
• Waliduje założenia i warunki brzegowe (np. czy referencja nie jest null, czy lista nie jest pusta).
• Odpowiednio reaguje na błędy, na przykład zgłasza wyjątki z jasnym komunikatem zamiast pozwolić aplikacji się "wysypać" bez wyjaśnienia.
• Jest czytelny i prosty (zgodnie z zasadą KISS: Keep It Simple, Stupid), co minimalizuje ryzyko błędów wynikających ze złożoności.
Zasadą defensywnego podejścia jest brak zaufania do danych i wywołań zewnętrznych. Twój kod powinien weryfikować wszystko, nawet jeśli teoretycznie "i tak będzie dobrze". Dzięki temu, gdy pojawi się nietypowy przypadek, np. użytkownik wprowadzi nieoczekiwane dane - Twój program sobie z nim poradzi albo przynajmniej bezpiecznie się zatrzyma z odpowiednim komunikatem.
Techniki defensywnego kodowania
Poniżej kilka kluczowych technik i dobrych praktyk, które pomogą Ci pisać kod defensywnie:
1. Walidacja wejścia: Zawsze sprawdzaj dane wejściowe funkcji/metody. Jeżeli oczekujesz liczby dodatniej, upewnij się, że otrzymana wartość spełnia ten warunek. W przeciwnym razie zareaguj, np. zwróć błąd lub zgłoś wyjątek.
2. Guard clauses (strażnicy): Na początku metod sprawdzaj warunki, które muszą być spełnione, aby dalszy kod miał sens. Przykładowo, jeśli funkcja nie powinna być wywołana z argumentem null, to pierwszą instrukcją w jej ciele może być:
if (user == null)
throw new ArgumentNullException(nameof(user));Taki "strażnik" natychmiast przerywa wykonanie metody, jeśli warunek wejściowy nie został spełniony. Dzięki temu błąd wychwycisz wcześniej, zamiast pozwolić mu spowodować trudniejsze do wykrycia skutki w głębi logiki programu.
3. Obsługa wyjątków: Stosuj mechanizm wyjątków do obsługi błędów, ale nie "łap" na siłę wszystkich wyjątków globalnie. Zamiast tego obsługuj te sytuacje, które potrafisz sensownie rozwiązać (np. brak pliku konfiguracyjnego, możesz wtedy utworzyć plik domyślny lub wyświetlić komunikat). Inne, nieoczekiwane wyjątki powinny zostać zasygnalizowane (np. zalogowane i przekazane dalej), by nie zatajać poważnych problemów.
4. Asercje i debugowanie: W czasie developmentu używaj asercji (Debug.Assert w .NET) do wychwytywania sytuacji, które nie powinny się zdarzyć. Asercje są wyłączane w kompilacji produkcyjnej, ale podczas testów potrafią w porę ostrzec programistę, że jego założenia co do działania kodu są błędne.
5. Zasada najmniejszego zaufania: Projektuj moduły tak, jakby inne części systemu mogły przekazać Ci niewłaściwe lub złośliwe dane. Przykładowo, nie zakładaj, że odpowiedź z obcego API zawsze będzie kompletna i zgodna z oczekiwaniami, zamiast tego sprawdzaj, czy otrzymane dane są poprawne i pełne, zanim z nich skorzystasz.
Poniżej prosty przykład metody napisanej defensywnie w C#:
public double ObliczProcent(double kwota, int procent)
{
/* Walidacja argumentów wejściowych */
if (kwota < 0)
throw new ArgumentOutOfRangeException(nameof(kwota), "Kwota nie może być ujemna");
if (procent < 0 || procent > 100)
throw new ArgumentOutOfRangeException(nameof(procent), "Procent musi być w zakresie 0-100");
/* Główna logika (wykonywana tylko gdy dane są poprawne) */
return kwota * (procent / 100.0);
}W powyższej funkcji najpierw sprawdzamy, czy kwota oraz procent mieszczą się w oczekiwanych zakresach. Jeśli nie, zgłaszamy adekwatny wyjątek z informacją, co jest nie tak. Dopiero gdy dane wejściowe są zweryfikowane, wykonujemy właściwe obliczenie. Dzięki temu unikamy dziwnych rezultatów (np. ujemnych rabatów lub błędów związanych z niepoprawnymi danymi) i od razu wiadomo, gdzie szukać przyczyny problemu w razie wyjątku.
Podsumowanie
Programowanie defensywne to nawyk, który warto w sobie wyrobić już od początku nauki kodowania. Pisząc ostrożny i odporny kod, oszczędzasz sobie (i innym) wielu godzin debugowania oraz frustracji. Pamiętaj: lepiej zawczasu wszystko sprawdzić i upewnić się, niż później żałować, że czegoś nie dopilnowaliśmy.
Jeśli chcesz poznać więcej dobrych praktyk i solidnie opanować podstawy programowania w C#/.NET, zachęcam do sprawdzenia mojego szkolenia online Zostań Programistą .NET. To kompleksowy program dla początkujących - w 15 tygodni przechodzę z Tobą od zupełnych podstaw do poziomu, który pozwoli Ci rozpocząć karierę programisty.