Inni dość dobrze podsumowali, dlaczego rzucać wcześnie . Pozwolę sobie skoncentrować się na tym, dlaczego zamiast tego złapać późną część, dla której nie widziałem satysfakcjonującego wyjaśnienia mojego smaku.
DLACZEGO DLACZEGO WYJĄTKI?
Wydaje się, że istnieje sporo zamieszania wokół tego, dlaczego w ogóle istnieją wyjątki. Podzielę się tutaj wielką tajemnicą: powodem wyjątków i obsługi wyjątków jest ... ABSTRACTION .
Czy widziałeś taki kod:
static int divide(int dividend, int divisor) throws DivideByZeroException {
if (divisor == 0)
throw new DivideByZeroException(); // that's a checked exception indeed
return dividend / divisor;
}
static void doDivide() {
int a = readInt();
int b = readInt();
try {
int res = divide(a, b);
System.out.println(res);
} catch (DivideByZeroException e) {
// checked exception... I'm forced to handle it!
System.out.println("Nah, can't divide by zero. Try again.");
}
}
Nie tak należy stosować wyjątki. Kod podobny do powyższego istnieje w prawdziwym życiu, ale jest bardziej aberracją i naprawdę stanowi wyjątek (kalambur). Na przykład definicja dzielenia , nawet w czystej matematyce, jest warunkowa: zawsze „kod dzwoniącego” musi obsłużyć wyjątkowy przypadek zera, aby ograniczyć domenę wejściową. Jest brzydki. Dzwoniący zawsze odczuwa ból. Mimo to w takich sytuacjach naturalny sposób sprawdzenia to :
static int divide(int dividend, int divisor) {
// throws unchecked ArithmeticException for 0 divisor
return dividend / divisor;
}
static void doDivide() {
int a = readInt();
int b = readInt();
if (b != 0) {
int res = divide(a, b);
System.out.println(res);
} else {
System.out.println("Nah, can't divide by zero. Try again.");
}
}
Alternatywnie możesz przejść do pełnego komandosa w stylu OOP w następujący sposób:
static class Division {
final int dividend;
final int divisor;
private Division(int dividend, int divisor) {
this.dividend = dividend;
this.divisor = divisor;
}
public boolean check() {
return divisor != 0;
}
public int eval() {
return dividend / divisor;
}
public static Division with(int dividend, int divisor) {
return new Division(dividend, divisor);
}
}
static void doDivide() {
int a = readInt();
int b = readInt();
Division d = Division.with(a, b);
if (d.check()) {
int res = d.eval();
System.out.println(res);
} else {
System.out.println("Nah, can't divide by zero. Try again.");
}
}
Jak widać, kod dzwoniącego jest obciążony sprawdzaniem wstępnym, ale po nim nie wykonuje żadnych wyjątków. Jeśli ArithmeticException
kiedykolwiek pochodzi z połączenia divide
lub eval
, to TY musisz zrobić obsługę wyjątków i naprawić swój kod, ponieważ zapomniałeś check()
. Z podobnych powodów złapanie a NullPointerException
jest prawie zawsze niewłaściwą rzeczą.
Teraz są ludzie, którzy twierdzą, że chcą zobaczyć wyjątkowe przypadki w sygnaturze metody / funkcji, tj. Jawnie rozszerzyć domenę wyjściową . To oni faworyzują sprawdzone wyjątki . Oczywiście zmiana domeny wyjściowej powinna wymusić dostosowanie dowolnego kodu wywołującego, co rzeczywiście można osiągnąć przy sprawdzonych wyjątkach. Ale nie potrzebujesz do tego wyjątków! Dlatego trzeba Nullable<T>
klas generycznych , zajęcia przypadków , algebraiczne typy danych oraz rodzaje związków . Niektórzy ludzie OO mogą nawet preferować powrót null
w przypadku takich prostych błędów:
static Integer divide(int dividend, int divisor) {
if (divisor == 0) return null;
return dividend / divisor;
}
static void doDivide() {
int a = readInt();
int b = readInt();
Integer res = divide(a, b);
if (res != null) {
System.out.println(res);
} else {
System.out.println("Nah, can't divide by zero. Try again.");
}
}
Technicznie wyjątki mogą być wykorzystane do celów takich jak powyżej, ale o to chodzi: wyjątki nie istnieją dla takiego zastosowania . Wyjątki są pro abstrakcyjne. Wyjątek dotyczą pośredniości. Wyjątki pozwalają na rozszerzenie domeny „wyniku” bez zerwania bezpośrednich umów z klientami i odroczenie obsługi błędów do „gdzie indziej”. Jeśli Twój kod zgłasza wyjątki, które są obsługiwane przez bezpośrednie wywołujące ten sam kod, bez żadnych warstw abstrakcji pomiędzy nimi, oznacza to, że robisz to NIEPRAWIDŁOWO
JAK ŁOWIĆ PÓŹNO?
I oto jesteśmy. Przekonywałem się, aby pokazać, że stosowanie wyjątków w powyższych scenariuszach nie jest tym, w jaki sposób należy stosować wyjątki. Istnieje jednak prawdziwy przypadek użycia, w którym abstrakcja i pośrednictwo oferowane przez obsługę wyjątków są niezbędne. Zrozumienie takiego użycia pomoże również zrozumieć zalecenie późnego połowu .
Ten przypadek użycia to: Programowanie przeciw abstrakcjom zasobów ...
Tak, logika biznesowa powinna być zaprogramowana na abstrakcje , a nie na konkretne wdrożenia. Kod „okablowania” IOC najwyższego poziomu utworzy konkretne implementacje abstrakcji zasobów i przekaże je logice biznesowej. Nic nowego tutaj. Ale konkretne implementacje tych abstrakcji zasobów mogą potencjalnie wprowadzać własne wyjątki specyficzne dla implementacji , prawda?
Kto więc może obsłużyć te wyjątki związane z implementacją? Czy w logice biznesowej można w ogóle obsługiwać wyjątki specyficzne dla zasobów? Nie, nie jest. Logika biznesowa jest zaprogramowana na abstrakcje, co wyklucza znajomość szczegółów dotyczących wyjątków specyficznych dla implementacji.
„Aha!”, Możesz powiedzieć: „ale właśnie dlatego możemy podklasować wyjątki i tworzyć hierarchie wyjątków” (sprawdź Mr. Spring !). Pozwól, że ci powiem, że to błąd. Po pierwsze, każda rozsądna książka na temat OOP mówi, że konkretne dziedziczenie jest złe, ale jakoś ten podstawowy element JVM, obsługa wyjątków, jest ściśle związany z konkretnym dziedzictwem. Jak na ironię, Joshua Bloch nie byłby w stanie napisać swojej skutecznej książki o Javie, zanim nie uzyskałby doświadczenia z działającą maszyną JVM, prawda? To bardziej książka „wyciągniętych wniosków” dla następnego pokolenia. Po drugie i, co ważniejsze, jeśli złapiesz wyjątek na wysokim poziomie, jak sobie z nim poradzisz?PatientNeedsImmediateAttentionException
: czy musimy dać jej pigułkę czy amputować jej nogi !? Co powiesz na instrukcję switch we wszystkich możliwych podklasach? Idzie twój polimorfizm, i idzie abstrakcja. Masz punkt.
Kto więc może obsłużyć wyjątki specyficzne dla zasobów? To musi być ten, który zna konkrecje! Ten, który utworzył instancję zasobu! Oczywiście kod „okablowania”! Spójrz na to:
Logika biznesowa zakodowana przeciwko abstrakcjom ... BRAK OBSŁUGI BŁĘDU ZASOBÓW BETONU!
static interface InputResource {
String fetchData();
}
static interface OutputResource {
void writeData(String data);
}
static void doMyBusiness(InputResource in, OutputResource out, int times) {
for (int i = 0; i < times; i++) {
System.out.println("fetching data");
String data = in.fetchData();
System.out.println("outputting data");
out.writeData(data);
}
}
Tymczasem gdzieś indziej konkretne wdrożenia ...
static class ConstantInputResource implements InputResource {
@Override
public String fetchData() {
return "Hello World!";
}
}
static class FailingInputResourceException extends RuntimeException {
public FailingInputResourceException(String message) {
super(message);
}
}
static class FailingInputResource implements InputResource {
@Override
public String fetchData() {
throw new FailingInputResourceException("I am a complete failure!");
}
}
static class StandardOutputResource implements OutputResource {
@Override
public void writeData(String data) {
System.out.println("DATA: " + data);
}
}
I wreszcie kod okablowania ... Kto zajmuje się konkretnymi wyjątkami od zasobów? Ten, który o nich wie!
static void start() {
InputResource in1 = new FailingInputResource();
InputResource in2 = new ConstantInputResource();
OutputResource out = new StandardOutputResource();
try {
ReusableBusinessLogicClass.doMyBusiness(in1, out, 3);
}
catch (FailingInputResourceException e)
{
System.out.println(e.getMessage());
System.out.println("retrying...");
ReusableBusinessLogicClass.doMyBusiness(in2, out, 3);
}
}
Teraz zrób ze mną. Powyższy kod jest uproszczony. Można powiedzieć, że masz aplikację korporacyjną / kontener WWW z wieloma zakresami zasobów zarządzanych przez kontener IOC i potrzebujesz automatycznych prób i ponownej inicjalizacji zasobów zakresu sesji lub żądania itp. Logika okablowania w zakresach niższego poziomu może otrzymać abstrakcyjne fabryki tworzyć zasoby, dlatego nie wiedząc o dokładnych implementacjach. Tylko zakresy wyższego poziomu naprawdę wiedziały, jakie wyjątki mogą rzucać te zasoby niższego poziomu. Teraz trzymaj się!
Niestety wyjątki zezwalają tylko na pośrednie stosy wywołań, a różne zakresy z różnymi licznościami zwykle działają na wielu różnych wątkach. Nie ma możliwości komunikowania się przez to z wyjątkami. Potrzebujemy tutaj czegoś mocniejszego. Odpowiedź: przekazywanie wiadomości asynchronicznych . Złap każdy wyjątek w katalogu głównym zakresu niższego poziomu. Nic nie ignoruj, nie pozwól, aby cokolwiek przeszło. Spowoduje to zamknięcie i usunięcie wszystkich zasobów utworzonych na stosie wywołań bieżącego zakresu. Następnie propaguj komunikaty o błędach do wyższych zakresów za pomocą kolejek / kanałów komunikatów w procedurze obsługi wyjątków, aż osiągniesz poziom, na którym znane są konkrecje. To facet, który wie, jak sobie z tym poradzić.
SUMMA SUMMARUM
Tak więc, zgodnie z moją interpretacją, „ złap późno” oznacza złapanie wyjątków w najbardziej dogodnym miejscu, GDZIE JUŻ NIE WYSTĘPUJESZ ABSTRAKCJI . Nie łap zbyt wcześnie! Przechwytuj wyjątki na warstwie, na której tworzysz konkretny wyjątek rzucający instancje abstrakcji zasobów, na warstwie znającej konkrecje abstrakcji. Warstwa „okablowania”.
HTH. Miłego kodowania!