Wyjątki: po co rzucać wcześnie? Po co łapać późno?


156

Istnieje wiele dobrze znanych najlepszych praktyk dotyczących obsługi wyjątków w izolacji. Wiem wystarczająco dobrze, co należy robić, a czego nie robić, ale sprawy komplikują się, jeśli chodzi o najlepsze praktyki lub wzorce w większych środowiskach. „Rzuć wcześnie, złap późno” - słyszałem wiele razy i nadal mnie to myli.

Dlaczego powinienem rzucać wcześnie i łapać późno, jeśli na warstwie niskiego poziomu zostanie zgłoszony wyjątek zerowy? Dlaczego powinienem złapać go na wyższej warstwie? Nie ma dla mnie sensu wychwytywanie wyjątku niskiego poziomu na wyższym poziomie, takiego jak warstwa biznesowa. Wydaje się, że narusza to obawy każdej warstwy.

Wyobraź sobie następującą sytuację:

Mam usługę, która oblicza liczbę. Aby obliczyć liczbę, usługa uzyskuje dostęp do repozytorium w celu uzyskania surowych danych i niektórych innych usług w celu przygotowania obliczeń. Jeśli coś poszło nie tak w warstwie pobierania danych, dlaczego powinienem przerzucić wyjątek DataRetrievalException na wyższy poziom? W przeciwieństwie do tego wolałbym zawinąć wyjątek w znaczący wyjątek, na przykład wyjątek CalculationServiceException.

Po co rzucać wcześnie, po co łapać późno?


104
Ideą „złapać późno” jest raczej, niż łapanie tak późno, jak to możliwe, złapać tak wcześnie, jak to jest przydatne. Jeśli masz na przykład analizator składni plików, nie ma sensu obsługiwać pliku, którego nie znaleziono. Co masz z tym zrobić, jaka jest twoja ścieżka odzyskiwania? Nie ma jednego, więc nie łap. Idź do leksyka, co tam robisz, jak możesz odzyskać to, aby Twój program mógł kontynuować? Nie może pozwolić, by wyjątek minął. Jak twój skaner może sobie z tym poradzić? Nie może, niech przejdzie. Jak kod wywołujący może sobie z tym poradzić? Może wypróbować inną ścieżkę do pliku lub ostrzec użytkownika, więc złap.
Phoshi

16
Jest bardzo niewiele przypadków, w których NullPointerException (zakładam, że właśnie to oznacza NPE) powinien kiedykolwiek zostać złapany; jeśli to możliwe, należy go przede wszystkim unikać. Jeśli masz wyjątki NullPointerExcept, masz uszkodzony kod, który należy naprawić. Najprawdopodobniej jest to również łatwa poprawka.
Phil

6
Proszę, chłopaki, zanim zasugerujecie zamknięcie tego pytania jako duplikatu, sprawdźcie, czy odpowiedź na drugie pytanie nie odpowiada zbyt dobrze na to pytanie.
Doc Brown

1
(Citebot) Today.java.net/article/2003/11/20/... Jeśli nie jest to źródło cytatu, podaj odniesienie do źródła, które Twoim zdaniem jest najbardziej oryginalną cytatem.
rwong

1
Przypomnienie dla wszystkich, którzy sięgają po to pytanie i pracują nad tworzeniem Androida. W Androidzie wyjątki muszą być wychwytywane i obsługiwane lokalnie - w tej samej funkcji, w której są wychwytywane po raz pierwszy. Wynika to z faktu, że wyjątki nie rozprzestrzeniają się między programami obsługi wiadomości - jeśli tak się stanie, aplikacja zostanie zabita. Dlatego nie powinieneś cytować tej porady podczas tworzenia Androida.
rwong

Odpowiedzi:


118

Z mojego doświadczenia wynika, że ​​najlepiej jest zgłaszać wyjątki w miejscu wystąpienia błędów. Robisz to, ponieważ jest to punkt, w którym najlepiej wiesz, dlaczego wyjątek został uruchomiony.

