Czy zbędne sprawdzanie stanu sprawdza się w stosunku do najlepszych praktyk?

16

Tworzę oprogramowanie przez ostatnie trzy lata, ale niedawno obudziłem się, jak nieświadomy dobrych praktyk. Doprowadziło mnie to do przeczytania książki „ Czysty kod” , która zmienia moje życie na lepsze, ale staram się uzyskać wgląd w niektóre z najlepszych podejść do pisania moich programów.

Mam program w języku Python, w którym ...

  1. użyj argparse required=Truedo wymuszenia dwóch argumentów, które są nazwami plików. pierwszy to nazwa pliku wejściowego, drugi to nazwa pliku wyjściowego
  2. mają funkcję, readFromInputFilektóra najpierw sprawdza, czy wprowadzono nazwę pliku wejściowego
  3. mają funkcję, writeToOutputFilektóra najpierw sprawdza, czy wprowadzono nazwę pliku wyjściowego

Mój program jest na tyle mały, że wierzę, że sprawdzanie w punktach 2 i 3 jest zbędne i powinno zostać usunięte, uwalniając w ten sposób obie funkcje od niepotrzebnego ifstanu. Zostałem jednak przekonany, że „podwójne sprawdzanie jest w porządku” i może być właściwym rozwiązaniem w programie, w którym funkcje można wywoływać z innej lokalizacji, w której nie występuje analiza argumentów.

(Ponadto, jeśli odczyt lub zapis nie powiedzie się, mam try exceptw każdej funkcji, aby wywołać odpowiedni komunikat o błędzie.)

Moje pytanie brzmi: czy najlepiej jest unikać sprawdzania wszystkich zbędnych warunków? Czy logika programu powinna być tak solidna, że ​​kontrole należy wykonywać tylko raz? Czy są jakieś dobre przykłady, które ilustrują to lub odwrotnie?

EDYCJA: Dziękuję wszystkim za odpowiedzi! Nauczyłem się czegoś od każdego. Widok tak wielu perspektyw pozwala mi lepiej zrozumieć, jak podejść do tego problemu i znaleźć rozwiązanie na podstawie moich wymagań. Dziękuję Ci!

Praca dyplomowa
źródło
Tutaj jest mocno uogólniona wersja pytanie: softwareengineering.stackexchange.com/questions/19549/... . Nie powiedziałbym, że jest duplikat, ponieważ ma znacznie większy cel, ale może to pomaga.
Doc Brown,

Odpowiedzi:

15

To, o co prosisz, nazywa się „solidnością” i nie ma dobrej ani złej odpowiedzi. Zależy to od wielkości i złożoności programu, liczby pracujących w nim osób oraz znaczenia wykrywania awarii.

W małych programach, które piszesz sam i tylko dla siebie, niezawodność jest zwykle znacznie mniejszą kwestią niż w przypadku pisania złożonego programu, który składa się z wielu komponentów, być może napisanych przez zespół. W takich systemach istnieją granice między komponentami w postaci publicznych interfejsów API, a na każdej granicy często dobrym pomysłem jest sprawdzanie poprawności parametrów wejściowych, nawet jeśli „logika programu powinna być tak solidna, aby kontrole były zbędne „. To sprawia, że ​​wykrywanie błędów jest znacznie łatwiejsze i pomaga zmniejszyć czasy debugowania.

W twoim przypadku musisz sam zdecydować, jakiego rodzaju cyklu życia spodziewasz się w swoim programie. Czy jest to program, który będzie używany i utrzymywany przez lata? W takim razie dodanie nadmiarowego czeku jest prawdopodobnie lepsze, ponieważ nie będzie mało prawdopodobne, że kod zostanie zrefaktoryzowany w przyszłości, a twoje readi writefunkcje mogą być używane w innym kontekście.

A może jest to mały program przeznaczony wyłącznie do nauki lub zabawy? Wtedy te podwójne kontrole nie będą konieczne.

W kontekście „Czystego kodu” można zapytać, czy podwójne sprawdzenie narusza zasadę OSUSZANIA. W rzeczywistości czasami robi to, przynajmniej w niewielkim stopniu: sprawdzanie poprawności danych wejściowych może być interpretowane jako część logiki biznesowej programu, a posiadanie go w dwóch miejscach może prowadzić do typowych problemów konserwacyjnych spowodowanych naruszeniem DRY. Wytrzymałość kontra DRY jest często kompromisem - odporność wymaga redundancji w kodzie, podczas gdy DRY próbuje zminimalizować redundancję. Wraz ze wzrostem złożoności programu niezawodność staje się coraz ważniejsza niż SUSZENIE podczas sprawdzania poprawności.

