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 ...
- użyj argparse
required=True
do wymuszenia dwóch argumentów, które są nazwami plików. pierwszy to nazwa pliku wejściowego, drugi to nazwa pliku wyjściowego - mają funkcję,
readFromInputFile
która najpierw sprawdza, czy wprowadzono nazwę pliku wejściowego - mają funkcję,
writeToOutputFile
któ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 if
stanu. 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 except
w 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!
źródło
Odpowiedzi:
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
read
iwrite
funkcje 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
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 zmianywriteToOutputFile
: 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.źródło
Redundancja nie jest grzechem. Jest niepotrzebna redundancja.
Jeśli
readFromInputFile()
iwriteToOutputFile()
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.Jeśli
readFromInputFile()
iwriteToOutputFile()
sprawdzisz same parametry, ponownie zobaczysz niestandardowy komunikat o błędzie, który wyjaśnia potrzebę nazw plików.Jeśli
readFromInputFile()
iwriteToOutputFile()
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.
źródło
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 .
źródło
Załóżmy, że masz funkcję (w C)
I nie możesz znaleźć żadnej dokumentacji dotyczącej ścieżki. A potem patrzysz na wdrożenie i mówi
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.
źródło
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:
argparse
moduł. Często złym pomysłem jest użycie biblioteki, a następnie samodzielne wykonanie zadania. Po co więc korzystać z biblioteki?źródło
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.
źródło
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:
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.
źródło
Twierdziłbym, że testy nie są zbędne.
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.
Używam dwóch reguł, kiedy należy wykonywać działania:
źródło
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.
źródło