Gdy wyjątek odtwarza kopie zapasowe warstw, łapanie i ponowne rzucanie jest dobrym sposobem na dodanie dodatkowego kontekstu do wyjątku. Może to oznaczać zgłoszenie innego rodzaju wyjątku, ale dołącz do niego oryginalny wyjątek.

Ostatecznie wyjątek dotrze do warstwy, w której będziesz mógł podejmować decyzje dotyczące przepływu kodu (np. Monitować użytkownika o działanie). To jest punkt, w którym powinieneś w końcu obsłużyć wyjątek i kontynuować normalne wykonanie.

Dzięki praktyce i doświadczeniu z bazą kodu dość łatwo jest ocenić, kiedy dodać dodatkowy kontekst do błędów, a tam, gdzie jest to najbardziej rozsądne, ostatecznie zająć się błędami.

Catch → Rethrow

Zrób to, gdzie możesz użytecznie dodać więcej informacji, które zaoszczędzą programistom na przejściu wszystkich warstw, aby zrozumieć problem.

Złap → Uchwyt

Zrób to, gdzie możesz podejmować ostateczne decyzje dotyczące tego, co jest właściwe, ale różni się przepływem wykonania przez oprogramowanie.

Złap → Błąd powrotu

Chociaż istnieją sytuacje, w których jest to właściwe, należy wziąć pod uwagę wychwycenie wyjątków i zwrócenie wartości błędu dzwoniącemu w celu refaktoryzacji do implementacji Catch → Rethrow.


Tak, już wiem i nie ma wątpliwości, że powinienem wprowadzić wyjątki w miejscu, w którym powstał błąd. Ale dlaczego nie powinienem złapać NPE i zamiast tego pozwolić mu wspiąć się na Stacktrace? Zawsze łapałem NPE i umieszczałem go w znaczącym wyjątku. Nie widzę też żadnej korzyści, dlaczego powinienem zgłosić wyjątek DAO do warstwy usługi lub interfejsu użytkownika. Zawsze łapałem go w warstwie usługi i pakowałem w wyjątek usługi z dodatkowymi szczegółowymi informacjami, dlaczego wywołanie usługi nie powiodło się.
shylynx

8
@shylynx Złapanie wyjątku, a następnie ponowne wygenerowanie bardziej znaczącego wyjątku jest dobrym rozwiązaniem. Czego nie należy robić to łowienie wyjątek zbyt wcześnie i potem nie Ponowne generowanie go. Błąd, o którym mówi to powiedzenie, zbyt wcześnie wychwytuje wyjątek, a następnie próbuje obsłużyć go na niewłaściwej warstwie w kodzie.
Simon B

Ujawnienie kontekstu w momencie otrzymania wyjątku ułatwia życie programistom w zespole. NPE wymaga dalszych badań, aby zrozumieć problem
Michael Shaw

4
@shylynx Można zadać pytanie: „Dlaczego masz w kodzie punkt, który może rzucić NullPointerException? Dlaczego nie sprawdzić nulli nie wprowadzić wyjątku (być może IllegalArgumentExceptionwcześniej), aby dzwoniący wiedział dokładnie, gdzie zło nullzostało przekazane?” Uważam, że to właśnie sugerowałaby część powiedzenia „rzut wczesny”.
jpmc26

2
@jpmc Wziąłem NPE tylko jako przykład, aby podkreślić obawy wokół warstw i wyjątki. Mógłbym również zastąpić go IllegalArgumentException.
shylynx

56