Na koniec pozwól mi podać przykład, co to oznacza w twoim przypadku. Załóżmy, że twoje wymagania zmienią się na coś w rodzaju

  • program powinien również działać z jednym argumentem, nazwą pliku wejściowego, jeśli nie podano nazwy pliku wyjściowego, jest on automatycznie konstruowany na podstawie nazwy pliku wejściowego poprzez zastąpienie przyrostka.

Czy to powoduje, że konieczna jest zmiana podwójnej weryfikacji w dwóch miejscach? Prawdopodobnie nie, taki wymóg prowadzi do jednej zmiany podczas wywoływania argparse, ale bez zmiany writeToOutputFile: ta funkcja nadal będzie wymagać nazwy pliku. Tak więc w twoim przypadku głosowałbym za dwukrotnym sprawdzeniem poprawności danych wejściowych, ryzyko wystąpienia problemów konserwacyjnych z powodu posiadania dwóch miejsc do zmiany jest IMHO znacznie niższe niż ryzyko wystąpienia problemów konserwacyjnych z powodu zamaskowanych błędów spowodowanych zbyt małą liczbą kontroli.

Doktor Brown
źródło
„... granice między komponentami w postaci publicznych interfejsów API ...” Obserwuję, że „klasy przekraczają granice”. Potrzebna jest więc klasa; spójna klasa domen biznesowych. Wnioskuję z tego OP, że wszechobecna zasada „jest prosta, więc nie potrzebuję klasy” działa tutaj. Może istnieć prosta klasa owijająca „obiekt podstawowy”, wymuszająca reguły biznesowe, takie jak „plik musi mieć nazwę”, która nie tylko przesusza istniejący kod, ale zachowuje go w przyszłości.
radarbob,
@radarbob: to, co napisałem, nie ogranicza się do OOP ani komponentów w postaci klas. Dotyczy to także dowolnych bibliotek z publicznym interfejsem API, obiektowym lub nie.
Doc Brown,
5

Redundancja nie jest grzechem. Jest niepotrzebna redundancja.

  1. Jeśli readFromInputFile()i writeToOutputFile()są funkcjami publicznymi (i zgodnie z konwencjami nazewnictwa Pythona są, ponieważ ich nazwy nie zaczynają się od dwóch znaków podkreślenia), to funkcje te mogą kiedyś być używane przez kogoś, kto całkowicie uniknie argumentacji. Oznacza to, że gdy pomijają argumenty, nie widzą niestandardowego komunikatu o błędzie argparse.

  2. Jeśli readFromInputFile()i writeToOutputFile()sprawdzisz same parametry, ponownie zobaczysz niestandardowy komunikat o błędzie, który wyjaśnia potrzebę nazw plików.

  3. Jeśli readFromInputFile()i writeToOutputFile()nie sprawdzić parametry sami, pojawi się żaden komunikat o błędzie zwyczaj. Użytkownik będzie musiał sam ustalić wynikowy wyjątek.

Wszystko sprowadza się do 3. Napisz kod, który faktycznie korzysta z tych funkcji, unikając argumentacji i wygeneruj komunikat błędu. Wyobraź sobie, że w ogóle nie zaglądałeś do tych funkcji i ufasz tylko ich nazwom, aby zapewnić wystarczającą wiedzę do użycia. Kiedy to wszystko, co wiesz, czy jest jakiś sposób, aby pomylić wyjątek? Czy istnieje potrzeba dostosowanego komunikatu o błędzie?

Trudno jest wyłączyć tę część mózgu, która pamięta wnętrze tych funkcji. Tak bardzo, że niektórzy zalecają napisanie kodu używającego przed kodem, który zostanie wykorzystany. W ten sposób dochodzisz do problemu, wiedząc już, jak wyglądają rzeczy z zewnątrz. Nie musisz robić TDD, aby to zrobić, ale jeśli zrobisz TDD, najpierw będziesz wchodził z zewnątrz.

candied_orange
źródło
4

Dobrym rozwiązaniem jest to, w jaki sposób uczynisz swoje metody samodzielnymi i nadającymi się do ponownego wykorzystania . Oznacza to, że metody powinny wybaczać w tym, co akceptują i powinny mieć dobrze zdefiniowane wyniki (dokładnie w tym, co zwracają). Oznacza to również, że powinni oni być w stanie z wdziękiem obsługiwać wszystko, co im przekazano, i nie przyjmować żadnych założeń dotyczących charakteru danych wejściowych, jakości, czasu itp.

