Posiadanie flagi wskazującej, czy powinniśmy zgłaszać błędy

64

Niedawno zacząłem pracować w miejscu z kilkoma znacznie starszymi programistami (około 50+). Pracowali nad krytycznymi aplikacjami dotyczącymi lotnictwa, w których system nie mógł ulec awarii. W rezultacie starszy programista ma tendencję do kodowania w ten sposób.

Zwykle umieszcza w obiektach wartość logiczną, aby wskazać, czy wyjątek powinien zostać zgłoszony, czy nie.

Przykład

public class AreaCalculator
{
    AreaCalculator(bool shouldThrowExceptions) { ... }
    CalculateArea(int x, int y)
    {
        if(x < 0 || y < 0)
        {
            if(shouldThrowExceptions) 
                throwException;
            else
                return 0;
        }
    }
}

(W naszym projekcie metoda może się nie powieść, ponieważ próbujemy użyć urządzenia sieciowego, które nie może być w tej chwili obecne. Przykład obszaru jest tylko przykładem flagi wyjątku)

Wydaje mi się, że to zapach kodu. Pisanie testów jednostkowych staje się nieco bardziej skomplikowane, ponieważ za każdym razem musisz testować flagę wyjątku. Ponadto, jeśli coś pójdzie nie tak, czy nie chcesz od razu wiedzieć? Czy to nie osoba dzwoniąca powinna decydować, jak kontynuować?

Jego logika / rozumowanie polega na tym, że nasz program musi zrobić jedną rzecz, pokazać dane użytkownikowi. Wszelkie inne wyjątki, które nas nie powstrzymują, powinny zostać zignorowane. Zgadzam się, że nie powinno się ich ignorować, ale powinny bańkować i zajmować się nimi odpowiednia osoba, i nie muszą w tym celu zajmować się flagami.

Czy to dobry sposób radzenia sobie z wyjątkami?

Edycja : Aby dać więcej kontekstu przy podejmowaniu decyzji projektowej, podejrzewam, że dzieje się tak, ponieważ jeśli ten komponent zawiedzie, program może nadal działać i wykonywać swoje główne zadanie. Dlatego nie chcielibyśmy zgłaszać wyjątku (a nie obsłużyć go?) I pozwolić mu usunąć program, gdy dla użytkownika działa poprawnie

Edycja 2 : Aby dać jeszcze większy kontekst, w naszym przypadku wywoływana jest metoda resetowania karty sieciowej. Problem pojawia się, gdy karta sieciowa jest odłączana i ponownie podłączana, przypisywany jest jej inny adres IP, dlatego Reset spowoduje zgłoszenie wyjątku, ponieważ będziemy próbować zresetować sprzęt za pomocą starego adresu IP.

Nicolas
źródło
22
c # ma konwencję dla tego tupotu Try-Parse. więcej informacji: docs.microsoft.com/en-us/dotnet/standard/design-guidelines/… Flaga nie pasuje do tego wzorca.
Peter
18
Jest to w zasadzie parametr kontrolny i zmienia sposób wykonywania metod wewnętrznych metod. To źle, niezależnie od scenariusza. martinfowler.com/bliki/FlagArgument.html , softwareengineering.stackexchange.com/questions/147977/… , medium.com/@amlcurran/...
bic
1
Oprócz komentarza Try-Parse od Petera, tutaj jest fajny artykuł o wyjątkach Vexing: blogs.msdn.microsoft.com/ericlippert/2008/09/10/...
Linaith,
2
„Dlatego nie chcielibyśmy zgłaszać wyjątku (a nie obsłużyć go?) I pozwolić mu usunąć program, gdy dla użytkownika działa dobrze” - wiesz, że możesz uchwycić wyjątki, prawda?
user253751
1
Jestem prawie pewien, że zostało to omówione wcześniej gdzie indziej, ale biorąc pod uwagę prosty przykład Obszaru, chętniej zastanawiałbym się, skąd będą pochodzić te liczby ujemne i czy można poradzić sobie z tym błędem w innym miejscu (np. cokolwiek czytało na przykład plik zawierający długość i szerokość); Jednak „próbujemy użyć urządzenia sieciowego, które w tej chwili nie może być obecne”. punkt mógłby zasługiwać na zupełnie inną odpowiedź, czy jest to interfejs API innej firmy, czy coś w branży, np. TCP / UDP?
jrh

Odpowiedzi:

74

Problem z tym podejściem polega na tym, że chociaż wyjątki nigdy nie są zgłaszane (a zatem aplikacja nigdy nie ulega awarii z powodu nieprzechwyconych wyjątków), zwracane wyniki niekoniecznie są poprawne, a użytkownik może nigdy nie wiedzieć, że występuje problem z danymi (lub czym jest ten problem i jak go naprawić).

Aby wyniki były poprawne i znaczące, metoda wywołująca musi sprawdzić wynik pod kątem numerów specjalnych - tzn. Określonych wartości zwracanych używanych w celu oznaczenia problemów, które pojawiły się podczas wykonywania metody. Liczby ujemne (lub zerowe) zwracane dla liczb dodatnich (takich jak obszar) są tego najlepszym przykładem w starszym kodzie. Jeśli jednak metoda wywołująca nie wie (lub zapomina!), Aby sprawdzić te numery specjalne, przetwarzanie może być kontynuowane bez pomyłki. Dane są następnie wyświetlane użytkownikowi pokazując obszar 0, który użytkownik wie, że jest niepoprawny, ale nie ma on żadnego wskazania, co poszło źle, gdzie i dlaczego. Następnie zastanawiają się, czy którakolwiek inna wartość jest błędna ...

Jeśli zgłoszony zostanie wyjątek, przetwarzanie zostanie zatrzymane, błąd zostanie (najlepiej) zarejestrowany, a użytkownik może zostać w jakiś sposób powiadomiony. Użytkownik może następnie naprawić wszystko, co jest nie tak, i spróbować ponownie. Właściwa obsługa wyjątków (i testowanie!) Zapewni, że krytyczne aplikacje nie ulegną awarii lub w inny sposób nie zakończą działania w nieprawidłowym stanie.

mmathis
źródło
1
@Quirk To imponujące, jak Chen zdołał złamać zasadę pojedynczej odpowiedzialności tylko w 3 lub 4 liniach. To jest prawdziwy problem. Ponadto problem, o którym mówi (programista nie zastanawia się nad konsekwencjami błędów w każdej linii), zawsze jest możliwa z niezaznaczonymi wyjątkami, a tylko czasami z sprawdzonymi wyjątkami. Wydaje mi się, że widziałem wszystkie argumenty przeciwko sprawdzonym wyjątkom i żaden z nich nie jest ważny.
TKK
@ TKK osobiście zdarzają się przypadki, w których naprawdę chciałbym sprawdzić sprawdzone wyjątki w .NET. Byłoby miło, gdyby istniały jakieś zaawansowane narzędzia analizy statycznej, które mogłyby upewnić się, że to, co dokumentuje interfejs API, ponieważ jego zgłoszone wyjątki są dokładne, chociaż prawdopodobnie byłoby to prawie niemożliwe, szczególnie w przypadku dostępu do zasobów natywnych.
jrh
1
@jrh Tak, byłoby miło, gdyby coś zakłócało jakiś wyjątek bezpieczeństwa w .NET, podobnie jak w przypadku TypeScript kludges bezpieczeństwo w JS.
TKK
47