Chcesz jak najszybciej zgłosić wyjątek, ponieważ ułatwia to znalezienie przyczyny. Rozważmy na przykład metodę, która może zawieść przy niektórych argumentach. Jeśli zweryfikujesz argumenty i nie powiedzie się na samym początku metody, natychmiast wiesz, że błąd występuje w kodzie wywołującym. Jeśli poczekasz, aż argumenty będą potrzebne przed niepowodzeniem, musisz postępować zgodnie z wykonaniem i dowiedzieć się, czy błąd znajduje się w kodzie wywołującym (zły argument), czy metoda ma błąd. Im wcześniej rzucisz wyjątek, tym bliżej jest jego podstawowej przyczyny i łatwiej jest dowiedzieć się, co poszło nie tak.

Przyczyny wyjątków są obsługiwane na wyższych poziomach, ponieważ niższe poziomy nie wiedzą, jaki jest właściwy sposób postępowania w przypadku błędu. W rzeczywistości może istnieć wiele odpowiednich sposobów obsługi tego samego błędu w zależności od tego, jaki jest kod wywołujący. Weźmy na przykład otwarcie pliku. Jeśli próbujesz otworzyć plik konfiguracyjny, którego nie ma, zignorowanie wyjątku i kontynuowanie konfiguracji domyślnej może być odpowiednią odpowiedzią. Jeśli otwierasz prywatny plik, który jest niezbędny do wykonania programu i jakoś go brakuje, prawdopodobnie jedyną opcją jest zamknięcie programu.

Zawijanie wyjątków we właściwe typy jest kwestią czysto ortogonalną.


1
+1 za jasne wyjaśnienie, dlaczego różne poziomy mają znaczenie. Doskonały przykład błędu systemu plików.
Juan Carlos Coto,

24

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 ArithmeticExceptionkiedykolwiek pochodzi z połączenia dividelub 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 NullPointerExceptionjest 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!


Masz rację, że kod dostarczający interfejs będzie wiedział więcej o tym, co może pójść nie tak, jak kod korzystający z interfejsu, ale załóżmy, że metoda wykorzystuje dwa zasoby tego samego typu interfejsu, a awarie należy traktować inaczej? A może jeden z tych zasobów wewnętrznie, jako szczegół implementacji nieznany twórcy, wykorzystuje inne zagnieżdżone zasoby tego samego typu? Mając rzut warstwy biznesowej WrappedFirstResourceExceptionlub WrappedSecondResourceExceptioni wymagający „okablowania” warstwę zajrzeć do wnętrza tego wyjątku, aby zobaczyć przyczynę problemu ...
SuperCat

... może być trudne, ale wydaje się lepsze niż zakładanie, że jakikolwiek FailingInputResourcewyjątek będzie wynikiem operacji z in1. Właściwie uważam, że w wielu przypadkach właściwym podejściem byłoby przepuszczenie przez warstwę okablowania obiektu obsługującego wyjątki i włączenie warstwy biznesowej, catchktóra następnie wywołuje handleExceptionmetodę tego obiektu . Ta metoda może spowodować ponowne podanie lub dostarczenie danych domyślnych lub wprowadzenie komunikatu „Przerwij / ponów / nie powiodło się” i pozwól operatorowi zdecydować, co robić itd., W zależności od wymaganej aplikacji.
supercat

@ supercat Rozumiem, co mówisz. Powiedziałbym, że konkretna implementacja zasobów jest odpowiedzialna za poznanie wyjątków, które może wyrzucić. Nie musi określać wszystkiego (tak zwane nieokreślone zachowanie ), ale musi zapewniać, że nie będzie dwuznaczności. Ponadto należy udokumentować niesprawdzone wyjątki czasu wykonywania. Jeśli jest to sprzeczne z dokumentacją, oznacza to błąd. Jeśli oczekuje się, że kod dzwoniącego zrobi coś sensownego w odniesieniu do wyjątku, absolutnym minimum jest to, że zasób zawija je w niektóre UnrecoverableInternalException, podobnie jak kod błędu HTTP 500.
Daniel Dinnyes