Jeśli programista ma zwyczaj pisania metod, które przyjmują założenia dotyczące tego, co zostało przekazane, na podstawie pomysłów takich jak „jeśli to jest zepsute, mamy większe problemy” lub „parametr X nie może mieć wartości Y, ponieważ reszta kod temu zapobiega ”, nagle nie ma już tak naprawdę niezależnych, oddzielonych komponentów. Twoje komponenty są zasadniczo zależne od szerszego systemu. Jest to rodzaj subtelnego ścisłego połączenia i prowadzi do wykładniczego wzrostu całkowitego kosztu posiadania wraz ze wzrostem złożoności systemu.

Pamiętaj, że może to oznaczać, że weryfikujesz te same informacje więcej niż raz. Ale to jest OK. Każdy komponent jest odpowiedzialny za własną walidację na swój własny sposób . Nie jest to naruszenie DRY, ponieważ walidacje są dokonywane przez oddzielone niezależne komponenty, a zmiana walidacji w jednym niekoniecznie musi być replikowana dokładnie w drugim. Tutaj nie ma redundancji. X ma obowiązek sprawdzić swoje dane wejściowe pod kątem własnych potrzeb i przekazać je Y. Y ma swój własny obowiązek sprawdzić dane wejściowe pod kątem własnych potrzeb .

Brad Thomas
źródło
1

Załóżmy, że masz funkcję (w C)

void readInputFile (const char* path);

I nie możesz znaleźć żadnej dokumentacji dotyczącej ścieżki. A potem patrzysz na wdrożenie i mówi

void readInputFile (const char* path)
{
    assert (path != NULL && strlen (path) > 0);

Ten test nie tylko sprawdza dane wejściowe do funkcji, ale także informuje użytkownika funkcji, że ścieżka nie może mieć wartości NULL lub pustego łańcucha.

gnasher729
źródło
0

Ogólnie rzecz biorąc, podwójne sprawdzanie nie zawsze jest dobre lub złe. W twoim przypadku zawsze jest wiele aspektów pytania, od których zależy sprawa. W Twoim przypadku:

  • Jak duży jest ten program? Im jest mniejszy, tym bardziej oczywiste jest, że dzwoniący postępuje właściwie. Kiedy twój program staje się większy, ważniejsze staje się dokładne określenie, jakie są warunki wstępne i końcowe każdej procedury.
  • argumenty są już sprawdzane przez argparsemoduł. Często złym pomysłem jest użycie biblioteki, a następnie samodzielne wykonanie zadania. Po co więc korzystać z biblioteki?
  • Jak prawdopodobne jest, że Twoja metoda zostanie ponownie użyta w kontekście, w którym program wywołujący nie sprawdza argumentów? Im bardziej prawdopodobne, tym ważniejsze jest sprawdzanie poprawności argumentów.
  • Co się stanie, jeśli argument ma iść brakuje? Nie znalezienie pliku wejściowego prawdopodobnie natychmiast przestanie przetwarzać. To prawdopodobnie oczywisty tryb awarii, który można łatwo naprawić. Podstępnym rodzajem błędów są te, w których program wesoło działa i daje nieprawidłowe wyniki bez zauważenia .
Kilian Foth
źródło
0

Twoje podwójne kontrole wydają się być w miejscach, w których są rzadko używane. Dzięki tym sprawdzeniom program jest po prostu bardziej niezawodny:

Czek za dużo nie zaszkodzi, jeden za mało może.

Jeśli jednak sprawdzasz wewnątrz często powtarzanej pętli, powinieneś pomyśleć o usunięciu nadmiarowości, nawet jeśli sama kontrola w większości przypadków nie jest kosztowna w porównaniu do tego, co następuje po kontroli.

qwerty_so
źródło
A ponieważ już go masz, nie warto go usuwać, chyba że jest w pętli lub coś takiego.
StarWeaver,
0

Być może mógłbyś zmienić swój punkt widzenia:

Jeśli coś pójdzie nie tak, jaki jest wynik? Czy zaszkodzi twojej aplikacji / użytkownikowi?

Oczywiście zawsze można się spierać, czy więcej lub mniej kontroli jest lepszych czy gorszych, ale jest to raczej scholastyczne pytanie. A ponieważ masz do czynienia z oprogramowaniem w świecie rzeczywistym , istnieją konsekwencje w świecie rzeczywistym.

Z kontekstu, który podajesz:

  • jeden plik wejściowy A
  • jeden plik wyjściowy B

Zakładam, że robisz transformacji od A do B . Jeśli A i B są małe, a transformacja jest niewielka, jakie są konsekwencje?

1) Zapomniałeś podać, skąd czytać: Wynik jest niczym . A czas wykonania będzie krótszy niż oczekiwano. Patrzysz na wynik - lub lepiej: szukaj brakującego wyniku, zobacz, że źle użyłeś polecenia, zacznij od nowa i wszystko w porządku