Czy to dobry sposób radzenia sobie z wyjątkami?

Nie, myślę, że to dość zła praktyka. Zgłoszenie wyjątku vs. zwrócenie wartości to fundamentalna zmiana w interfejsie API, zmiana podpisu metody i sprawienie, że metoda zachowuje się zupełnie inaczej niż z perspektywy interfejsu.

Ogólnie, projektując klasy i ich interfejsy API, powinniśmy to rozważyć

  1. może istnieć wiele instancji klasy o różnych konfiguracjach pływających w tym samym programie w tym samym czasie i

  2. z powodu wstrzykiwania zależności i dowolnej liczby innych praktyk programistycznych, jeden klient konsumujący może tworzyć obiekty i przekazywać je innym, aby ich używał - tak więc często rozdzielamy twórców obiektów od użytkowników obiektów.

Zastanów się teraz, co musi zrobić osoba wywołująca metodę, aby skorzystać z danej instancji, np. W celu wywołania metody obliczeniowej: osoba dzwoniąca musiałaby zarówno sprawdzić, czy obszar jest zerowy, jak i wyłapać wyjątki - ouch! Uwagi dotyczące testowania dotyczą nie tylko samej klasy, ale także obsługi błędów przez osoby dzwoniące ...

Powinniśmy zawsze ułatwiać konsumentowi jak najłatwiejszą pracę; ta konfiguracja logiczna w konstruktorze, która zmienia interfejs API metody instancji, jest przeciwieństwem powodowania, że ​​konsumujący programista kliencki (może ty lub twój kolega) wpadnie w pułapkę sukcesu.

Aby oferować oba interfejsy API, możesz znacznie lepiej i bardziej normalnie zapewnić dwie różne klasy - jedną, która zawsze generuje błąd, a drugą, która zawsze zwraca 0 w przypadku błędu, lub dwie różne metody z jedną klasą. W ten sposób klient konsumujący może łatwo dokładnie wiedzieć, jak sprawdzać i obsługiwać błędy.

Używając dwóch różnych klas lub dwóch różnych metod, możesz łatwiej używać IDE, znaleźć metody użytkowników i funkcje refaktoryzacji itp., Ponieważ dwa przypadki użycia nie są już połączone. Odczytywanie kodu, pisanie, konserwacja, recenzje i testowanie jest również prostsze.


Z drugiej strony osobiście uważam, że nie powinniśmy brać boolowskich parametrów konfiguracyjnych, w których wszyscy wywołujący po prostu przekazują stałą . Taka parametryzacja konfiguracji łączy dwa oddzielne przypadki użycia bez rzeczywistej korzyści.

Spójrz na swoją bazę kodu i sprawdź, czy zmienna (lub wyrażenie niestałe) jest kiedykolwiek używana dla parametru konfiguracyjnego boolean w konstruktorze! Wątpię.


Kolejnym zagadnieniem jest pytanie, dlaczego obliczanie obszaru może się nie powieść. Najlepiej może być wrzucenie konstruktora, jeśli nie można wykonać obliczeń. Jeśli jednak nie wiesz, czy można dokonać obliczeń, dopóki obiekt nie zostanie ponownie zainicjowany, rozważ rozważ użycie różnych klas do rozróżnienia tych stanów (niegotowy do obliczenia obszaru a gotowy do obliczenia obszaru).

Przeczytałem, że twoja awaria jest zorientowana na zdalne sterowanie, więc może nie mieć zastosowania; tylko trochę do przemyślenia.


Czy to nie osoba dzwoniąca powinna decydować, jak kontynuować?

Tak, zgadzam się. Wydaje się przedwczesne, aby odbiorca zdecydował, że obszar 0 jest prawidłową odpowiedzią w warunkach błędu (zwłaszcza, że ​​0 jest prawidłowym obszarem, więc nie ma możliwości odróżnienia błędu od rzeczywistego 0, chociaż może nie dotyczyć Twojej aplikacji).

Erik Eidt
źródło
Tak naprawdę nie musisz wcale sprawdzać wyjątku, ponieważ musisz sprawdzić argumenty przed wywołaniem metody. Sprawdzanie wyniku względem zera nie rozróżnia argumentów prawnych 0, 0 i niedozwolonych argumentów negatywnych. API to naprawdę okropne IMHO.
BlackJack
Wpisy MS z załącznika K dla iostreamów C99 i C ++ to przykłady API, w których hak lub flaga radykalnie zmienia reakcję na awarie.
Deduplicator
37

Pracowali nad krytycznymi aplikacjami dotyczącymi lotnictwa, w których system nie mógł ulec awarii. W rezultacie ...

To interesujące wprowadzenie, które sprawia wrażenie, że motywacją tego projektu jest unikanie rzucania wyjątków w niektórych kontekstach, „ponieważ system może wtedy spaść” . Ale jeśli system „może zostać wyłączony z powodu wyjątku”, jest to wyraźne wskazanie, że

  • wyjątki nie są obsługiwane właściwie , przynajmniej nie sztywno.

Jeśli więc program, który korzysta z tego programu, AreaCalculatorjest wadliwy, Twój kolega woli nie mieć programu „wcześnie upaść”, ale zwrócić pewną złą wartość (mając nadzieję, że nikt go nie zauważy lub nikt nie zrobi z nim czegoś ważnego). To faktycznie maskuje błąd , a z mojego doświadczenia wynika, że ​​prędzej czy później doprowadzi do kolejnych błędów, dla których trudno jest znaleźć podstawową przyczynę.

IMHO napisanie programu, który w żadnym wypadku nie ulega awarii, ale pokazuje nieprawidłowe dane lub wyniki obliczeń, zwykle nie jest w żaden sposób lepszy niż pozwolenie na awarię programu. Jedynym właściwym podejściem jest umożliwienie dzwoniącemu zauważenia błędu, zaradzenia mu, umożliwienia mu podjęcia decyzji, czy użytkownik musi zostać poinformowany o niewłaściwym zachowaniu i czy można bezpiecznie kontynuować przetwarzanie lub czy jest to bezpieczniejsze aby całkowicie zatrzymać program. Dlatego poleciłbym jedno z poniższych:

  • utrudniają przeoczenie faktu, że funkcja może zgłosić wyjątek. Dokumentacja i standardy kodowania są tutaj Twoim przyjacielem, a regularne przeglądy kodu powinny wspierać prawidłowe użycie komponentów i odpowiednią obsługę wyjątków.

  • wytrenuj zespół, aby oczekiwał i radził sobie z wyjątkami, gdy używają komponentów „czarnej skrzynki” i mając na uwadze globalne zachowanie programu.

  • jeśli z jakichś powodów uważasz, że nie możesz uzyskać kodu wywołującego (lub deweloperów, którzy go piszą), aby prawidłowo obsługiwał wyjątki, to w ostateczności możesz zaprojektować interfejs API z wyraźnymi zmiennymi wyjściowymi błędów i bez wyjątków, takich jak

    CalculateArea(int x, int y, out ErrorCode err)

    więc dzwoniącemu trudno przeoczyć tę funkcję, może się nie powieść. Ale to jest bardzo brzydkie IMHO w C #; jest to stara defensywna technika programowania z C, w której nie ma wyjątków, i normalnie nie jest konieczne, aby działać w dzisiejszych czasach.

