Funkcja zwraca true / false vs. void w przypadku powodzenia i zgłasza wyjątek w przypadku niepowodzenia

22

Buduję interfejs API, funkcję, która przesyła plik. Ta funkcja nie zwróci niczego / unieważni, jeśli plik został poprawnie przesłany i zgłasza wyjątek, gdy wystąpił jakiś problem.

Dlaczego wyjątek, a nie tylko fałsz? Ponieważ wewnątrz wyjątku mogę określić przyczynę niepowodzenia (brak połączenia, brak nazwy pliku, nieprawidłowe hasło, brak opisu pliku itp.). Chciałem zbudować niestandardowy wyjątek (z pewnym wyliczeniem, aby pomóc użytkownikowi interfejsu API obsłużyć wszystkie błędy).

Czy to dobra praktyka, czy też lepiej jest zwracać obiekt (z logiczną wartością wewnętrzną, opcjonalnym komunikatem o błędzie i wyliczeniem błędów)?

Accollativo
źródło
59
Prawda i fałsz idą w parze. Pustka i wyjątek pasują do siebie. Prawda i wyjątek idą w parze jak koty i podatki. Pewnego dnia mogę czytać twój kod. Proszę, nie rób mi tego.
candied_orange
4
Lubię wzorce, w których zwracany jest obiekt, który zawiera albo sukces, albo porażkę. Na przykład Rust ma Result<T, E>gdzie Tjest wynikiem, jeśli się powiedzie, i Ejest błędem, jeśli się nie powiedzie. Zgłoszenie wyjątku (może być - co nie jest tak pewne w Javie) może być kosztowne, ponieważ wymaga rozwinięcia stosu wywołań, ale utworzenie obiektu wyjątku jest tanie. Oczywiście trzymaj się ustalonych wzorców języka, którego używasz, ale nie łącz boolean i wyjątków takich jak ten.
Dan Pantry
4
Uważaj tutaj. Możesz iść w dół do króliczej nory. Gdy Twój program jest w stanie poradzić sobie z problemem, zwykle łatwiej jest po prostu go wykryć i rozwiązać, sprawdzając warunki wstępne. Rzadko wychwytujesz wyjątek, sprawdzasz jego dane w kodzie i próbujesz ponownie po rozwiązaniu problemu. W związku z tym rzadko ma wartość bardzo wiele typów wyjątków. Jeśli próbujesz rejestrować problemy, ma to o wiele większy sens, ale nie spodziewaj się, że napiszesz kilka bloków catch ze znaczną ilością kodu.
jpmc26
4
@DanPantry W Javie stos wywołań jest sprawdzany podczas tworzenia wyjątku, a nie podczas jego zgłaszania . fillInStackTrace();nazywa się superkonstruktorem w klasieThrowable
Simon Forsberg

Odpowiedzi:

35

Zgłoszenie wyjątku jest po prostu dodatkowym sposobem, aby metoda zwróciła wartość. Dzwoniący może równie łatwo sprawdzić wartość zwracaną, jak złapać wyjątek i to sprawdzić. Dlatego przed podjęciem decyzji throwi returnwymaga innych kryteriów.

Rzadkich wyjątków należy często unikać, jeśli zagraża to wydajności programu (konstruowanie obiektu wyjątku i odwijanie stosu wywołań to dla komputera znacznie więcej pracy niż tylko wypychanie na niego wartości). Ale jeśli celem Twojej metody jest przesłanie pliku, wąskim gardłem zawsze będzie we / wy sieci i systemu plików, więc optymalizacja metody zwrotu nie ma sensu.

Niewłaściwym pomysłem jest również wprowadzanie wyjątków dla tego, co powinno być prostym przepływem kontrolnym (np. Gdy wyszukiwanie zakończy się powodzeniem przez znalezienie jego wartości), ponieważ narusza to oczekiwania użytkowników API. Ale metoda, która nie spełnia swojego celu, jest wyjątkowym przypadkiem (a przynajmniej powinna być), więc nie widzę powodu, aby nie zgłaszać wyjątku. Jeśli to zrobisz, równie dobrze możesz uczynić go niestandardowym, bardziej informacyjnym wyjątkiem (ale dobrym pomysłem jest uczynienie go podklasą standardowego, bardziej ogólnego wyjątku, takiego jak IOException).