@ superupat O Twojej sugestii dotyczącej konfigurowalnych programów obsługi błędów: dokładnie! W moim ostatnim przykładzie logika obsługi błędów jest zakodowana na stałe, wywołując doMyBusinessmetodę statyczną . Było tak ze względu na zwięzłość i jest całkowicie możliwe, aby uczynić go bardziej dynamicznym. Taka Handlerklasa byłaby utworzona z pewnymi zasobami wejścia / wyjścia i miałaby handlemetodę, która odbiera klasę implementującą ReusableBusinessLogicInterface. Następnie można połączyć / skonfigurować, aby użyć innej implementacji modułu obsługi, zasobów i logiki biznesowej w warstwie okablowania gdzieś nad nimi.
Daniel Dinnyes

10

Aby odpowiedzieć poprawnie na to pytanie, cofnijmy się o krok i zadajmy jeszcze bardziej fundamentalne pytanie.

Dlaczego w ogóle mamy wyjątki?

Zgłaszamy wyjątki, aby osoba dzwoniąca z naszej metody wiedziała, że ​​nie możemy zrobić tego, o co nas poproszono. Rodzaj wyjątku wyjaśnia, dlaczego nie mogliśmy zrobić tego, co chcieliśmy.

Rzućmy okiem na kod:

double MethodA()
{
    return PropertyA - PropertyB.NestedProperty;
}

Ten kod może oczywiście zgłosić wyjątek PropertyBzerowy, jeśli jest pusty. W tym przypadku możemy zrobić dwie rzeczy, aby „poprawić” tę sytuację. Moglibyśmy:

  • Automatycznie utwórz Właściwość B, jeśli jej nie mamy; lub
  • Pozwól wyjątkowi przejść do metody wywołania.

Utworzenie tutaj PropertyB może być bardzo niebezpieczne. Z jakiego powodu ta metoda ma utworzyć właściwość B? Z pewnością naruszałoby to zasadę pojedynczej odpowiedzialności. Według wszelkiego prawdopodobieństwa, jeśli właściwość B nie istnieje tutaj, oznacza to, że coś poszło nie tak. Wywoływana jest metoda na częściowo skonstruowanym obiekcie lub niepoprawnie ustawiono właściwość B na null. Tworząc tutaj PropertyB, moglibyśmy ukryć znacznie większy błąd, który mógłby nas ugryźć później, taki jak błąd powodujący uszkodzenie danych.

Jeśli zamiast tego pozwolimy, aby odnośnik zerowy pojawił się, informujemy programistę, który wywołał tę metodę, tak szybko, jak to możliwe, że coś poszło nie tak. Istotny warunek wstępny wywołania tej metody został pominięty.

W efekcie rzucamy wcześnie, ponieważ znacznie lepiej oddziela nasze obawy. Jak tylko wystąpi awaria, informujemy o tym deweloperów.

Dlaczego „łapiemy się za późno” to inna historia. Naprawdę nie chcemy łapać za późno, naprawdę chcemy łapać tak wcześnie, jak wiemy, jak właściwie rozwiązać problem. Czasami będzie to piętnaście warstw abstrakcji później, a czasem będzie to w momencie stworzenia.

Chodzi o to, że chcemy wychwycić wyjątek na warstwie abstrakcji, która pozwala nam obsłużyć wyjątek w miejscu, w którym mamy wszystkie informacje potrzebne do prawidłowego obsłużenia wyjątku.


Myślę, że używasz programistów w niewłaściwym sensie. Powiedziałeś także, że narusza to zasadę pojedynczej odpowiedzialności, ale w rzeczywistości wiele inicjalizacji na żądanie i buforowania wartości jest wdrażanych w ten sposób (oczywiście z odpowiednią kontrolą współbieżności zamiast)
Daniel Dinnyes,

W podanym przykładzie, co z sprawdzaniem wartości null przed operacją odejmowania, tak jak toif(PropertyB == null) return 0;
user1451111

1
Czy możesz również rozwinąć swój ostatni akapit, a konkretnie, co rozumiesz przez „ warstwę abstrakcji ”.
user1451111