Doktor Brown
źródło
3
„pisanie programu, który nie ulega awarii pod żadnym pozorem, ale pokazuje nieprawidłowe dane lub wyniki obliczeń, zwykle nie jest w żaden sposób lepszy niż pozwolenie na awarię programu” Ogólnie w pełni się zgadzam, chociaż mogłem sobie wyobrazić, że w lotnictwie prawdopodobnie wolałbym mieć samolot nadal idzie z instrumentami pokazującymi nieprawidłowe wartości w porównaniu do wyłączenia komputera samolotu. W przypadku mniej krytycznych aplikacji zdecydowanie lepiej nie maskować błędów.
Trilarion
18
@Trilarion: jeśli program dla komputera pokładowego nie zawiera właściwej obsługi wyjątków, „naprawienie tego” przez spowodowanie, że komponenty nie będą zgłaszać wyjątków, jest bardzo błędnym podejściem. Jeśli program ulegnie awarii, powinien istnieć nadmiarowy system tworzenia kopii zapasowych, który może przejąć kontrolę. Jeśli na przykład program nie ulega awarii i pokazuje niewłaściwą wysokość, piloci mogą pomyśleć „wszystko jest w porządku”, podczas gdy samolot wpada na następną górę.
Doc Brown
7
@Trilarion: jeśli komputer pokładowy pokazuje niewłaściwą wysokość, a samolot z tego powodu się zawiesi, to również ci nie pomoże (szczególnie, gdy istnieje system zapasowy i nie otrzymujesz informacji, że musi przejąć kontrolę). Systemy do tworzenia kopii zapasowych komputerów samolotów nie są nowym pomysłem, Google na „systemy do tworzenia kopii zapasowych samolotów”, jestem prawie pewien, że inżynierowie na całym świecie zawsze wbudowywali nadmiarowe systemy w każdy system krytyczny dla życia (a gdyby nie dlatego, że nie stracili ubezpieczenie).
Doc Brown
4
To. Jeśli nie możesz sobie pozwolić na awarię programu, nie możesz pozwolić sobie na ciche udzielanie błędnych odpowiedzi. Prawidłowa odpowiedź to odpowiednia obsługa wyjątków we wszystkich przypadkach. W przypadku witryny internetowej oznacza to globalny moduł obsługi, który konwertuje nieoczekiwane błędy na 500. Możesz również mieć dodatkowe moduły obsługi dla bardziej specyficznych sytuacji, takich jak posiadanie try/ catchwewnątrz pętli, jeśli potrzebujesz przetwarzania, aby kontynuować, jeśli jeden element zawiedzie.
jpmc26
2
Uzyskanie niewłaściwego wyniku jest zawsze najgorszym rodzajem niepowodzenia; przypomina mi to zasadę dotyczącą optymalizacji: „popraw ją przed optymalizacją, ponieważ szybsze uzyskanie błędnej odpowiedzi nadal nie przynosi korzyści nikomu”.
Toby Speight
13

Pisanie testów jednostkowych staje się nieco bardziej skomplikowane, ponieważ za każdym razem musisz testować flagę wyjątku.

Każda funkcja z parametrami n będzie trudniejsza do przetestowania niż funkcja z parametrami n-1 . Rozłóż to na absurd, a argumentem jest, że funkcje w ogóle nie powinny mieć parametrów, ponieważ to sprawia, że ​​najłatwiej je przetestować.

Chociaż pisanie kodu, który jest łatwy do przetestowania, jest świetnym pomysłem, straszne jest umieszczanie prostoty testowania nad pisaniem kodu, które jest przydatne dla osób, które muszą go wywoływać. Jeśli przykład w pytaniu zawiera przełącznik, który określa, czy zgłoszony jest wyjątek, możliwe jest, że osoby dzwoniące, które chcą tego zachowania, zasługują na dodanie go do funkcji. Gdzie granica między złożonymi a zbyt złożonymi kłamstwami jest wezwaniem do oceny; każdy, kto próbuje ci powiedzieć, że jasna linia obowiązuje we wszystkich sytuacjach, powinien być podejrzliwy.

Ponadto, jeśli coś pójdzie nie tak, czy nie chcesz od razu wiedzieć?

To zależy od twojej definicji zła. Przykład w pytaniu definiuje błędnie jako „biorąc pod uwagę wymiar mniejszy od zera i shouldThrowExceptionsjest prawdziwy”. Nadanie wymiaru, jeśli mniej niż zero, nie jest błędne, gdy shouldThrowExceptionsjest fałszywe, ponieważ przełącznik indukuje inne zachowanie. To po prostu nie jest wyjątkowa sytuacja.

Prawdziwy problem polega na tym, że przełącznik został źle nazwany, ponieważ nie opisuje, co robi funkcja. Gdyby nadano mu lepszą nazwę treatInvalidDimensionsAsZero, zadałbyś to pytanie?

Czy to nie osoba dzwoniąca powinna decydować, jak kontynuować?

Rozmówca ma ustalić, jak kontynuować. W takim przypadku robi to wcześniej, ustawiając lub usuwając, shouldThrowExceptionsa funkcja zachowuje się zgodnie ze swoim stanem.

Przykład jest patologicznie prosty, ponieważ wykonuje pojedyncze obliczenia i zwraca. Jeśli uczynisz go nieco bardziej złożonym, takim jak obliczanie sumy pierwiastków kwadratowych z listy liczb, zgłaszanie wyjątków może powodować problemy z dzwoniącymi, których nie mogą rozwiązać. Jeśli przekażę listę, [5, 6, -1, 8, 12]a funkcja zgłasza wyjątek -1, nie mam możliwości powiedzieć funkcji, aby kontynuowała działanie, ponieważ zostanie ona już przerwana i wyrzuci sumę. Jeśli lista jest ogromnym zestawem danych, generowanie kopii bez liczb ujemnych przed wywołaniem funkcji może być niepraktyczne, więc jestem zmuszony powiedzieć z góry, jak należy traktować nieprawidłowe liczby, albo w formie „po prostu je zignoruj” „zmień, a może przekaż lambda, który zostanie wezwany do podjęcia tej decyzji.

