Jak obsługiwać nieprawidłowe dane wprowadzone przez użytkownika?

12

Zastanawiam się nad tym zagadnieniem od dłuższego czasu i byłbym ciekawy opinii innych programistów.

Mam tendencję do bardzo defensywnego stylu programowania. Mój typowy blok lub metoda wygląda następująco:

T foo(par1, par2, par3, ...)
{
    // Check that all parameters are correct, return undefined (null)
    // or throw exception if this is not the case.

    // Compute and (possibly) return result.
}

Ponadto podczas obliczania sprawdzam wszystkie wskaźniki przed ich odwołaniem. Mój pomysł jest taki, że jeśli jest jakiś błąd i jakiś wskaźnik NULL powinien się gdzieś pojawić, mój program powinien sobie z tym poradzić i po prostu odmówić kontynuowania obliczeń. Oczywiście może powiadomić o problemie z komunikatem o błędzie w dzienniku lub innym mechanizmie.

Mówiąc bardziej abstrakcyjnie, moje podejście jest takie

if all input is OK --> compute result
else               --> do not compute result, notify problem

Inni programiści, w tym moi koledzy, używają innej strategii. Np. Nie sprawdzają wskaźników. Zakładają, że fragment kodu powinien mieć poprawne dane wejściowe i nie powinien być odpowiedzialny za to, co się stanie, jeśli dane wejściowe są nieprawidłowe. Ponadto, jeśli wyjątek wskaźnika NULL spowoduje awarię programu, błąd zostanie łatwiej znaleziony podczas testowania i będzie miał większe szanse na naprawienie.

Moja odpowiedź na to pytanie jest normalna: ale co jeśli błąd nie zostanie wykryty podczas testowania i pojawi się, gdy produkt będzie już używany przez klienta? Jaki jest preferowany sposób ujawnienia się błędu? Czy powinien to być program, który nie wykonuje określonej czynności, ale może nadal działać, czy program, który ulega awarii i wymaga ponownego uruchomienia?

Zreasumowanie

Które z dwóch podejść do obsługi niewłaściwych danych wejściowych doradziłbyś?

Inconsistent input --> no action + notification

lub

Inconsistent input --> undefined behaviour or crash

Edytować

Dziękuję za odpowiedzi i sugestie. Jestem również fanem projektowania na podstawie umowy. Ale nawet jeśli ufam osobie, która napisała kod wywołujący moje metody (być może to jestem ja), nadal mogą występować błędy, które prowadzą do błędnych danych wejściowych. Więc moim podejściem jest, aby nigdy nie zakładać, że metoda została poprawnie przesłana.

Ponadto użyłbym mechanizmu, aby złapać problem i powiadomić o nim. W systemie programistycznym np. Otwiera okno dialogowe, aby powiadomić użytkownika. W systemie produkcyjnym po prostu zapisuje pewne informacje w dzienniku. Nie sądzę, że dodatkowe kontrole mogą prowadzić do problemów z wydajnością. Nie jestem pewien, czy stwierdzenia są wystarczające, czy są wyłączone w systemie produkcyjnym: być może wystąpi jakaś sytuacja w produkcji, która nie wystąpiła podczas testowania.

W każdym razie byłem naprawdę zaskoczony, że wiele osób postępuje w odwrotny sposób: pozwalają aplikacji na awarię „celowo”, ponieważ utrzymują, że ułatwi to znalezienie błędów podczas testowania.

Giorgio
źródło
Zawsze koduj defensywnie. Ostatecznie, ze względu na wydajność, możesz ustawić przełącznik, aby wyłączyć niektóre testy w trybie zwolnienia.
deadalnix
Dzisiaj naprawiłem błąd związany z brakującym sprawdzaniem wskaźnika NULL. Niektóre obiekty zostały utworzone podczas wylogowywania aplikacji, a konstruktor użył modułu pobierającego, aby uzyskać dostęp do innego obiektu, którego już nie było. W tym momencie obiekt nie miał być utworzony. Został stworzony z powodu innego błędu: jakiś czasomierz nie został zatrzymany podczas wylogowania -> wysłano sygnał -> odbiorca próbował utworzyć obiekt -> zapytał konstruktor i użył innego obiektu -> wskaźnik NULL -> awaria ). Naprawdę nie podoba mi się, że taka paskudna sytuacja psuje moją aplikację.
Giorgio
1
Reguła naprawy: Kiedy musisz zawieść, zawiedź głośno i jak najszybciej.
deadalnix
„Reguła naprawy: gdy musisz zawieść, zawiedź głośno i tak szybko, jak to możliwe.”: Myślę, że wszystkie BSOD systemu Windows są zgodne z tą regułą. :-)
Giorgio