Jeśli wykonujemy jakąś pracę we / wy, warstwa abstrakcyjna, aby uchwycić wyjątek we / wy, byłaby tam, gdzie wykonujemy tę pracę. W tym momencie mamy wszystkie informacje, których potrzebujemy, aby zdecydować, czy chcemy ponowić próbę, wyrzucić okno komunikatu do użytkownika lub utworzyć obiekt przy użyciu zestawu wartości domyślnych.
Stephen

„W podanym przykładzie, co z sprawdzaniem wartości null przed operacją odejmowania, na przykład, jeśli (Właściwość B == null) zwraca 0;” Fuj Oznaczałoby to metodę wywołującą, że mam ważne rzeczy do odjęcia do iz. Oczywiście jest to kontekstowe, ale sprawdzanie błędów w większości przypadków byłoby złą praktyką.
Stephen

6

Rzuć, gdy tylko zobaczysz coś, na co warto rzucić, aby uniknąć umieszczenia obiektów w niewłaściwym stanie. Oznacza to, że jeśli wskaźnik zerowy został przekazany, sprawdzasz go wcześnie i rzucasz NPE, zanim będzie on miał szansę zejść na niższy poziom.

Złap, gdy tylko wiesz, co zrobić, aby naprawić błąd (na ogół nie jest to miejsce, w którym możesz rzucić, w przeciwnym razie możesz po prostu użyć if-else), jeśli przekazany zostanie niepoprawny parametr, warstwa, która podała parametr, powinna poradzić sobie z konsekwencjami .


1
Napisałeś: Rzuć wkrótce ... Złap wkrótce ...! Dlaczego? Jest to całkowicie sprzeczne podejście w przeciwieństwie do „rzucaj wcześnie, łap późno”.
shylynx

1
@shylynx Nie wiem, skąd pochodzi „rzut wczesny, złap późno”, ale jego wartość jest wątpliwa. Co dokładnie oznacza „spóźnić się”? To, gdzie sens ma złapanie wyjątku (jeśli w ogóle), zależy od problemu. Jedyne, co jest jasne, to to, że chcesz wykryć problemy (i rzucić) jak najwcześniej.
Doval

2
Zakładam, że „złapanie późno” ma na celu przeciwstawienie się praktyce łapania, zanim będziesz wiedział, co zrobić, aby naprawić błąd - np. Czasami widzisz funkcje, które wychwytują wszystko, aby wydrukować komunikat o błędzie, a następnie zwrócić wyjątek.

@Hurkyl: Problem z opóźnieniem polega na tym, że jeśli wyjątek przepłynie przez warstwy, które nic o nim nie wiedzą, kodowi może być trudno zrobić coś w tej sytuacji, aby wiedzieć, że tak naprawdę jest tak spodziewany. Jako prosty przykład załóżmy, że jeśli analizator składni pliku dokumentu użytkownika musi załadować kodek z dysku i podczas odczytu wystąpi błąd dysku, kod wywołujący analizator składni może działać niewłaściwie, jeśli podejrzewa błąd dysku podczas odczytu użytkownika dokument.
supercat

4

Prawidłowa reguła biznesowa brzmi „jeśli oprogramowanie niższego poziomu nie obliczy wartości, wtedy ...”

Można to wyrazić tylko na wyższym poziomie, w przeciwnym razie oprogramowanie na niższym poziomie próbuje zmienić swoje zachowanie w oparciu o własną poprawność, co kończy się jedynie węzłem.


2

Przede wszystkim wyjątki dotyczą wyjątkowych sytuacji. W twoim przykładzie nie można obliczyć żadnej liczby, jeśli surowe dane nie są obecne, ponieważ nie można ich załadować.

Z mojego doświadczenia wynika, że ​​dobrą praktyką jest abstrakcyjne wyjątki podczas wchodzenia na stos. Zazwyczaj punkty, w których chcesz to zrobić, są za każdym razem, gdy wyjątek przekracza granicę między dwiema warstwami.