Jego logika / rozumowanie polega na tym, że nasz program musi zrobić jedną rzecz, pokazać dane użytkownikowi. Wszelkie inne wyjątki, które nas nie powstrzymują, powinny zostać zignorowane. Zgadzam się, że nie powinno się ich ignorować, ale powinny bańkować i zajmować się nimi odpowiednia osoba, i nie muszą w tym celu zajmować się flagami.

Ponownie, nie ma jednego uniwersalnego rozwiązania. W tym przykładzie funkcja została prawdopodobnie zapisana w specyfikacji, która mówi, jak postępować z wymiarami ujemnymi. Ostatnią rzeczą, którą chcesz zrobić, jest obniżenie stosunku sygnału do szumu w dziennikach poprzez wypełnienie ich komunikatami „normalnie wyjątek zostałby tu zgłoszony, ale dzwoniący powiedział, aby nie zawracać sobie głowy”.

Jako jeden z tych znacznie starszych programistów prosiłbym, abyście uprzejmie odeszli od mojego trawnika. ;-)

Blrfl
źródło
Zgadzam się, że nazewnictwo i zamiar są niezwykle ważne i w tym przypadku właściwa nazwa parametru może naprawdę zmienić tabele, więc +1, ale 1. our program needs to do 1 thing, show data to user. Any other exception that doesn't stop us from doing so should be ignoredsposób myślenia może prowadzić do podjęcia decyzji przez użytkownika na podstawie niewłaściwych danych (ponieważ tak naprawdę program musi to zrobić 1 rzecz - aby pomóc użytkownikowi w podejmowaniu świadomych decyzji) 2. podobne przypadki bool ExecuteJob(bool throwOnError = false)są zwykle bardzo podatne na błędy i prowadzą do kodu, który trudno jest uzasadnić po prostu czytając go.
Eugene Podskal
@EugenePodskal Myślę, że założeniem jest, że „pokaż dane” oznacza „pokaż prawidłowe dane”. Pytający nie twierdzi, że gotowy produkt nie działa, tylko że może być napisany „źle”. W drugim punkcie musiałbym zobaczyć twarde dane. W moim bieżącym projekcie mam kilka często używanych funkcji, które mają przełącznik rzut / brak rzucania i nie są trudniejsze do uzasadnienia niż jakakolwiek inna funkcja, ale to jeden punkt danych.
Blrfl
Dobra odpowiedź, myślę, że ta logika dotyczy znacznie szerszego zakresu okoliczności niż tylko PO. BTW, nowy cpp ma wersję rzut / bez rzucania właśnie z tych powodów. Wiem, że istnieją małe różnice, ale ...
drjpizzle
8

Kod „krytyczny” dla bezpieczeństwa i „normalny” może prowadzić do bardzo różnych wyobrażeń o tym, jak wygląda „dobra praktyka”. Wiele się nakłada - niektóre rzeczy są ryzykowne i należy ich unikać w obu przypadkach - ale nadal istnieją znaczne różnice. Jeśli dodasz wymóg, aby być pewnym, że będą reagować, te odchylenia stają się dość znaczne.

Często dotyczą one rzeczy, których można się spodziewać:

  • W przypadku git, zła odpowiedź może być bardzo zła w stosunku do: zbyt długiego / przerywania / zawieszania się, a nawet awarii (które w rzeczywistości nie są problemami, powiedzmy, przypadkową zmianą zameldowanego kodu).

    Jednak: w przypadku tablicy przyrządów z blokadą obliczeniową siły g uniemożliwiającą wykonanie obliczenia prędkości powietrza może być nie do przyjęcia.

Niektóre są mniej oczywiste:

  • Jeśli przetestowali wiele , wyniki pierwszego rzędu (jak prawidłowych odpowiedzi) nie są tak duże zmartwienie stosunkowo mówienia. Wiesz, że twoje testy to pokryją. Jednak jeśli był ukryty stan lub przepływ kontrolny, nie wiesz, że nie będzie to przyczyną czegoś o wiele bardziej subtelnego. Trudno to wykluczyć podczas testowania.

  • Wykazanie bezpieczeństwa jest względnie ważne. Niewielu klientów usiądzie z uzasadnieniem, czy kupowane przez nich źródło jest bezpieczne, czy nie. Jeśli jesteś na rynku lotniczym, z drugiej strony ...

Jak to się odnosi do twojego przykładu:

Nie wiem Istnieje wiele procesów myślowych, które mogą mieć wiodące reguły, takie jak „Kod produkcji bez użycia jednego”, który jest stosowany w kodzie krytycznym dla bezpieczeństwa, co byłoby dość głupie w bardziej typowych sytuacjach.

Niektóre odnoszą się do osadzania, niektóre bezpieczeństwa, a może inne ... Niektóre są dobre (potrzebne były ścisłe ograniczenia wydajności / pamięci), niektóre są złe (nie obsługujemy wyjątków właściwie, więc najlepiej nie ryzykować). Przez większość czasu nawet wiedza, dlaczego to zrobili, tak naprawdę nie odpowiada na pytanie. Na przykład, jeśli ma to na celu łatwiejsze kontrolowanie kodu niż poprawienie go, czy jest to dobra praktyka? Naprawdę nie możesz powiedzieć. Są różnymi zwierzętami i wymagają innego traktowania.

To powiedziawszy, wygląda mi na podejrzanego, ALE :

Niezbędne dla bezpieczeństwa decyzje dotyczące oprogramowania i projektowania oprogramowania prawdopodobnie nie powinny być podejmowane przez nieznajomych podczas wymiany stosów inżynierii oprogramowania. Może to być dobry powód, aby to zrobić, nawet jeśli jest to część złego systemu. Nie czytaj zbyt wiele na nic innego niż jako „pożywienie do namysłu”.

drjpizzle
źródło
7

Czasami zgłoszenie wyjątku nie jest najlepszą metodą. Nie tylko z powodu rozwijania stosu, ale czasami dlatego, że wyłapanie wyjątku jest problematyczne, szczególnie wzdłuż połączeń językowych lub interfejsu.

Dobrym sposobem na poradzenie sobie z tym jest zwrócenie wzbogaconego typu danych. Ten typ danych ma stan wystarczający do opisania wszystkich szczęśliwych ścieżek i wszystkich nieszczęśliwych ścieżek. Chodzi o to, że jeśli wejdziesz w interakcję z tą funkcją (członek / globalny / w inny sposób), będziesz zmuszony poradzić sobie z wynikiem.

Biorąc to pod uwagę, ten wzbogacony typ danych nie powinien wymuszać działania. Wyobraź sobie w swojej okolicy przykład czegoś takiego var area_calc = new AreaCalculator(); var volume = area_calc.CalculateArea(x, y) * z;. Wydaje się, że przydatne volumepowinno być pole pomnożone przez głębokość - może to być sześcian, walec itp.