Kilian Foth
źródło
7
Jeśli oczekiwany wynik nie powiedzie się w przypadku żądania przesłania pliku, byłbym zaskoczony wyjątkiem zgłoszonym w twarz (szczególnie jeśli w sygnaturze metody znajduje się wartość zwrotna).
hoffmale
1
Zauważ, że powodem rozszerzenia standardowego wyjątku jest to, że wielu (prawdopodobnie nawet większość) rozmówców nie napisze kodu, aby spróbować rozwiązać problem. W rezultacie większość dzwoniących będzie chciała prostego „złapać tę dużą grupę wyjątków”, bez konieczności ich jawnego wypisywania.
jpmc26
4
@gbjbaanb Właśnie dlatego należy stosować wyjątki, gdy naprawdę istnieje jakaś niemożliwa do naprawienia sytuacja. Gdy próbujesz otworzyć plik i nie można go odczytać, jest to dobry przypadek wyjątku. Podczas sprawdzania istnienia pliku należy użyć wartości logicznej, ponieważ jest prawdopodobne, że pliku nie ma.
Malcolm,
21
Kilian mówi w rekurencyjnym haśle „wyjątki dotyczą wyjątkowych sytuacji”, wyjątkowymi sytuacjami są sytuacje, gdy funkcja nie może spełnić swojego celu. Jeśli funkcja ma czytać plik, a pliku nie można odczytać , jest to wyjątkowe . Jeśli funkcja ma powiedzieć, czy plik istnieje (nie wspominając o potencjalnym TOCTOU), to plik nieistniejący nie jest wyjątkowy, ponieważ funkcja może nadal robić to, co powinna. Jeśli funkcja ma potwierdzać, że plik istnieje, to nie istnieje, jest wyjątkowy, ponieważ to właśnie oznacza „twierdzić”.
Steve Jessop,
10
Zauważ, że jedyną różnicą między funkcją, która informuje, czy plik istnieje, a funkcją, która twierdzi, że plik istnieje, jest to, czy przypadek nieistnienia pliku jest wyjątkowy. Ludzie czasem mówią tak, jakby to była właściwość warunku błędu, niezależnie od tego, czy „jest wyjątkowa”. To nie jest właściwość warunku błędu, to połączona właściwość warunku błędu i celu funkcji, w której występuje. W przeciwnym razie nigdy nie złapalibyśmy wyjątków.
Steve Jessop,
31

Nie ma absolutnie żadnego powodu, aby powrócić truedo sukcesu, jeśli nie powrócisz falsew przypadku niepowodzenia. Jak powinien wyglądać kod klienta?

if (result = tryMyAPICall()) {
    // business logic
}
else {
    // this will *never* happen anyways
}

W takim przypadku dzwoniący potrzebuje bloku try-catch, ale wtedy może lepiej napisać:

try {
    result = tryMyAPICall();
    // business logic
    // will only reach this line when no exception
    // no reason to use an if-condition
} catch (SomeException se) { }

Zatem truewartość zwracana jest całkowicie nieistotna dla osoby dzwoniącej. Więc po prostu zachowaj metodę void.


Zasadniczo istnieją trzy sposoby projektowania trybów awaryjnych.

  1. Zwraca wartość prawda / fałsz
  2. Użyj voidwyjątku „wyrzuć” (zaznaczone)
  3. zwraca pośredni obiekt wynikowy .

Powrót true/false

Jest to używane w niektórych starszych interfejsach API, głównie typu C. Wady są obviuos, nie masz pojęcia, co poszło nie tak. PHP robi to dość często, co prowadzi do takiego kodu:

if (xyz_parse($data) === FALSE)
   $error = xyz_last_error();

W kontekstach wielowątkowych jest jeszcze gorzej.

Zgłaszaj (zaznaczone) wyjątki

To dobry sposób na zrobienie tego. W niektórych momentach możesz spodziewać się niepowodzenia. Java robi to z gniazdami. Podstawowym założeniem jest, że połączenie powinno się powieść, ale wszyscy wiedzą, że niektóre operacje mogą się nie powieść. Wśród nich są połączenia gniazd. Dzwoniący zostaje zmuszony do obsługi awarii. jest to ładna konstrukcja, ponieważ zapewnia, że ​​osoba dzwoniąca rzeczywiście poradzi sobie z awarią, i daje mu elegancki sposób radzenia sobie z awarią.

Zwraca wynikowy obiekt

To kolejny dobry sposób na poradzenie sobie z tym. Jest często używany do analizowania lub po prostu do sprawdzania poprawności.

ValidationResult result = parser.validate(data);
if (result.isValid())
    // business logic
else
    error = result.getvalidationError();