Odpowiedzi:

8

Masz rację. Bądź paranoikiem. Nie ufaj innym kodom, nawet jeśli to Twój własny kod. Zapominacie rzeczy, wprowadzacie zmiany, kod ewoluuje. Nie ufaj kodowi zewnętrznemu.

Podkreślono dobrze: co, jeśli dane wejściowe są nieprawidłowe, ale program nie ulega awarii? Następnie dostajesz śmieci do bazy danych i błędy w dół linii.

Na pytanie o liczbę (np. Cenę w dolarach lub liczbę jednostek) chciałbym wpisać „1e9” i zobaczyć, co robi kod. To może się zdarzyć.

Cztery dekady temu, otrzymując licencjat z informatyki z UCBerkeley, powiedziano nam, że dobry program obsługuje 50% błędów. Bądź paranoikiem.

Andy Canfield
źródło
Tak, IMHO to jedna z niewielu sytuacji, w których bycie paranoikiem jest cechą, a nie problemem.
Giorgio
„Co jeśli dane wejściowe są niepoprawne, ale program nie ulega awarii? Wówczas pojawia się śmieci w bazie danych i błędy w dół linii.”: Zamiast awarii program może odmówić wykonania operacji i zwrócić niezdefiniowany wynik. Niezdefiniowane zostaną propagowane przez obliczenia i żadne śmieci nie zostaną wygenerowane. Ale program nie musi upaść, aby to osiągnąć.
Giorgio
Tak, ale - chodzi mi o to, że program musi wykryć nieprawidłowe dane wejściowe i sobie z tym poradzić. Jeśli dane wejściowe nie zostaną sprawdzone, zadziała to w systemie, a nieprzyjemne rzeczy przyjdą później. Nawet awaria jest lepsza!
Andy Canfield
Całkowicie się z tobą zgadzam: moja typowa metoda lub funkcja zaczyna się od sekwencji kontroli, aby upewnić się, że dane wejściowe są prawidłowe.
Giorgio
Dzisiaj ponownie otrzymałem potwierdzenie, że strategia „sprawdź wszystko, nie ufaj nic” jest często dobrym pomysłem. Mój kolega miał wyjątek NULL ze względu na brakujący czek. Okazało się, że w tym kontekście poprawne było posiadanie wskaźnika NULL, ponieważ niektóre dane nie zostały załadowane, i poprawne było sprawdzenie wskaźnika i po prostu nic nie robić, gdy jest on NULL. :-)
Giorgio
7

Masz już dobry pomysł

Które z dwóch podejść do obsługi niewłaściwych danych wejściowych doradziłbyś?

Niespójne dane wejściowe -> brak działania + powiadomienie

albo lepiej

Niespójne dane wejściowe -> odpowiednio obsługiwane działanie

Nie możesz tak naprawdę podejść do programowania w stylu „ciasteczek” (możesz), ale skończyłbyś się formalnym projektem, który robi rzeczy z przyzwyczajenia, a nie ze świadomego wyboru.

Temperament dogmatyzmu z pragmatyzmem.

Steve McConnell powiedział to najlepiej

Steve McConnell prawie napisał książkę ( Code Complete ) o programowaniu obronnym i była to jedna z metod, które doradził, abyś zawsze sprawdzał poprawność swoich danych wejściowych.

Nie pamiętam, czy Steve o tym wspominał, jednak powinieneś rozważyć zrobienie tego dla nieprywatnych metod i funkcji, i tylko dla innych, gdy zostanie to uznane za konieczne.

Justin Shield
źródło
2
Zamiast publicznych, sugerowałbym, wszystkie nieprywatne metody obrony w językach, które chroniły, współużytkowały lub nie posiadały koncepcji ograniczenia dostępu (wszystko jest jawne, domyślnie).
JustinC,
3