Ale co, jeśli usługa area_calc nie działa? Następnie area_calc .CalculateArea(x, y)zwrócił bogaty typ danych zawierający błąd. Czy pomnażanie tego jest legalne z? To dobre pytanie. Możesz zmusić użytkowników do natychmiastowego sprawdzenia. To jednak psuje logikę obsługi błędów.

var area_calc = new AreaCalculator();
var area_result = area_calc.CalculateArea(x, y);
if (area_result.bad())
{
    //handle unhappy path
}
var volume = area_result.value() * z;

vs

var area_calc = new AreaCalculator();
var volume = area_calc.CalculateArea(x, y) * z;
if (volume.bad())
{
    //handle unhappy path
}

Zasadniczo logika jest podzielona na dwie linie i podzielona przez obsługę błędów w pierwszym przypadku, podczas gdy drugi przypadek ma całą odpowiednią logikę w jednym wierszu, a następnie obsługę błędów.

W tym drugim przypadku volumejest to bogaty typ danych. To nie tylko liczba. To sprawia, że ​​pamięć jest większa i volumenadal trzeba będzie zbadać ją pod kątem wystąpienia błędu. Dodatkowo volumemoże zasilać inne obliczenia, zanim użytkownik zdecyduje się obsłużyć błąd, co pozwoli mu zamanifestować się w kilku różnych lokalizacjach. Może to być dobre lub złe w zależności od specyfiki sytuacji.

Alternatywnie volumemoże to być zwykły typ danych - tylko liczba, ale co stanie się z warunkiem błędu? Może się zdarzyć, że wartość domyślnie przekształci się, jeśli będzie w szczęśliwym stanie. Gdyby był w niezadowolonym stanie, może zwrócić wartość domyślną / błąd (dla obszaru 0 lub -1 może wydawać się rozsądny). Alternatywnie może wygenerować wyjątek po tej stronie granicy interfejsu / języka.

... foo() {
   var area_calc = new AreaCalculator();
   return area_calc.CalculateArea(x, y) * z;
}
var volume = foo();
if (volume <= 0)
{
    //handle error
}

vs.

... foo() {
   var area_calc = new AreaCalculator();
   return area_calc.CalculateArea(x, y) * z;
}

try { var volume = foo(); }
catch(...)
{
    //handle error
}

Przekazując złą lub potencjalnie złą wartość, na użytkownika spoczywa duży ciężar weryfikacji danych. Jest to źródło błędów, ponieważ jeśli chodzi o kompilator, zwracana wartość jest prawidłową liczbą całkowitą. Jeśli coś nie zostało sprawdzone, odkryjesz to, gdy coś pójdzie nie tak. Drugi przypadek łączy to, co najlepsze z obu światów, zezwalając wyjątkom na obsługę nieszczęśliwych ścieżek, podczas gdy szczęśliwe ścieżki przebiegają normalnie. Niestety zmusza to użytkownika do rozsądnego obchodzenia się z wyjątkami, co jest trudne.

Żeby było jasne, niezadowolona ścieżka to przypadek nieznany logice biznesowej (domena wyjątku), nieudana walidacja jest dobrą ścieżką, ponieważ wiesz, jak sobie z tym poradzić według reguł biznesowych (domena reguł).

Najlepszym rozwiązaniem byłoby takie, które zezwala na wszystkie scenariusze (w granicach rozsądku).

  • Użytkownik powinien mieć możliwość zapytania o zły stan i natychmiast go obsłużyć
  • Użytkownik powinien mieć możliwość działania na wzbogaconym typie, tak jakby podążono ścieżką szczęśliwą i propagować szczegóły błędu.
  • Użytkownik powinien mieć możliwość wyodrębnienia wartości szczęśliwej ścieżki za pomocą rzutowania (domyślnie / jawnie, co jest uzasadnione), generując wyjątek dla niezadowolonych ścieżek.
  • Użytkownik powinien być w stanie wyodrębnić wartość szczęśliwej ścieżki lub użyć wartości domyślnej (dostarczonej lub nie)

Coś jak:

Rich::value_type value_or_default(Rich&, Rich::value_type default_value = ...);
bool bad(Rich&);
...unhappy path report... bad_state(Rich&);
Rich& assert_not_bad(Rich&);
class Rich
{
public:
   typedef ... value_type;

   operator value_type() { assert_not_bad(*this); return ...value...; }
   operator X(...) { if (bad(*this)) return ...propagate badness to new value...; /*operate and generate new value*/; }
}

//check
if (bad(x))
{
    var report = bad_state(x);
    //handle error
}

//rethrow
assert_not_bad(x);
var result = (assert_not_bad(x) + 23) / 45;

//propogate
var y = x * 23;

//implicit throw
Rich::value_type val = x;
var val = ((Rich::value_type)x) + 34;
var val2 = static_cast<Rich::value_type>(x) % 3;

//default value
var defaulted = value_or_default(x);
var defaulted_to = value_or_default(x, 55);
Kain0_0
źródło
@TobySpeight Wystarczy, te rzeczy są wrażliwe na kontekst i mają swój zakres.
Kain0_0
Myślę, że problemem są bloki „assert_not_bad”. Myślę, że skończy się to w tym samym miejscu, w którym oryginalny kod próbował rozwiązać. W testach należy je jednak zauważyć, jeśli naprawdę są twierdzeniami, należy je zdjąć przed produkcją na prawdziwym samolocie. w przeciwnym razie kilka świetnych punktów.
drjpizzle
@drjpizzle Argumentowałbym, że gdyby było wystarczająco ważne, aby dodać osłonę do testowania, ważne jest, aby pozostawić osłonę na miejscu podczas pracy w produkcji. Obecność samego strażnika wywołuje wątpliwości. Jeśli wątpisz w kod na tyle, aby go chronić podczas testowania, wątpisz w to z przyczyn technicznych. tj. Warunek może / faktycznie występuje. Wykonywanie testów nie dowodzi, że warunek ten nigdy nie zostanie osiągnięty w produkcji. Oznacza to, że istnieje znany warunek, który może wystąpić, który należy gdzieś rozwiązać. Myślę, że to, jak sobie z tym poradzić, stanowi problem.
Kain0_0
3

Odpowiem z punktu widzenia C ++. Jestem prawie pewien, że wszystkie podstawowe koncepcje można przenieść do C #.

Wygląda na to, że preferowanym stylem jest „zawsze zgłaszaj wyjątki”:

int CalculateArea(int x, int y) {
    if (x < 0 || y < 0) {
        throw Exception("negative side lengths");
    }
    return x * y;
}

Może to stanowić problem dla kodu C ++, ponieważ obsługa wyjątków jest ciężka - powoduje to, że przypadek awarii działa wolno, i sprawia, że ​​przypadek awarii alokuje pamięć (która czasami nawet nie jest dostępna) i ogólnie czyni rzeczy mniej przewidywalnymi. Ciężka waga EH jest jednym z powodów, dla których słyszysz ludzi mówiących na przykład: „Nie używaj wyjątków dla kontroli przepływu”.

