Myślę, że przeczytałem ten sam wywiad z Brucem Eckelem, który przeprowadziłeś - i zawsze mnie to denerwowało. W rzeczywistości argument został wysunięty przez rozmówcę (jeśli rzeczywiście jest to post, o którym mówisz) Anders Hejlsberg, genialny MS za .NET i C #.
http://www.artima.com/intv/handcuffs.html
Choć jestem fanem Hejlsberga i jego twórczości, ten argument zawsze wydawał mi się fałszywy. Zasadniczo sprowadza się do:
„Sprawdzone wyjątki są złe, ponieważ programiści po prostu nadużywają ich, zawsze je łapiąc i odrzucając, co prowadzi do ukrywania i ignorowania problemów, które w innym przypadku zostałyby przedstawione użytkownikowi”.
Przez „inaczej przedstawione użytkownikowi” mam na myśli to, że jeśli użyjesz wyjątku środowiska wykonawczego, leniwy programista po prostu go zignoruje (zamiast złapać go z pustym blokiem catch) i użytkownik go zobaczy.
Podsumowanie argumentu jest takie, że „programiści nie będą ich używać poprawnie, a ich niewłaściwe użycie jest gorsze niż ich brak” .
Argument ten zawiera pewną prawdę i podejrzewam, że motywacja Goslingów do nieprzekazywania operatorów w Javie wynika z podobnego argumentu - mylą programistę, ponieważ są często wykorzystywani.
Ale ostatecznie uważam, że jest to fałszywy argument Hejlsberga i być może post-hoc stworzony w celu wyjaśnienia braku, a nie przemyślana decyzja.
Twierdziłbym, że podczas gdy nadmierne wykorzystanie sprawdzonych wyjątków jest złą rzeczą i prowadzi do niechlujnej obsługi przez użytkowników, ale ich właściwe użycie pozwala programistom API dać ogromne korzyści programistom klienta API.
Teraz programista API musi uważać, aby nie rzucać sprawdzonymi wyjątkami w inne miejsce, w przeciwnym razie po prostu drażnią programistę klienta. Bardzo leniwy programista kliencki (Exception) {}
ucieknie się do złapania, gdy Hejlsberg ostrzega, a wszelkie korzyści zostaną utracone i nastąpi piekło. Ale w niektórych okolicznościach dobry sprawdzony wyjątek po prostu nie zastąpi.
Dla mnie klasycznym przykładem jest interfejs API do otwierania plików. Każdy język programowania w historii języków (przynajmniej w systemach plików) ma gdzieś interfejs API, który pozwala otworzyć plik. I każdy programista kliencki korzystający z tego interfejsu API wie, że musi sobie poradzić ze sprawą, że plik, który próbują otworzyć, nie istnieje. Pozwólcie, że sformułuję to: Każdy programista klienta korzystający z tego interfejsu API powinien wiedzieć , że musi poradzić sobie z tą sprawą. I jest rub: czy programista API może pomóc im wiedzieć, że powinni sobie z tym poradzić, komentując samemu, czy też może nalegać aby klient zajął się tym.
W języku C idiom wygląda mniej więcej tak
if (f = fopen("goodluckfindingthisfile")) { ... }
else { // file not found ...
gdzie fopen
oznacza niepowodzenie, zwracając 0, a C (głupio) pozwala traktować 0 jako wartość logiczną i ... Zasadniczo uczysz się tego idiomu i wszystko w porządku. Ale co jeśli jesteś noobem i nie nauczyłeś się tego idiomu. Potem oczywiście zaczynasz
f = fopen("goodluckfindingthisfile");
f.read(); // BANG!
i uczyć się na własnej skórze.
Zauważ, że mówimy tu tylko o silnie typowanych językach: Istnieje jasne pojęcie o tym, czym jest interfejs API w silnie typowanym języku: To jest zestaw funkcji (metod) do użycia z jasno zdefiniowanym protokołem dla każdego z nich.
Ten jasno zdefiniowany protokół jest zazwyczaj definiowany przez sygnaturę metody. Tutaj fopen wymaga, abyś przekazał mu ciąg (lub znak * w przypadku C). Jeśli podasz coś innego, pojawi się błąd kompilacji. Nie przestrzegałeś protokołu - nie używasz poprawnie interfejsu API.
W niektórych (niejasnych) językach typ zwracany jest również częścią protokołu. Jeśli spróbujesz wywołać odpowiednik fopen()
w niektórych językach bez przypisywania go do zmiennej, otrzymasz również błąd czasu kompilacji (możesz to zrobić tylko przy użyciu funkcji void).
Chodzi mi o to, że: w języku o typie statycznym programista API zachęca klienta do właściwego używania interfejsu API, uniemożliwiając kompilację kodu klienta, jeśli popełni oczywiste błędy.
(W dynamicznie pisanym języku, takim jak Ruby, możesz przekazać wszystko, na przykład float, jako nazwę pliku - i to się skompiluje. Po co męczyć użytkownika sprawdzonymi wyjątkami, jeśli nawet nie zamierzasz kontrolować argumentów metody. przedstawione tutaj argumenty dotyczą tylko języków o typie statycznym).
A co ze sprawdzonymi wyjątkami?
Oto jeden z interfejsów API Java, których można użyć do otwarcia pliku.
try {
f = new FileInputStream("goodluckfindingthisfile");
}
catch (FileNotFoundException e) {
// deal with it. No really, deal with it!
... // this is me dealing with it
}
Widzisz ten haczyk? Oto podpis tej metody API:
public FileInputStream(String name)
throws FileNotFoundException
Uwaga: FileNotFoundException
jest to sprawdzony wyjątek.
Programista API mówi ci: „Możesz użyć tego konstruktora, aby utworzyć nowy FileInputStream, ale Ty
a) musi przekazać nazwę pliku jako ciąg
b) musi zaakceptować możliwość, że pliku nie można znaleźć w czasie wykonywania „
I o to mi chodzi.
Kluczem jest w zasadzie to, co pytanie brzmi: „Rzeczy, które są poza kontrolą programisty”. Moja pierwsza myśl była taka, że on / ona ma na myśli rzeczy, które są poza API kontrolą programistów . Ale tak naprawdę sprawdzone wyjątki, gdy są właściwie używane, powinny naprawdę dotyczyć rzeczy, które są poza kontrolą programisty klienta i programisty API. Myślę, że to klucz do nie nadużywania sprawdzonych wyjątków.
Myślę, że otwieranie plików dobrze ilustruje tę kwestię. Programista API wie, że możesz nadać im nazwę pliku, która okaże się nieistniejąca w momencie wywołania API i że nie będzie w stanie zwrócić ci tego, co chciałeś, ale będzie musiał zgłosić wyjątek. Wiedzą również, że zdarza się to dość regularnie i że programista klienta może oczekiwać, że nazwa pliku będzie poprawna w momencie, w którym napisał wywołanie, ale może być niepoprawny w czasie wykonywania z powodów niezależnych od nich.
Interfejs API wyraźnie to wyjaśnia: będą przypadki, w których ten plik nie będzie istniał w momencie, gdy do mnie zadzwonisz, a ty lepiej sobie z nim poradzić.
Byłoby to wyraźniejsze w przypadku kontr-przypadku. Wyobraź sobie, że piszę interfejs API tabeli. Mam gdzieś model tabeli z interfejsem API zawierającym tę metodę:
public RowData getRowData(int row)
Teraz jako programista API wiem, że będą przypadki, w których niektórzy klienci przekażą wartość ujemną dla wiersza lub wartości wiersza poza tabelą. Mogę więc kusić się, aby rzucić sprawdzony wyjątek i zmusić klienta do zajęcia się nim:
public RowData getRowData(int row) throws CheckedInvalidRowNumberException
(Oczywiście nie nazwałbym tego „sprawdzonym”).
Jest to złe użycie sprawdzonych wyjątków. Kod klienta będzie pełen wywołań do pobierania danych wierszy, z których każde będzie musiało użyć try / catch i po co? Czy zamierzają zgłosić użytkownikowi, że szukano niewłaściwego wiersza? Prawdopodobnie nie - ponieważ niezależnie od interfejsu użytkownika otaczającego mój widok tabeli, nie powinien pozwolić użytkownikowi wejść w stan, w którym żądany jest nielegalny wiersz. Jest to więc błąd programisty klienta.
Programista API nadal może przewidzieć, że klient będzie kodować takie błędy i powinien obsłużyć je z wyjątkiem środowiska wykonawczego takiego jak IllegalArgumentException
.
Z zaznaczonym wyjątkiem getRowData
jest to wyraźnie przypadek, który doprowadzi leniwego programistę Hejlsberga do dodania pustych połowów. Gdy tak się stanie, niedozwolone wartości wierszy nie będą oczywiste nawet dla debugującego testera lub dewelopera klienta, a raczej doprowadzą do błędów domieszkowych, których trudno jest wskazać źródło. Rakiety Arianne wybuchną po starcie.
Okej, więc jest problem: mówię, że sprawdzony wyjątek FileNotFoundException
jest nie tylko dobrą rzeczą, ale niezbędnym narzędziem w zestawie narzędzi programistów API do definiowania API w najbardziej użyteczny sposób dla programisty klienta. Jest CheckedInvalidRowNumberException
to jednak duża niedogodność, która prowadzi do złego programowania i należy jej unikać. Ale jak odróżnić.
Wydaje mi się, że nie jest to nauka ścisła i sądzę, że leży ona u podstaw i być może uzasadnia do pewnego stopnia argument Hejlsberga. Ale nie cieszę się, że tutaj wylewam dziecko z kąpielą, więc pozwólcie mi wyodrębnić kilka zasad, aby odróżnić sprawdzone wyjątki od złych:
Poza kontrolą klienta lub Zamknięty kontra Otwarty:
Zaznaczone wyjątki należy stosować tylko wtedy, gdy przypadek błędu wymyka się spod kontroli zarówno API, jak i programisty klienta. Ma to związek z tym, jak otwarty lub zamknięty jest system. W ograniczonym interfejsie użytkownika, w którym programista klienta ma kontrolę, powiedzmy, nad wszystkimi przyciskami, poleceniami klawiatury itp., Które dodają i usuwają wiersze z widoku tabeli (system zamknięty), jest to błąd programistyczny klienta, jeśli próbuje pobrać dane z nieistniejący rząd. W systemie operacyjnym opartym na plikach, w którym dowolna liczba użytkowników / aplikacji może dodawać i usuwać pliki (system otwarty), możliwe jest, że plik, o który prosi klient, został usunięty bez ich wiedzy, dlatego należy się spodziewać, że poradzi sobie z tym .
Wszechobecność:
Sprawdzone wyjątki nie powinny być używane w wywołaniach API często wykonywanych przez klienta. Przez często mam na myśli wiele miejsc w kodzie klienta - niezbyt często na czas. Więc kod klienta nie próbuje często otwierać tego samego pliku, ale mój widok tabeli jest RowData
wszędzie z różnych metod. W szczególności zamierzam pisać dużo kodu
if (model.getRowData().getCell(0).isEmpty())
i bolesne będzie za każdym razem owijanie w try / catch.
Informowanie użytkownika:
Sprawdzone wyjątki należy stosować w przypadkach, w których można sobie wyobrazić przydatny komunikat o błędzie wyświetlany użytkownikowi końcowemu. To jest „i co zrobisz, kiedy to się stanie?” pytanie, które zadałem powyżej. Odnosi się to również do punktu 1. Ponieważ możesz przewidzieć, że coś poza twoim systemem klienta-API może spowodować, że pliku nie będzie, możesz rozsądnie poinformować o tym użytkownika:
"Error: could not find the file 'goodluckfindingthisfile'"
Ponieważ twój nielegalny numer wiersza został spowodowany wewnętrznym błędem i nie z winy użytkownika, naprawdę nie możesz podać żadnych przydatnych informacji. Jeśli Twoja aplikacja nie przepuści wyjątków środowiska wykonawczego do konsoli, prawdopodobnie skończy się to brzydkim komunikatem:
"Internal error occured: IllegalArgumentException in ...."
Krótko mówiąc, jeśli nie uważasz, że programista klienta może wyjaśnić twój wyjątek w sposób, który pomaga użytkownikowi, prawdopodobnie nie powinieneś używać sprawdzonego wyjątku.
To są moje zasady. Nieco wymyślone i bez wątpienia będą wyjątki (proszę pomóż mi je ulepszyć, jeśli chcesz). Ale moim głównym argumentem jest to, że istnieją przypadki, w FileNotFoundException
których sprawdzany wyjątek jest równie ważny i użyteczny jako część umowy API, jak typy parametrów. Nie powinniśmy więc rezygnować z tego tylko dlatego, że jest niewłaściwie używane.
Przepraszam, nie chciałem robić tego tak długo i głupio. Zakończę dwiema sugestiami:
Odp .: Programiści API: oszczędnie używają sprawdzonych wyjątków, aby zachować ich użyteczność. W razie wątpliwości użyj niezaznaczonego wyjątku.
B: Programiści klienccy: we wczesnym etapie tworzenia nawiniętego wyjątku (google it). JDK 1.4 i nowsze zawierają w RuntimeException
tym celu konstruktora , ale możesz też łatwo stworzyć własny. Oto konstruktor:
public RuntimeException(Throwable cause)
Następnie przyzwyczaj się do tego, gdy musisz obsłużyć sprawdzony wyjątek i czujesz się leniwy (lub myślisz, że programista API nadgorliwie używał sprawdzonego wyjątku w pierwszej kolejności), nie połykaj wyjątku, zawiń go i wrzućcie go ponownie.
try {
overzealousAPI(thisArgumentWontWork);
}
catch (OverzealousCheckedException exception) {
throw new RuntimeException(exception);
}
Umieść to w jednym z małych szablonów kodu IDE i używaj go, gdy czujesz się leniwy. W ten sposób, jeśli naprawdę chcesz obsłużyć sprawdzony wyjątek, będziesz zmuszony wrócić i rozwiązać problem po zobaczeniu problemu w czasie wykonywania. Ponieważ, wierzcie mi (i Andersowi Hejlsbergowi), nigdy nie wrócicie do tego TODO w swoim
catch (Exception e) { /* TODO deal with this at some point (yeah right) */}