Ładna, czysta logika dla dzwoniącego.

Trwa dyskusja, kiedy użyć drugiego przypadku, a kiedy trzeciego. Niektóre osoby uważają, że wyjątki powinny być wyjątkowe i że nie należy projektować z uwzględnieniem wyjątków i prawie zawsze będą korzystać z trzeciej opcji. W porządku Ale sprawdziliśmy wyjątki w Javie, więc nie widzę powodu, aby z nich nie korzystać. Używam sprawdzonych wyrażeń, gdy podstawowym założeniem jest, że wywołanie powinno zostać zakończone (jak użycie gniazda), ale niepowodzenie jest możliwe, i używam trzeciej opcji, gdy jest bardzo niejasne, że wywołanie powinno zostać zwiększone (jak sprawdzanie poprawności danych). Ale są na ten temat różne opinie.

W twoim przypadku wybrałbym void+ Exception. Oczekujesz, że przesyłanie pliku się zwiększy, a kiedy to zrobi, będzie wyjątkowe. Ale dzwoniący jest zmuszony obsłużyć ten tryb awarii i możesz zwrócić wyjątek, który poprawnie opisuje, jaki rodzaj błędu wystąpił.

Polygnome
źródło
5

To naprawdę sprowadza się do tego, czy awaria jest wyjątkowa, czy oczekiwana .

Jeśli błąd nie jest czymś, czego zwykle się spodziewasz, jest prawdopodobne, że użytkownik API po prostu wywoła twoją metodę bez specjalnej obsługi błędów. Zgłoszenie wyjątku pozwala na podniesienie stosu do miejsca, w którym zostanie zauważony.

Jeśli z drugiej strony błędy są powszechne, należy zoptymalizować programistę, który je sprawdza, a try-catchklauzule są nieco bardziej kłopotliwe niż seria if/eliflub switch.

Zawsze projektuj interfejs API w myśleniu osoby, która go używa.

Xiong Chiamiov
źródło
1
W moim przypadku, jak ktoś zauważył, powinna wystąpić wyjątkowa awaria, że ​​użytkownik interfejsu API nie może załadować pliku.
Accollativo
4

Nie zwracaj wartości logicznej, jeśli nigdy nie zamierzasz wracać false. Po prostu stwórz metodę voidi dokument, że zgłosi wyjątek ( IOExceptionodpowiedni) w przypadku niepowodzenia.

Powodem tego jest to, że jeśli zwrócisz wartość logiczną, użytkownik Twojego interfejsu API może stwierdzić, że może to zrobić:

if (fileUpload(file) == false) {
    // Handle failure.
}

To oczywiście nie zadziała; oznacza to, że istnieje niezgodność między umową metody a jej zachowaniem. Jeśli zgłosisz sprawdzony wyjątek, użytkownik metody musi obsłużyć awarię:

try {
    fileUpload(file);
} catch (IOException e) {
    // Handle failure.
}
JeroenHoek
źródło
3

Jeśli metoda ma wartość zwracaną, zgłoszenie wyjątku może zaskoczyć użytkowników. Jeśli widzę, że metoda zwraca wartość logiczną, jestem całkiem przekonany, że zwróci wartość prawda, jeśli się powiedzie, lub fałsz, jeśli nie, i utworzę strukturę kodu za pomocą klauzuli if-else. Jeśli Twój wyjątek i tak będzie oparty na wyliczeniu, możesz zamiast tego zwrócić wartość wyliczenia, podobnie jak w Windows HResults, i zachować wartość wyliczenia na wypadek, gdy metoda zakończy się powodzeniem.

Irytujące jest również wyjątek rzucania metod, gdy nie jest to ostatni krok procedury. Konieczność napisania try-catch i przekierowania kontroli do bloku catch to dobry przepis na spaghetti i należy go unikać.

Jeśli idziesz do przodu z wyjątkami, spróbuj zamiast tego anulować void, użytkownicy stwierdzą, że zakończy się sukcesem, jeśli nie zostaną zgłoszone żadne wyjątki, i żaden nie spróbuje użyć klauzuli if-else, aby zmienić kierunek kontroli.

ccControl
źródło
1
Wyliczenie było tylko pomysłem, aby pomóc użytkownikowi interfejsu API obsłużyć różne typy błędów bez analizowania komunikatu o błędzie. Zatem z twojego punktu widzenia w moim przypadku lepiej jest zwrócić wyliczenie i dodać wyliczenie dla „OK”.
Accollativo