Tak więc niektóre biblioteki (takie jak <filesystem>) używają tego, co C ++ nazywa „podwójnym API” lub tego, co C # nazywa Try-Parsewzorcem (dzięki Peter za wskazówkę!)

int CalculateArea(int x, int y) {
    if (x < 0 || y < 0) {
        throw Exception("negative side lengths");
    }
    return x * y;
}

bool TryCalculateArea(int x, int y, int& result) {
    if (x < 0 || y < 0) {
        return false;
    }
    result = x * y;
    return true;
}

int a1 = CalculateArea(x, y);
int a2;
if (TryCalculateArea(x, y, a2)) {
    // use a2
}

Od razu widać problem z „podwójnymi interfejsami API”: dużo duplikacji kodu, brak wskazówek dla użytkowników, który interfejs API jest „właściwy” do użycia, a użytkownik musi dokonać trudnego wyboru między użytecznymi komunikatami o błędach ( CalculateArea) i speed ( TryCalculateArea), ponieważ szybsza wersja bierze nasz użyteczny "negative side lengths"wyjątek i spłaszcza go w bezużyteczny false- „coś poszło nie tak, nie pytaj mnie co i gdzie”. (Niektóre podwójne API użyć bardziej wyrazisty rodzaj błędu, jak int errnoi C ++ 's std::error_code, ale to nadal nie powiedzieć, gdzie wystąpił błąd - po prostu, że nie występują gdzieś).

Jeśli nie możesz zdecydować, jak powinien zachowywać się Twój kod, zawsze możesz podważyć decyzję dzwoniącego!

template<class F>
int CalculateArea(int x, int y, F errorCallback) {
    if (x < 0 || y < 0) {
        return errorCallback(x, y, "negative side lengths");
    }
    return x * y;
}

int a1 = CalculateArea(x, y, [](auto...) { return 0; });
int a2 = CalculateArea(x, y, [](int, int, auto msg) { throw Exception(msg); });
int a3 = CalculateArea(x, y, [](int, int, auto) { return x * y; });

Zasadniczo to robi twój współpracownik; poza tym, że rozdziela on „moduł obsługi błędów” na zmienną globalną:

std::function<int(const char *)> g_errorCallback;

int CalculateArea(int x, int y) {
    if (x < 0 || y < 0) {
        return g_errorCallback("negative side lengths");
    }
    return x * y;
}

g_errorCallback = [](auto) { return 0; };
int a1 = CalculateArea(x, y);
g_errorCallback = [](const char *msg) { throw Exception(msg); };
int a2 = CalculateArea(x, y);

Przeniesienie ważnych parametrów z jawnych parametrów funkcji do stanu globalnego jest prawie zawsze złym pomysłem. Nie polecam tego. (Fakt, że w twoim przypadku nie jest to stan globalny, ale po prostu państwo członkowskie w całej instancji łagodzi nieco złą sytuację, ale niewiele).

Ponadto współpracownik niepotrzebnie ogranicza liczbę możliwych zachowań związanych z obsługą błędów. Zamiast pozwolić żadnej obsługi błędów lambda, on zdecydował się na tylko dwa:

bool g_errorViaException;

int CalculateArea(int x, int y) {
    if (x < 0 || y < 0) {
        return g_errorViaException ? throw Exception("negative side lengths") : 0;
    }
    return x * y;
}

g_errorViaException = false;
int a1 = CalculateArea(x, y);
g_errorViaException = true;
int a2 = CalculateArea(x, y);

Jest to prawdopodobnie „kwaśne miejsce” spośród którejkolwiek z tych możliwych strategii. Odebrałeś całą elastyczność użytkownikowi końcowemu, zmuszając go do korzystania z jednego z twoich dokładnie dwóch zwrotnych poleceń obsługi błędów; i masz wszystkie problemy wspólnego stanu globalnego; i nadal płacisz za tę gałąź warunkową wszędzie.

Wreszcie, powszechnym rozwiązaniem w C ++ (lub dowolnym języku z kompilacją warunkową) byłoby zmuszenie użytkownika do podjęcia decyzji dla całego programu, globalnie, w czasie kompilacji, tak aby niepoddaną ścieżkę kodową można było całkowicie zoptymalizować:

int CalculateArea(int x, int y) {
    if (x < 0 || y < 0) {
#ifdef NEXCEPTIONS
        return 0;
#else
        throw Exception("negative side lengths");
#endif
    }
    return x * y;
}

// Now these two function calls *must* have the same behavior,
// which is a nice property for a program to have.
// Improves understandability.
//
int a1 = CalculateArea(x, y);
int a2 = CalculateArea(x, y);

Przykładem tego, co działa w ten sposób, jest assertmakro w C i C ++, które warunkuje jego zachowanie na makrze preprocesora NDEBUG.

Quuxplusone
źródło
Jeśli ktoś zwraca std::optionalze TryCalculateArea()zamiast, to jest proste ujednolicenie realizację obu częściach podwójnym interfejsem w jednym funkcyjnym-Szablon z kompilacji flagą.
Deduplicator
@Deduplicator: Może z std::expected. Wystarczy std::optional, chyba że źle zrozumiem zaproponowane przez Ciebie rozwiązanie, nadal ucierpi na tym, co powiedziałem: użytkownik musi dokonać trudnego wyboru między użytecznymi komunikatami o błędach a szybkością, ponieważ szybsza wersja bierze nasz użyteczny "negative side lengths"wyjątek i spłaszcza go do bezużytecznego false- coś poszło nie tak, nie pytaj mnie co i gdzie. ”
Quuxplusone
Właśnie dlatego libc ++ <filesystem> faktycznie robi coś bardzo zbliżonego do wzorca współpracownika OP: std::error_code *ecprzepływa przez wszystkie poziomy API, a następnie na dole robi moralny odpowiednik if (ec == nullptr) throw something; else *ec = some error code. ( ifErrorHandler
Abstrahuje to,
Cóż, byłaby to opcja, aby zachować rozszerzone informacje o błędach bez rzucania. Może być odpowiedni lub nie wart potencjalnego dodatkowego kosztu.
Deduplicator
1
Tyle dobrych myśli zawartych w tej odpowiedzi ... Zdecydowanie potrzebuje więcej głosów pozytywnych :-)
cmaster
1

Myślę, że należy wspomnieć, skąd wziął się twój kolega.

Obecnie C # ma wzór TryGet public bool TryThing(out result). Pozwala to uzyskać wynik, a jednocześnie dać ci znać, czy ten wynik jest nawet prawidłową wartością. (Na przykład wszystkie intwartości są poprawnymi wynikami Math.sum(int, int), ale jeśli wartość przepełni się, ten konkretny wynik może być śmieci). Jest to jednak stosunkowo nowy wzór.

Przed outsłowem kluczowym albo trzeba było rzucić wyjątek (kosztowny, a program wywołujący musi go złapać lub zabić cały program), utworzyć specjalną strukturę (klasa przed klasą lub rodzajami była naprawdę rzeczą) dla każdego wyniku reprezentującego wartość oraz możliwe błędy (czasochłonne w tworzeniu i rozszerzaniu oprogramowania) lub zwracanie domyślnej wartości „błąd” (która mogła nie być błędem).

Podejście, z którego korzysta Twój kolega, daje im wczesny wyjątek od wyjątków podczas testowania / debugowania nowych funkcji, a jednocześnie zapewnia bezpieczeństwo i wydajność w czasie wykonywania (wydajność była krytycznym problemem ~ 30 lat temu) po prostu zwracając domyślną wartość błędu. Teraz jest to wzorzec, w którym oprogramowanie zostało zapisane, i oczekiwany wzorzec idzie naprzód, więc naturalne jest, aby robić to w ten sposób, mimo że istnieją teraz lepsze sposoby. Najprawdopodobniej ten wzór został odziedziczony z epoki oprogramowania lub wzorca, z którego twoja uczelnia nigdy nie wyrosła (stare nawyki trudno przełamać).

Inne odpowiedzi już wyjaśniają, dlaczego jest to uważane za złą praktykę, więc po prostu zakończę zaleceniem przeczytania wzoru TryGet (być może również enkapsulacji tego, co obietnica powinna złożyć obiektowi wywołującemu).

Tezra
źródło
Przed outsłowem kluczowym napiszesz boolfunkcję, która prowadzi wskaźnik do wyniku, tj ref. Parametr. Możesz to zrobić w VB6 w 1998 roku. Słowo outkluczowe po prostu kupuje pewność kompilacji w czasie, że parametr jest przypisywany po powrocie funkcji, to wszystko. Jest to jednak miły i użyteczny wzór.
Mathieu Guindon
@MathieuGuindon Tak, ale GetTry nie był jeszcze dobrze znanym / ustalonym wzorcem, a nawet gdyby tak było, nie jestem do końca pewien, czy zostałby użyty. W końcu część ołowiu do Y2K polegała na tym, że przechowywanie czegoś większego niż 0-99 było niedopuszczalne.
Tezra
0

Są chwile, kiedy chcesz zastosować jego podejście, ale nie uważałbym ich za „normalną” sytuację. Kluczem do ustalenia, w którym jesteś przypadku, jest:

Jego logika / rozumowanie polega na tym, że nasz program musi zrobić jedną rzecz, pokazać dane użytkownikowi. Wszelkie inne wyjątki, które nas nie powstrzymują, powinny zostać zignorowane.

Sprawdź wymagania. Jeśli twoje wymagania mówią, że masz jedno zadanie, czyli pokazywanie danych użytkownikowi, to ma rację. Jednak z mojego doświadczenia wynika , że większość użytkowników również dba o to, jakie dane są wyświetlane. Chcą poprawnych danych. Niektóre systemy po prostu chcą zawieść cicho i pozwolić użytkownikowi stwierdzić, że coś poszło nie tak, ale uważam je za wyjątek od reguły.

Kluczowe pytanie, które zadałbym po awarii, brzmi: „Czy system jest w stanie, w którym oczekiwania użytkownika i niezmienniki oprogramowania są aktualne?” Jeśli tak, to po prostu wróć i kontynuuj. W praktyce nie dzieje się tak w większości programów.

Jeśli chodzi o samą flagę, flaga wyjątków jest zwykle uważana za zapach kodu, ponieważ użytkownik musi w jakiś sposób wiedzieć, w którym trybie jest moduł, aby zrozumieć, jak działa ta funkcja. Jeśli jest w !shouldThrowExceptionstrybie, użytkownik musi wiedzieć, że jest odpowiedzialny za wykrywanie błędów i utrzymywanie oczekiwań i niezmienników, gdy wystąpią. Są również odpowiedzialni od razu na linii, na której wywoływana jest funkcja. Taka flaga jest zwykle bardzo myląca.

Tak się jednak zdarza. Weź pod uwagę, że wiele procesorów pozwala na zmianę zachowania zmiennoprzecinkowego w programie. Program, który chce mieć łagodniejsze standardy, może to zrobić po prostu zmieniając rejestr (który jest faktycznie flagą). Sztuczka polega na tym, że powinieneś być bardzo ostrożny, aby uniknąć przypadkowego nadepnięcia innym palcom. Kod często sprawdza bieżącą flagę, ustawia żądane ustawienie, wykonuje operacje, a następnie ustawia z powrotem. W ten sposób nikt nie dziwi się tej zmianie.

Cort Ammon
źródło
0

Ten konkretny przykład ma ciekawą funkcję, która może wpłynąć na reguły ...

CalculateArea(int x, int y)
{
    if(x < 0 || y < 0)
    {
        if(shouldThrowExceptions) 
            throwException;
        else
            return 0;
    }
}

Widzę tutaj warunek wstępny . Niepowodzenie sprawdzania warunków wstępnych oznacza błąd wyżej w stosie wywołań. Powstaje zatem pytanie, czy ten kod jest odpowiedzialny za zgłaszanie błędów zlokalizowanych gdzie indziej?

Niektóre napięcia tutaj wynikają z faktu, że ten interfejs wykazuje prymitywną obsesję - xi yprzypuszczalnie mają reprezentować rzeczywiste pomiary długości. W kontekście programowania, w którym konkretne typy domen są rozsądnym wyborem, w efekcie przenieślibyśmy warunek wstępny bliżej źródła danych - innymi słowy, podnieśliśmy odpowiedzialność za integralność danych dalej na stosie wywołań, gdzie mamy lepsze wyczucie kontekstu.

To powiedziawszy, nie widzę nic zasadniczo złego w posiadaniu dwóch różnych strategii zarządzania nieudanym czekiem. Wolę używać kompozycji, aby określić, która strategia jest w użyciu; flaga funkcji byłaby używana w katalogu głównym kompozycji, a nie w implementacji metody bibliotecznej.

// Configurable dependencies
AreaCalculator(PreconditionFailureStrategy strategy)

CalculateArea(int x, int y)
{
    if (x < 0 || y < 0) {
        return this.strategy.fail(0);
    }
    // ...
}

Pracowali nad krytycznymi aplikacjami dotyczącymi lotnictwa, w których system nie mógł ulec awarii.

National Traffic and Safety Board jest naprawdę dobra; Mogę zasugerować alternatywne techniki wdrażania dla siwobrodych, ale nie jestem skłonny spierać się z nimi o projektowanie grodzi w podsystemie raportowania błędów.

Mówiąc szerzej: jaki jest koszt dla firmy? Awarie witryny są o wiele tańsze niż w przypadku systemu o krytycznym znaczeniu dla życia.

VoiceOfUnreason
źródło
Podoba mi się propozycja alternatywnego sposobu zachowania pożądanej elastyczności podczas modernizacji.
drjpizzle
-1

Metody albo obsługują wyjątki, albo nie, nie ma potrzeby stosowania flag w językach takich jak C #.

public int Method1()
{
  ...code

 return 0;
}

Jeśli coś pójdzie nie tak w ... kodzie, to wyjątek będzie musiał zostać obsłużony przez osobę dzwoniącą. Jeśli nikt nie poradzi sobie z błędem, program zostanie zakończony.

public int Method1()
{
try {  
...code
}
catch {}
 ...Handle error 
}
return 0;
}