2) Zapomniałeś podać plik wyjściowy. Powoduje to różne scenariusze:

a) Dane wejściowe są odczytywane jednocześnie. Następnie rozpoczyna się transformacja i wynik powinien zostać zapisany, ale zamiast tego pojawia się błąd. W zależności od czasu użytkownik musi czekać (w zależności od masy danych, które mogłyby zostać przetworzone), może to być denerwujące.

b) Dane wejściowe są odczytywane krok po kroku. Następnie proces pisania natychmiast kończy działanie, jak w (1), a użytkownik zaczyna od nowa.

Niechlujstwa sprawdzanie może być postrzegane jako OK w pewnych okolicznościach. Zależy to całkowicie od twojego przypadku użycia i twoich intencji.

Dodatkowo: Powinieneś unikać paranoi i nie robić zbyt wielu podwójnych kontroli.

Thomas Junk
źródło
0

Twierdziłbym, że testy nie są zbędne.

  • Masz dwie funkcje publiczne, które wymagają nazwy pliku jako parametru wejściowego. Należy zweryfikować ich parametry. Funkcje te mogą potencjalnie być użyte w każdym programie, który potrzebuje ich funkcjonalności.
  • Masz program, który wymaga dwóch argumentów, które muszą być nazwami plików. Zdarza się korzystać z funkcji. Program powinien sprawdzić swoje parametry.

Podczas gdy nazwy plików są sprawdzane dwukrotnie, są one sprawdzane w różnych celach. W małym programie, w którym można ufać parametrom funkcji, zostały zweryfikowane, kontrole funkcji można uznać za zbędne.

Bardziej niezawodne rozwiązanie miałoby jeden lub dwa walidatory nazw plików.

  • W przypadku pliku wejściowego możesz sprawdzić, czy parametr określa plik do odczytu.
  • W przypadku pliku wyjściowego możesz sprawdzić, czy parametr jest plikiem do zapisu lub poprawną nazwą pliku, którą można utworzyć i zapisać.

Używam dwóch reguł, kiedy należy wykonywać działania:

  • Zrób je jak najwcześniej. Działa to dobrze w przypadku rzeczy, które zawsze będą wymagane. Z punktu widzenia tego programu jest to sprawdzenie wartości argv, a późniejsze sprawdzanie poprawności w logice programów byłoby zbędne. Jeśli funkcje zostaną przeniesione do biblioteki, nie będą już nadmiarowe, ponieważ biblioteka nie może ufać, że wszystkie osoby wywołujące zweryfikowały parametry.
  • Zrób je tak późno, jak to możliwe. Działa to wyjątkowo dobrze w przypadku rzeczy, które rzadko będą wymagane. Z punktu widzenia tego programu są to kontrole parametrów funkcji.
BillThor
źródło
0

Czek jest zbędny. Naprawienie tego wymaga jednak usunięcia readFromInputFile i writeToOutputFile i zastąpienia ich readFromStream i writeToStream.

W miejscu, w którym kod odbiera strumień plików, wiesz, że masz prawidłowy strumień podłączony do prawidłowego pliku lub cokolwiek innego, z czym można połączyć strumień. Pozwala to uniknąć zbędnych kontroli.

Możesz wtedy zapytać: cóż, nadal musisz gdzieś otworzyć strumień. Tak, ale dzieje się tak wewnętrznie w metodzie analizy argumentów. Masz tam dwie kontrole, jedną dla sprawdzenia, czy nazwa pliku jest wymagana, drugą sprawdzenie, czy plik wskazany przez nazwę pliku jest poprawnym plikiem w danym kontekście (np. Plik wejściowy istnieje, katalog wyjściowy jest zapisywalny). Są to różnego rodzaju kontrole, więc nie są zbędne i występują w ramach metody analizy argumentów (obwód aplikacji), a nie w aplikacji podstawowej.

Lie Ryan
źródło