Nie ma tutaj „poprawnej” odpowiedzi, szczególnie bez określenia języka, rodzaju kodu i rodzaju produktu, do którego kod może się dostać. Rozważać:

  • Język ma znaczenie. W Objective-C często wysyłanie wiadomości do zera jest w porządku; nic się nie dzieje, ale program również nie ulega awarii. Java nie ma wyraźnych wskaźników, więc zerowe wskaźniki nie są tutaj dużym problemem. W C musisz być bardziej ostrożny.

  • Być paranoikiem oznacza nierozsądne, nieuzasadnione podejrzenie lub nieufność. Prawdopodobnie nie jest to lepsze dla oprogramowania niż dla ludzi.

  • Twój poziom troski powinien być współmierny do poziomu ryzyka w kodzie i prawdopodobnej trudności w identyfikowaniu pojawiających się problemów. Co dzieje się w najgorszym przypadku? Użytkownik uruchamia ponownie program i kontynuuje od momentu przerwania? Firma traci miliony dolarów?

  • Nie zawsze można zidentyfikować złe dane wejściowe. Możesz religijnie porównać swoje wskaźniki do zera, ale to łapie tylko jedną z 2 ^ 32 możliwych wartości, z których prawie wszystkie są złe.

  • Istnieje wiele różnych mechanizmów radzenia sobie z błędami. Znowu zależy to w pewnym stopniu od języka. Możesz użyć makr asercji, instrukcji warunkowych, testów jednostkowych, obsługi wyjątków, starannego projektowania i innych technik. Żaden z nich nie jest niezawodny i żaden nie jest odpowiedni dla każdej sytuacji.

Więc sprowadza się głównie do tego, gdzie chcesz nałożyć odpowiedzialność. Jeśli piszesz bibliotekę do użytku przez innych, prawdopodobnie chcesz zachować jak największą ostrożność w zakresie otrzymywanych danych wejściowych i starać się emitować pomocne błędy, jeśli to możliwe. W twoich prywatnych funkcjach i metodach możesz użyć aserts, aby wyłapać głupie błędy, ale w inny sposób nałożyć odpowiedzialność na osobę dzwoniącą (to ty), aby nie przekazywać śmieci.

Caleb
źródło
+1 - Dobra odpowiedź. Moją główną obawą jest to, że nieprawidłowe dane wejściowe mogą powodować problemy, które pojawiają się w systemie produkcyjnym (gdy jest za późno, aby coś z tym zrobić). Oczywiście uważam, że masz całkowitą rację mówiąc, że to zależy od szkód, jakie taki problem może wyrządzić użytkownikowi.
Giorgio
Język odgrywa dużą rolę. W PHP połowa kodu metody kończy się sprawdzeniem, jaki jest typ zmiennej i podjęcie odpowiedniej akcji. W Javie, jeśli metoda akceptuje liczbę całkowitą, nie możesz przekazać jej nic innego, więc twoja metoda jest bardziej przejrzysta.
rozdział
1

Zdecydowanie powinno być powiadomienie, takie jak zgłoszony wyjątek. Służy jako informacja od innych programistów, którzy mogą niewłaściwie wykorzystywać kod, który napisałeś (próbując użyć go do czegoś, co nie było zamierzone), że ich dane wejściowe są nieprawidłowe lub powodują błędy. Jest to bardzo przydatne w śledzeniu błędów, podczas gdy jeśli po prostu zwrócisz null, ich kod będzie kontynuowany, dopóki nie spróbują użyć wyniku i uzyskać wyjątek od innego kodu.

Jeśli Twój kod napotka błąd podczas połączenia z innym kodem (być może nieudaną aktualizacją bazy danych), który jest poza zakresem tego fragmentu kodu, naprawdę nie masz nad nim kontroli, a jedynym wyjściem jest zgłoszenie wyjątku wyjaśniającego wiesz (tylko to, co mówi kod, do którego zadzwoniłeś). Jeśli wiesz, że niektóre dane wejściowe nieuchronnie doprowadzą do takiego wyniku, po prostu nie możesz zawracać sobie głowy wykonaniem kodu i zgłosić wyjątek stwierdzający, które dane wejściowe są nieprawidłowe i dlaczego.