W takim przypadku, jeśli coś złego dzieje się w ... kodzie, Metoda 1 zajmuje się problemem i program powinien kontynuować.

To, gdzie poradzisz sobie z wyjątkami, zależy od ciebie. Z pewnością możesz je zignorować, łapiąc i nie robiąc nic. Ale upewniłbym się, że ignorujesz tylko określone rodzaje wyjątków, których możesz się spodziewać. Ignorowanie ( exception ex) jest niebezpieczne, ponieważ niektóre wyjątki, których nie chcesz ignorować, takie jak wyjątki systemowe dotyczące braku pamięci i tym podobne.

Jon Raynor
źródło
3
Obecna konfiguracja opublikowana przez OP dotyczy decyzji o dobrowolnym zgłoszeniu wyjątku. Kod OP nie prowadzi do niechcianego połykania rzeczy takich jak wyjątki braku pamięci. Jeśli już, twierdzenie, że wyjątki powodują awarię systemu, oznacza, że ​​podstawa kodu nie wychwytuje wyjątków, a zatem nie połyka żadnych wyjątków; zarówno te, które były i nie były celowo rzucane przez logikę biznesową OP.
Flater
-1

To podejście łamie filozofię „szybko zawieść, zawieść mocno”.

Dlaczego chcesz szybko zawieść:

  • Im szybciej zawiedziesz, tym bliżej widocznego objawu awarii będzie bliżej faktycznej przyczyny awarii. To sprawia, że ​​debugowanie jest znacznie łatwiejsze - w najlepszym przypadku wiersz błędu znajduje się w pierwszej linii śledzenia stosu.
  • Im szybciej zawiedziesz (i odpowiednio złapiesz błąd), tym mniej prawdopodobne jest, że pomylisz resztę programu.
  • Im trudniej ci się nie powiedzie (tzn. Rzucisz wyjątek zamiast tylko zwracać kod „-1” lub coś w tym rodzaju), tym bardziej prawdopodobne jest, że osoba dzwoniąca naprawdę dba o błąd i nie tylko pracuje z nieprawidłowymi wartościami.

