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.
Odpowiedzi:
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.
źródło
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ć
może istnieć wiele instancji klasy o różnych konfiguracjach pływających w tym samym programie w tym samym czasie i
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.
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).
źródło
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
Jeśli więc program, który korzysta z tego programu,
AreaCalculator
jest 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
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.
źródło
try
/catch
wewnątrz pętli, jeśli potrzebujesz przetwarzania, aby kontynuować, jeśli jeden element zawiedzie.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.
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
shouldThrowExceptions
jest prawdziwy”. Nadanie wymiaru, jeśli mniej niż zero, nie jest błędne, gdyshouldThrowExceptions
jest 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?Rozmówca ma ustalić, jak kontynuować. W takim przypadku robi to wcześniej, ustawiając lub usuwając,
shouldThrowExceptions
a 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.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. ;-)
źródło
our program needs to do 1 thing, show data to user. Any other exception that doesn't stop us from doing so should be ignored
sposó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 przypadkibool 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.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”.
źródło
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 przydatnevolume
powinno 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 legalnez
? To dobre pytanie. Możesz zmusić użytkowników do natychmiastowego sprawdzenia. To jednak psuje logikę obsługi błędów.vs
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
volume
jest to bogaty typ danych. To nie tylko liczba. To sprawia, że pamięć jest większa ivolume
nadal trzeba będzie zbadać ją pod kątem wystąpienia błędu. Dodatkowovolume
moż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
volume
moż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.vs.
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).
Coś jak:
źródło
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”:
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 # nazywaTry-Parse
wzorcem (dzięki Peter za wskazówkę!)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żytecznyfalse
- „coś poszło nie tak, nie pytaj mnie co i gdzie”. (Niektóre podwójne API użyć bardziej wyrazisty rodzaj błędu, jakint errno
i C ++ 'sstd::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!
Zasadniczo to robi twój współpracownik; poza tym, że rozdziela on „moduł obsługi błędów” na zmienną globalną:
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:
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ć:
Przykładem tego, co działa w ten sposób, jest
assert
makro w C i C ++, które warunkuje jego zachowanie na makrze preprocesoraNDEBUG
.źródło
std::optional
zeTryCalculateArea()
zamiast, to jest proste ujednolicenie realizację obu częściach podwójnym interfejsem w jednym funkcyjnym-Szablon z kompilacji flagą.std::expected
. Wystarczystd::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żytecznegofalse
- coś poszło nie tak, nie pytaj mnie co i gdzie. ”std::error_code *ec
przepływa przez wszystkie poziomy API, a następnie na dole robi moralny odpowiednikif (ec == nullptr) throw something; else *ec = some error code
. (if
ErrorHandler
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 wszystkieint
wartości są poprawnymi wynikamiMath.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
out
sł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).
źródło
out
słowem kluczowym napiszeszbool
funkcję, która prowadzi wskaźnik do wyniku, tjref
. Parametr. Możesz to zrobić w VB6 w 1998 roku. Słowoout
kluczowe 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.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:
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
!shouldThrowExceptions
trybie, 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.
źródło
Ten konkretny przykład ma ciekawą funkcję, która może wpłynąć na reguły ...
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ę -
x
iy
przypuszczalnie 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.
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.
źródło
Metody albo obsługują wyjątki, albo nie, nie ma potrzeby stosowania flag w językach takich jak C #.
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.
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.źródło
To podejście łamie filozofię „szybko zawieść, zawieść mocno”.
Dlaczego chcesz szybko zawieść:
Wady nie braku szybko i mocno:
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:
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.
źródło