Jeśli wystąpi błąd podczas gromadzenia nieprzetworzonych danych w warstwie danych, zgłoś wyjątek, aby powiadomić osobę, która zażądała danych. Nie próbuj omijać tego problemu tutaj. Złożoność kodu obsługi może być bardzo wysoka. Również warstwa danych jest odpowiedzialna tylko za żądanie danych, a nie za obsługę błędów, które występują podczas tej operacji. To właśnie oznacza „rzut wcześnie” .

W twoim przykładzie warstwą chwytającą jest warstwa usługi. Sama usługa to nowa warstwa, siedząca nad warstwą dostępu do danych. Więc chcesz złapać wyjątek. Być może Twoja usługa ma infrastrukturę przełączania awaryjnego i próbuje zażądać danych z innego repozytorium. Jeśli to również się nie powiedzie, zawiń wyjątek w czymś, co osoba dzwoniąca usługi rozumie (jeśli jest to usługa internetowa, może to być błąd SOAP). Ustaw oryginalny wyjątek jako wyjątek wewnętrzny, aby późniejsze warstwy mogły rejestrować dokładnie to, co poszło nie tak.

Błąd usługi może zostać wykryty przez warstwę wywołującą usługę (na przykład interfejs użytkownika). I to właśnie oznacza „złapać późno” . Jeśli nie możesz obsłużyć wyjątku w dolnej warstwie, ponownie go rzuć. Jeśli najwyższa warstwa nie może obsłużyć wyjątku, obsłuż go! Może to obejmować logowanie lub prezentację.

Powodem, dla którego powinieneś powrócić do wyjątków (jak opisano powyżej, zawijając je w bardziej ogólnych wyjątkach) jest to, że użytkownik najprawdopodobniej nie jest w stanie zrozumieć, że wystąpił błąd, ponieważ na przykład wskaźnik wskazywał na niepoprawną pamięć. I nie obchodzi go to. Troszczy się tylko o to, aby usługa nie mogła obliczyć tej liczby i to jest informacja, którą powinien mu pokazać.

Idąc dalej możesz (w idealnym świecie) całkowicie pominąć try/ catchkodować z interfejsu użytkownika. Zamiast tego użyj globalnej procedury obsługi wyjątków, która jest w stanie zrozumieć wyjątki, które mogą być zgłaszane przez niższe warstwy, zapisuje je w dzienniku i zawija w obiekty błędów, które zawierają znaczące (i prawdopodobnie zlokalizowane) informacje o błędzie. Obiekty te można łatwo przedstawić użytkownikowi w dowolnej formie (skrzynki wiadomości, powiadomienia, tosty wiadomości itd.).


1

Generalnie generowanie wyjątków wcześnie jest dobrą praktyką, ponieważ nie chcesz, aby złamane umowy przepływały przez kod dalej, niż to konieczne. Na przykład, jeśli oczekujesz, że określony parametr funkcji będzie dodatnią liczbą całkowitą, powinieneś wymusić to ograniczenie w punkcie wywołania funkcji zamiast czekać, aż ta zmienna zostanie użyta gdzie indziej na stosie kodu.

Późne łapanie nie mogę komentować, ponieważ mam własne zasady i zmienia się to w zależności od projektu. Jedyne, co próbuję zrobić, to podzielić wyjątki na dwie grupy. Jeden służy wyłącznie do użytku wewnętrznego, a drugi do użytku zewnętrznego. Wyjątek wewnętrzny jest przechwytywany i obsługiwany przez mój własny kod, a wyjątek zewnętrzny powinien być obsługiwany przez dowolny kod, który do mnie wzywa. Jest to w zasadzie forma łapania rzeczy później, ale nie do końca, ponieważ oferuje mi elastyczność w zakresie odstępstwa od reguły w razie potrzeby w wewnętrznym kodzie.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.