Wady nie braku szybko i mocno:

  • Jeśli unikniesz widocznych awarii, tzn. Będziesz udawać, że wszystko jest w porządku, zwykle utrudniasz znalezienie rzeczywistego błędu. Wyobraź sobie, że wartość zwracana z twojego przykładu jest częścią procedury, która oblicza sumę 100 obszarów; tj. wywołanie tej funkcji 100 razy i zsumowanie zwracanych wartości. Jeśli cicho stłumisz błąd, nie będzie żadnego sposobu, aby dowiedzieć się, gdzie występuje rzeczywisty błąd; a wszystkie poniższe obliczenia będą po cichu błędne.
  • Jeśli opóźnisz niepowodzenie (zwracając niemożliwą wartość zwracaną, taką jak „-1” dla obszaru), zwiększasz prawdopodobieństwo, że osoba wywołująca twoją funkcję po prostu nie przejmuje się tym i zapomina obsłużyć błąd; mimo że mają pod ręką informacje o awarii.

Wreszcie, faktyczna obsługa błędów oparta na wyjątkach ma tę zaletę, że możesz przekazać komunikat o błędzie lub obiekt „poza pasmem”, możesz łatwo podpiąć się do logowania błędów, ostrzegania itp. Bez pisania jednej dodatkowej linii w kodzie domeny .

Istnieją więc nie tylko proste przyczyny techniczne, ale także powody „systemowe”, które sprawiają, że bardzo przydatne jest szybkie zawodzenie.

Na koniec dnia, nie zawodzi mocno i szybko w naszych obecnych czasach, w których obsługa wyjątków jest lekka, a bardzo stabilna jest tylko w połowie kryminalna. Rozumiem doskonale, skąd bierze się myśl, że dobrze jest tłumić wyjątki, ale to już nie ma zastosowania.

Zwłaszcza w danym przypadku, gdzie można nawet dać opcję o tym, czy rzucać wyjątki czy nie: oznacza to, że rozmówca musi zdecydować, czy inaczej . Więc nie ma żadnej wady, aby osoba dzwoniąca złapała wyjątek i odpowiednio go obsługiwała.

Jeden komentarz pojawia się w komentarzu:

W innych odpowiedziach wskazano, że szybkie i trudne awarie nie są pożądane w krytycznych aplikacjach, gdy sprzęt, na którym pracujesz, jest samolotem.

Awaria szybko i mocno nie oznacza, że ​​cała aplikacja ulega awarii. Oznacza to, że w miejscu, w którym wystąpił błąd, lokalnie ulega awarii. W przykładzie PO metoda niskiego poziomu obliczająca pewien obszar nie powinna po cichu zastępować błędu niewłaściwą wartością. Powinno wyraźnie zawieść.

Niektórzy dzwoniący w górę łańcucha oczywiście muszą wychwycić ten błąd / wyjątek i odpowiednio go obsłużyć. Jeśli ta metoda została zastosowana w samolocie, prawdopodobnie powinno to doprowadzić do zaświecenia się diody LED błędu lub przynajmniej do wyświetlenia „obszaru obliczania błędów” zamiast niewłaściwego obszaru.

AnoE
źródło
3
W innych odpowiedziach wskazano, że szybkie i trudne awarie nie są pożądane w krytycznych aplikacjach, gdy sprzęt, na którym pracujesz, jest samolotem.
HAEM
1
@HAEM, to nieporozumienie na temat tego, co oznacza porażka szybka i trudna. Do odpowiedzi dodałem akapit na ten temat.
AnoE
Nawet jeśli intencją nie jest, aby „tak mocno” zawiodło, nadal ryzykowne jest granie z tego rodzaju ogniem.
drjpizzle
Właśnie o to chodzi, @drjpizzle. Jeśli jesteś przyzwyczajony do szybkiego porażki, to z mojego doświadczenia nie jest to „ryzykowne” ani „bawiące się tego rodzaju ogniem”. Au contraire. Oznacza to, że przyzwyczajasz się do myślenia „co by się stało, gdybym tu miał wyjątek”, gdzie „tutaj” oznacza wszędzie , i masz tendencję do bycia świadomym, czy miejsce, w którym aktualnie programujesz, będzie miało poważny problem ( katastrofa lotnicza, cokolwiek w tym przypadku). Zabawa ogniem polegałaby na tym, że wszystko będzie w porządku, a każdy element ma wątpliwości, że wszystko jest w porządku ...
AnoE