W przypadku uwagi bardziej związanej z użytkownikiem końcowym najlepiej jest zwrócić coś opisowego, ale prostego, aby każdy mógł to zrozumieć. Jeśli klient zadzwoni i powie „program się zawiesił, napraw go”, masz dużo pracy, aby wyśledzić, co poszło nie tak i dlaczego, i masz nadzieję, że uda ci się odtworzyć problem. Właściwe postępowanie z wyjątkami może nie tylko zapobiec awarii, ale także dostarczyć cennych informacji. Wywołanie klienta z informacją: „Program wyświetla mi błąd. Mówi, że„ XYZ nie jest poprawnym wejściem dla metody M, ponieważ Z jest zbyt duży ”, lub coś takiego, nawet jeśli nie mają pojęcia, co to znaczy, ty dokładnie wiedzieć, gdzie szukać. Dodatkowo, w zależności od praktyk biznesowych Twojej / Twojej firmy, może to nawet nie być naprawienie tych problemów, więc najlepiej zostawić im dobrą mapę.

Krótka wersja mojej odpowiedzi jest taka, że ​​Twoja pierwsza opcja jest najlepsza.

Inconsistent input -> no action + notify caller
yoozer8
źródło
1

Walczyłem z tym samym problemem podczas przechodzenia przez uniwersytecką klasę programowania. Pochyliłem się w stronę strony paranoicznej i zwykle sprawdzam wszystko, ale powiedziano mi, że to niewłaściwe zachowanie.

Uczono nas „Projektowanie na podstawie umowy”. Nacisk kładziony jest na to, aby warunki wstępne, niezmienniki i warunki końcowe były określone w komentarzach i dokumentach projektowych. Jako osoba wdrażająca moją część kodu, powinienem zaufać architektowi oprogramowania i wzmocnić go, postępując zgodnie ze specyfikacjami zawierającymi warunki wstępne (jakie dane wejściowe muszą obsługiwać moje metody i jakie dane wejściowe nie będę wysyłał) . Nadmierne sprawdzanie w każdym wywołaniu metody powoduje wzdęcia.

Podczas iteracji kompilacji należy stosować twierdzenia w celu weryfikacji poprawności programu (sprawdzanie warunków wstępnych, niezmienników, warunków końcowych). Twierdzenia zostałyby wówczas wyłączone w kompilacji produkcyjnej.

Richard
źródło
0

Używanie „asercji” jest sposobem na powiadomienie innych programistów, że robią to źle, oczywiście tylko metodami „prywatnymi” . Włączanie / wyłączanie ich to tylko jedna flaga do dodania / usunięcia w czasie kompilacji i dlatego łatwo jest usunąć twierdzenia z kodu produkcyjnego. Są też doskonałym narzędziem wiedzieć, czy jesteś w jakiś sposób robi to źle własnymi metodami.

Jeśli chodzi o weryfikację parametrów wejściowych w ramach metod publicznych / chronionych, wolę pracować defensywnie i sprawdzać parametry i zgłaszać wyjątek InvalidArgumentException lub podobny. Dlatego tu są. Zależy to również od tego, czy piszesz interfejs API, czy nie. Jeśli jest to interfejs API, a nawet więcej, jeśli jest to zamknięte źródło, lepiej zweryfikuj wszystko, aby programiści wiedzieli dokładnie, co poszło nie tak. W przeciwnym razie, jeśli źródło jest dostępne dla innych programistów, nie jest ono czarno-białe. Po prostu bądź zgodny ze swoimi wyborami.

Edycja: aby dodać, że jeśli spojrzysz na przykład na Oracle JDK, zobaczysz, że nigdy nie sprawdzają „zerowej” i nie powodują awarii kodu. Ponieważ i tak będzie generować wyjątek NullPointerException, po co zawracać sobie głowę sprawdzaniem wartości NULL i zgłaszaniem jawnego wyjątku. To chyba ma sens.

Jalayn
źródło
W Javie pojawia się wyjątek wskaźnika zerowego. W C ++ wskaźnik zerowy powoduje awarię aplikacji. Być może istnieją inne przykłady: dzielenie przez zero, indeks poza zakresem i tak dalej.
Giorgio