Rzucanie wyjątkami a "zwykłe" zaprojektowanie obsługi błędu

0
danek napisał(a):

gdy widzę ParseResult parse(String s) to od razu widzę z czym mam doczynienia

No nie wiem, ja się nie domyślam od razu PatternSyntaxException.
Prędzej spodziewam się FormatException?
A może IllegalFormatException?
Czy jednak IllegalArgumentException?
Co poleci kiedy String będzie tak duży, że poleci inne exception (DOM wielki XML cały do pamięci)?

1

wyjątek czy result

hmm, jeżeli jest fajnie przemyślany sposób na handling errorów to raczej nie ma potrzeby rzucać wyjątków,

ale gdy jesteśmy na siedemdziesiątym metrze pod ziemią w zagnieżdżeniu ifkuf i nagle brakuje my_hashtable[key], a w dodatku jest 15:40, no to może jednak Exception? bo przecież recovery byłoby ciężkie

w sumie zawsze starałem się na siłę nie używać wyjątków do obsługi mało prawdopodobnych i lecieć w różne Resulty,Try itd. ale później dużo nudnego boilerplejtu się z tego generuje

if (check1.CompletedSuccessfully && today is friday)
{
    var id = getId();
    var check2 = xd(id);

    if (check2.CompletedSuccessfully)
    {
        return 15;
    }
}
12

W kontekście PHP to nie wiem co powiedzieć - języki bez statycznego systemu typów są strasznie męczące w użyciu, nie bardzo można w nich zrobić bezpieczną w użyciu obsługę błędów.

Co do reszty - staticly typed.

Jest prosta (w teori) zasada przy projektowaniu api:
a) jeśli problem w wyliczeniu wyniku jest po stronie wywołującego funkcję (caller), a dokładniej problemem są podane argumenty - to wówczas powinniśmy zwrócić normalnie wynik. Może to oznaczać, że trzeba rozszerzyć typ zwracanego wyniku.

przykład prostszy:
parseInt(a:String): ParseResult
gdzie ParseResult ma np. metody isOK(), onSuccess( f) (co tam akurat modne)

przykład ciekawszy:
executeSql(a:String):R w przypadku jeśli podamy błędny SQL nie powinna rzucać wyjątkiem tylko zwracać odpowiedni rezultat (R), który niekoniecznie jest "listą wierszy", bo być może jest informacją co jest nie halo ze stringiem.

Dlaczego?
Bo wyjątki zwykle związane są z jakimś stacktracem i dodatkowymi zasobami - jeśli robimy funkcję executeSql(a:String), a ktoś robi akurat jakiegoś SQL Explorera, gdzie użytkownik wklepuje z palca jaki chce string.. to nie chcemy zawalać systemu jakimiś stacktracami - bo przecież błędny SQL to w takiej sytuacji norma (ludzie nie są zbyt bystrzy przecież).

Dawniej w takiej javie używało się CheckedException do modelowania nietypowych rezultatów (bo zmuszają klienta do obsługi) - to się nie sprawdziło. Obecnie raczej używamy czegoś w stylu Either - co jest po prostu generycznym rozwiązaniem na BadResult/GoodResult. (Żeby nie pisać podobnego kodu wiele razy). Kompilator wymusza na wywołujacym jakąś obsługę tej sytuacji.
Konkretnie omawiana funkcja może mieć sygnaturę:
executeSql(a:String):Either<SQLProblem,ResultList>

b) Jeśli jednak problem powstaje po stronie funkcji (ona temu winna), problem nie jest w argumentach - to wówczas możemy zwrócić wyjątek. Jest to sytuacja niespodziwana dla wywołującego (który może się na wiele przygotować, ale raczej nie spodziewa się, że funkcja jest do kitu).
Przykład:
jeśli mamy funkcję getUserFromDB(id:UUID) : User, a w środku tej funkcji jest wykonany executeSQL i okaże się, że spierniczyliśmy SQL to w tej sytuacji powinniśmy raczej rzucić wyjątkiem. To my (robiąc funkcję getUserDB) zepsuliśmy coś, nie możemy zwalić winy na wywołującego (że złe id). Więc złóżmy się ładnie, wyprodukujmy stack trace - niech chociaż będą jasne dowody winy. Tu zachodzi transformacja: zwykły rezultat na niższym poziomie (SQLProblem) przechodzi w wyjątek na poziomie wyższym - WrongSQLException.

Caller i tak nic nie zdziała raczej - może chcieć na jakimś tam poziomie złapać wyjątek i obsłużyć, ale ta obsługa to zwykle będzie wrzucenie informacji do loga diagnostycznego. Czasem uparciuch może powtórzyć i liczyć, że nam przeszło.

Warto zauważyć, że getUserFromDB powinien raczej wyglądać tak: getUserFromDB( id:UUID) : Option<User>, bo pewnie nie wszystkie UUID prowadzą do znalezienia usera w bazie danych. A szukanie po złym id to raczej wina wywołujacego (przypadek a)).

W javie tu się dawniej używało RuntimeException (wyjątki dziedziczące z RE) i obecnie RuntimeException to jest nadal dobre rozwiązanie na taki przypadek.

Oczywiście w praktyce całkiem często spotykamy przypadki, gdzie nie wiadomo, gdzie tak naprawdę leży wina (w argumentach czy stronie wywoływanej funkcji) i mamy wątpliwości. Trudno.

2
TomRiddle napisał(a):

Trochę jakby zamykać oczy i krzyczeć: "mam tylko jedną warstwę w swojej aplikacji". Wtedy faktycznie ma się jedną perspektywę.

Efektem mojej pracy ma być prawidłowo działająca aplikacja, o jej prawidłowym działaniu upewniam patrząc w różne metryki, m.in. liczbę rzucanych wyjątków. I nie chciałbym, żeby jakiś DatabaseConnectionException umknął w gąszczu UserAgeCanNotBeNegativeException ani też mieć setek tysięcy wyjątków w logach, podczas gdy w rzeczywistości nie było żadnego.
Pracowałem przy systemach robionych w taki sposób, nie było to wygodne.

Tylko skąd klasa nisko, ma wiedzieć jakiego rodzaju informację o błędzie zwrócić albo czy w ogóle zwrócić. Klasa nisko nie powinna wiedzieć, czy z punktu widzenia aplikacji taka sytuacja powinna być obsłużona czy nie.

Klasa zwraca wynik: prawidłowy bądź błędny. O tym, czy informację o błędzie ignorujemy i np. powtarzamy próbę danej operacji, czy informujemy użytkownika decyduje warstwa wyżej.

Bo co za róznica, czy sytuacja której się nie spodziewamy powstała w wyniku "właściwie niemożliwej do wystąpienia z przyczyny innej niż spory fakap" czy innej, np niepoprawny encoding, format, albo jakikolwiek inny, duży lub mały błąd?

Taka, że jedno powoduje, że aplikacja nie może (albo nie mogła przez jakiś czas) działać, a drugie nie.

Masz coś na poparcie tezy że rzucanie wyjątków do "małych" błędzików jest niezgodne z ich przeznaczeniem? To element języka jak każdy inny.

No samą nazwę tego elementu języka. :) To się nazywa Exception, a nie LazyHack.

I nie da się im powiedzieć żeby zignorowały jedną klasę wyjątku? To na prawdę nowoczesne.

Da się, tylko to jest dodatkowa praca. I po co, skoro zawsze można wyjątku nie rzucać.

Może się nie rozumiemy. Rozważmy dwa case'y:

  • A. Interfejs funkcji, zwraca liczbę lat ile ktoś ma, lub -1 jeśli nie można ustalić wieku.
  • B. Interfejs funkcji, zwraca liczę lat ile ktoś ma, lub AgeNotFoundException.

Która z tych funkcji jest fajniejsza do użycia i przysporzy mniej bugów?

Yyyy.... no jak pisałem, to jest wiosna 2021 za oknem, nie wiedziałem, że tu o jakieś magiczne numerki chodzi.
Ale jeśli mnie pytasz, czy wolę strzał wyjątkiem w ryj, czy kopa w jaja, to wybieram strzał wyjątkiem.
Tylko to nie znaczy, że strzał w ryj jest miły i nie dało się go uniknąć. ;)

Widać łatwo, że korzystając z A, jest szansa że trafimy na scenariusz 1 i musimy pamiętaj jakiej wartości się spodziewamy, zrobić ifa na niej. Jeśli tego nie zrobimy, albo zrobimy ifa na złą wartość, magic value -1 pójdzie dalej w program i potencjalnie wyświetli się userowi "wiek: -1", oczywisty bug, który może nie zostać znaleziony. Gorzej jeśli ta wartość jest w dalszych operacjach użyta do czegoś innego. Jeśli dalsze operacje nie rzucą wyjątku na niepoprawną wartość, tylko użyją jej w dalszych obliczeniach niepoprawnie, może to bardzo skutecznie schować buga. Bardzo ciężkego do wytropienia.

No tu się w pełni zgadzam, kopniaki w jaja są bardzo bolesne i może być ciężko się pozbierać. Dlatego tak się nie robi i w ogóle się tego nie rozważa nawet.
A wyjątku z kolei możesz nie wychwycić i nie obsłużyć, bo np. możesz nie wiedzieć, że funkcja go zwraca. Potem użytkownik zrobi coś źle, a aplikacja w efekcie się wywali. To nie jest dobre podejście.

TomRiddle napisał(a):

Mam wrażenie że Ty @danek oraz @somekind macie na myśli takie błędy które będą obsłużone prędzej czy później. Ja mówię o wyjątkach które mogą nie być obsłużone nigdy. I dla takich przypadków nie ma zupełnie sensu zmiana interfejsu metody pod to.

Nie trzeba niczego zmieniać, jeśli od początku robi się dobrze.

1

Zwracany wynik powinien byc polimorficzny. Czy to bedzie Either czy Exception czy Result to juz drugorzedne.
Wazne zeby mozna bylo odczytac ten wynik na kilku poziomach szczegolowosci:

  • czy blad czy nie blad
  • jaki jest kod bledu i komunikat
  • jaka jest klasa bledu (np ParseError czy OutOfMemoryError?)
  • jakie sa dane kontekstowe bledu (np dla ParseError indeks znaku)
1

Tylko skąd klasa nisko, ma wiedzieć jakiego rodzaju informację o błędzie zwrócić albo czy w ogóle zwrócić. Klasa nisko nie powinna wiedzieć, czy z punktu widzenia aplikacji taka sytuacja powinna być obsłużona czy nie. Skąd może wiedzeć czy dostała błędy HTML od usera, i powinna mu powiedzieć w formacie user-friendly? Czy błędny html został tam przekazany w wyniku błędu w uploadzie, o czym user nie powinien wiedzieć?

No i dlatego powinna zwracać właśnie jakąs monade typu Option czy Either albo ew. jakieś enum czy saealed class. Jak klient biblioteki stwierdzi że ma co z tym robić to wtedy wtedy obsłuży Option.None albo Either.Left (w sensie zmapuje czy coś) a jak będzie to sytuacja krytyczna to zrobi np

. option.orElseThrow(() -> new IllegalArgumentException("Twój argument jest inwalidą"); );
1
somekind napisał(a):
TomRiddle napisał(a):

Trochę jakby zamykać oczy i krzyczeć: "mam tylko jedną warstwę w swojej aplikacji". Wtedy faktycznie ma się jedną perspektywę.

Efektem mojej pracy ma być prawidłowo działająca aplikacja, o jej prawidłowym działaniu upewniam patrząc w różne metryki, m.in. liczbę rzucanych wyjątków. I nie chciałbym, żeby jakiś DatabaseConnectionException umknął w gąszczu UserAgeCanNotBeNegativeException ani też mieć setek tysięcy wyjątków w logach, podczas gdy w rzeczywistości nie było żadnego.
Pracowałem przy systemach robionych w taki sposób, nie było to wygodne.

To zależy od prezentacji tych logów w narzędziu do monitoringu które wybrałeś. Jeśli korzystasz ze splunka albo czegoś podobnego to możesz je pogrupować, tak że nawet jeśli wyjątek NotSoImportantException poleci 1000x to zobaczysz go i tak, zgrupowanego w zakresie którym wybrałeś.

Także argument z "gąszczem" i "umykaniem" do mnie nie trafia, bo to kwestia umiejętności korzystania z narzędzi diagnostycznych.

Tylko skąd klasa nisko, ma wiedzieć jakiego rodzaju informację o błędzie zwrócić albo czy w ogóle zwrócić. Klasa nisko nie powinna wiedzieć, czy z punktu widzenia aplikacji taka sytuacja powinna być obsłużona czy nie.

Klasa zwraca wynik: prawidłowy bądź błędny. O tym, czy informację o błędzie ignorujemy i np. powtarzamy próbę danej operacji, czy informujemy użytkownika decyduje warstwa wyżej.

No zgadzam się. Ale czemu nie wyjątkiem, tylko return-typem?

Bo co za róznica, czy sytuacja której się nie spodziewamy powstała w wyniku "właściwie niemożliwej do wystąpienia z przyczyny innej niż spory fakap" czy innej, np niepoprawny encoding, format, albo jakikolwiek inny, duży lub mały błąd?

Taka, że jedno powoduje, że aplikacja nie może (albo nie mogła przez jakiś czas) działać, a drugie nie.

Efekt i tak jest ten sam.

Nie ważne czy cała aplikacja leży, czy tylko jakiś jej pod element (np jedna funkcja), i nie ważne czy używasz wyjątków czy PageResult return-typów, efekt działania jest taki sam. Tzn, w przypadku "dużego" fakapu, cała aplikacja leży; nie ważne czy przez wyjątek czy przez return-type; w przypadku pojedynczej funkcjonalności, mały fuckup, ta jedna funkcja nie działa, nie ważne czy przez wyjątek czy przez return-type.

Innymi słowy; co za różnica jakiego elementu języka użyjesz, skoro zachowanie będzie takie samo. Trochę jak podobne użycie if i switch.

Popatrz, i wyjątki i return type możesz zalogować. I wyjątki i return-type mogą przejść przez warstwy. Czemu miałbym nie rzucić wyjątku, kiedy klasa w 10tej warstwie dostanie niepoprawnego HTMLa?

Masz coś na poparcie tezy że rzucanie wyjątków do "małych" błędzików jest niezgodne z ich przeznaczeniem? To element języka jak każdy inny.

No samą nazwę tego elementu języka. :) To się nazywa Exception, a nie LazyHack.

True, ale nie nazywa się też AppCrashException, żeby zajmować stanowisko że exceptiony mogą lecieć tylko wtedy kiedy apka ma się wywalić.

I nie da się im powiedzieć żeby zignorowały jedną klasę wyjątku? To na prawdę nowoczesne.

Da się, tylko to jest dodatkowa praca. I po co, skoro zawsze można wyjątku nie rzucać.

Po to, żeby nie stracić wszystkich zalet które oferują wyjątki, które taki PageResult w return type'ie nie oferuje.

Może się nie rozumiemy. Rozważmy dwa case'y:

  • A. Interfejs funkcji, zwraca liczbę lat ile ktoś ma, lub -1 jeśli nie można ustalić wieku.
  • B. Interfejs funkcji, zwraca liczę lat ile ktoś ma, lub AgeNotFoundException.

Która z tych funkcji jest fajniejsza do użycia i przysporzy mniej bugów?

Yyyy.... no jak pisałem, to jest wiosna 2021 za oknem, nie wiedziałem, że tu o jakieś magiczne numerki chodzi.
Ale jeśli mnie pytasz, czy wolę strzał wyjątkiem w ryj, czy kopa w jaja, to wybieram strzał wyjątkiem.
Tylko to nie znaczy, że strzał w ryj jest miły i nie dało się go uniknąć. ;)

No okej, przykład z -1 średni, mea culpa

Widać łatwo, że korzystając z A, jest szansa że trafimy na scenariusz 1 i musimy pamiętaj jakiej wartości się spodziewamy, zrobić ifa na niej. Jeśli tego nie zrobimy, albo zrobimy ifa na złą wartość, magic value -1 pójdzie dalej w program i potencjalnie wyświetli się userowi "wiek: -1", oczywisty bug, który może nie zostać znaleziony. Gorzej jeśli ta wartość jest w dalszych operacjach użyta do czegoś innego. Jeśli dalsze operacje nie rzucą wyjątku na niepoprawną wartość, tylko użyją jej w dalszych obliczeniach niepoprawnie, może to bardzo skutecznie schować buga. Bardzo ciężkego do wytropienia.

No tu się w pełni zgadzam, kopniaki w jaja są bardzo bolesne i może być ciężko się pozbierać. Dlatego tak się nie robi i w ogóle się tego nie rozważa nawet.
A wyjątku z kolei możesz nie wychwycić i nie obsłużyć, bo np. możesz nie wiedzieć, że funkcja go zwraca. Potem użytkownik zrobi coś źle, a aplikacja w efekcie się wywali. To nie jest dobre podejście.

Im dłużej czytam Twoje wypowiedzi, tym coraz większe wrażenie mam, że kiedy wypowiadasz się użyciu tych wyjątków, to mówisz tylko o aplikacjach end-userów, które są bardzo wysokopoziomowe, korzystają z wysokopoziomowych frameworków, i mają dosyć mało logiki ale dużo scenariuszy biznesowych (jak np panele administracyjne jakichś serwisów). Czy to dobre wrażenie?

Ponieważ, faktycznie, kiedy piszesz takie aplikacje to jest bardzo niewiele przykładów rzeczy o których mówię; i wiele przykładów rzeczy o których Ty mówisz. Zgadzam się że w takich aplikacjach podejście "błędny wynik" to jest "dobry return type", a wyjątek == błąd, i patrzenie na to wszystko jednowarstwowo ma jakiś sens. Być może taka abstrakcja pomaga znacznie w diagnostyce. I mam wrażenie że mówisz o back-endzie tych aplikacji. Jeśli dodatkowo to są mikroserwisy, który każdy z nich jest otulony narzędziami do diagnostyki jak AppDynamics czy inne, to faktycznie to może być pomocne, bo nie musisz już myśleć o warstwach aplikacji, to o aplikacji jako o jednym bycie. Jeśli tak patrzysz na aplikacje, to faktycznie wyjątki "małe" mają mniej sensu (i tym mniej sensu mają im mniejsza jest aplikacja). I get that. Dodatkowo, jeśli patrzysz na pajęczynę aplikacji, to interesuje Cię tylko to czy stoją czy nie stoją, albo czy mogą np przeprowadzać płatności, etc. więc duże fakapy, i on nich chcesz wiedzieć. Nie chcesz wiedzieć o sfailowanych encodingach, więc nie chcesz ich logować, ale narzędzia z których korzystasz traktują wyjątki jak błędy i je logują; więc nie rzucasz wyjątków, więc musiałeś znależć inny sposób propagacji błędnych wartości więc przekazujesz PageResult. Domyślam się że taki kompromis jest dla Ciebie akceptowalny, bo (domyślam się) że sporadyczny bug (np. błędne wystąpienie ujemnego wieku, albo innego "niepozornego buga" albo "małego błędu") jest niską ceną, za to żeby nie ryzykować położenia aplikacji i wytworzenia sobie takiej prostej struktury. It all makes sense to me. Jeśli masz high-level aplikację blisko usera, albo blisko jakiegoś monitoringu to takie podejście ma dużo zalet.

Ale ja mówię o wytwarzaniu aplikacji w ogóle; nie tylko o wysokopoziomowych aplikacjach end-userów wspieranych przez bardzo dojrzałe frameworki; bo rozumiem że rynek programistów w dzisiejszych czasach jest bardzo silnie przesunięty na webówkę wszelakopojętą; ale ja mówię i o innych rozwiązaniach, bibliotekach, toolach, modułach, kompilatorach, aplikacjach desktopowych lub mobilnych, grach, nie wiem, kodekach, aplikacji multimedialnych, maszynach wirtualnych, pluginach, sterownikach, dll'kach, class loaderach, aplikacjach które uczą AI, o programowaniu w ogóle. O wszystkich rozwiązaniach, niekoniecznie blisko usera.

Szczerze mówiąc, nie sądzę że takie podejście sprawdzi się w tych przypadkach, wydaje mi się że wyjątki i ich elastyczność sprawdzą się dużo lepiej.

Dodatkowo, Ty kiedy mówisz "błąd" (popraw mnie jeśli się mylę, to jest tylko tak jak odbieram Twoją wypowiedź), mam wrażenie że rozróżniasz dwa typy błędów:

  • crasher (jak nie możność połączenia się z db), to mam wrażenie że uważasz że takie zasługują na wyjątki
  • błąd do ohandlowania, co można ogarnąć poprzez te PageResult typy; i tu mam wrażenie że mówisz że one nie zasługują na wyjątki.

To jest extension tego podejścia high-level'owych aplikacji.

Nie widzę w Twoich wypowiedziach zupełnie miejsca na wyjątki które nie są do ohandlowania, a to jest dokładnie to o czym mówię od 5 odpowiedzi.

Przykładem tego o czym jak mówię, jest np taki kod new String("\xc3\x28", "UTF-8") ("\xc3\x28" to jest niepoprawny unicode). Ja sobie nie wyobrażam żeby taki kod skończył się czymkolwiek innym niż wyjątkiem. Correct me if I'm wrong?

Zróbmy eksperyment, może nie mam racji zupełnie. Powiedzmy, że mamy funkcje która ma dostać listę stringów, i ma z nich skleić ścieżkę. Np z join("folder/", "/b/", "c", "", "d"). Jakbyś zaprojektował taką funkcje, jakie miałaby return type'y, czy rzucałaby kiedykolwiek jakikolwiek exception, i jakie case'y byś przewidział? Zakładamy że Ty ją musisz napisać i nie jest dostarczona przez framework.

Dodatkowo, dodatkowo. Jak głęboko takie PageResult mogą sięgnąć? Czy to znaczy że idealnie napisana aplikacja nie ma return type'ów takich jak int lub String, to każda funkcja zwraca PageResult który zawiera albo wynik albo każdy błąd z klasy?

5

@TomRiddle:

new String("\xc3\x28", "UTF-8") 

Ja sobie nie wyobrażam żeby taki kod skończył się czymkolwiek innym niż wyjątkiem. Correct me if I'm wrong?

To tylko znaczy, że słaby design. Kod nie powinien się kompilować, a dokładniej - w takim razie String nie powinien mieć takiego konstruktora.

Generalnie konstuktory to słabe miejsce do dyskusji o api i wyjątkach. W większości języków konstuktory są dość ograniczone jako funkcje.
I dlatego często się ich nie wystawia w dobrym api.

Co więcej dyskusja na poziomie Stringów czy Intów w kontekście tematu też jest bez sensu - ze względów wydajnościowych operacje niskopoziomowe często mają słabe, nieidiotoodporne api. Co nie znaczy, że wchodząc na poziom lekko wyższy nadal musimy się takich koncepcji trzymać.

Dodatkowo, dodatkowo. Jak głęboko takie PageResult mogą sięgnąć? Czy to znaczy że idealnie napisana aplikacja nie ma return type'ów takich jak int lub String, to każda funkcja zwraca PageResult który zawiera albo wynik albo każdy błąd z klasy?

W dobrze zaprojektowanym api nie tylko często nie zwracamy int czy String, ale i nie przyjmujemy takich typów jako argumenty.
Nie każdy int to dobry kandydat na miesiąc, czy wiek, nie każdy String to poprawna ścieżka do pliku.

Przy okazji - dobrze zaprojektowane api to dla mnie api, które wymaga ode mnie jak najmniej czytania dokumentacji. Po to jest system typów, żeby prowadził za rączkę. Jak się nie skompiluje, to znaczy, że źle użyłem.

4

Wyjątki są fajnym mechanizmem na papierze, ale niestety widzę, że w praktyce w dużych projektach wyjątki służą głównie do dziurawienia abstrakcji. Chodzi o to że często bardzo niskopoziomowy wyjątek potrafi przelecieć kilka warstw wyżej, gdzie w sumie nie ma dużego sensu. Np. wołasz jakiś serwis i dostajesz File not found zamiast "serwis X niepoprawnie skonfigurowany" a file not found zagnieżdżone dopiero jako przyczyna / szczegół. Po prostu programistom nie chce opakowywać.

Natomiast mechanizm zwracania błędów przez return takie opakowanie wymusza, bo bardzo szybko okazuje się że w tej wyższej warstwie potrzebujemy jednego typu na błędy związane z tą warstwa.

3

Jeśli wejdziesz głębiej, możesz zobaczyć że to że 2 drugi to też jest stytuacja wyjątkowa, ale nie z punktu widzenia usera; tylko klasy/programisty

@TomRiddle:
No to jest kwestia interpretacji, co oznacza "sytuacja wyjątkowa".
Nie podlega dyskusji, że "typowy" wyjątek czyli chociażby dzielenie przez zero jest sytuacją wyjątkową. Ale czy na pewno dostarczenie pliku w innym formacie, który dodatkowo i tak może zostać przetworzony, tylko inną ścieżką, jest sytuacją wyjątkową, czy po prostu innym sposobem obsłużenia danej sytuacji? Skoro liczysz się z tym, że jeśli poda się plik html do konwersji to on i tak ma zostać przerobiony, to taka sytuacja nie jest wyjątkiem (czyli w moim rozumieniu - czymś nagłym, nieprzewidzianym) tylko jedną z zaprojektowanych ścieżek postępowania.

gdyby się okazało że te dane nie są od usera, tylko są wynikiem jebnięcia się programisty to możesz przemilczeć buga. Buga o którym byś się dowiedział, gdyby klasa rzuciła wyjątek.

Ale przecież to i tak jest decyzja po stronie programisty, co zrobi w chwili wyjątku.Równie dobrze można zapisać informację do jakiegoś loga błędów, a potem udawać że wszystko jest OK, albo poinformować usera że coś się nie udało, ale nie trzeba mu pchać gołego exceptiona na front. Korzystanie z wyjątku w przypadku błędu po stronie użytkownika nie wyklucza "przyjaznego" potraktowania sprawy.

Bo niektórych błędów nie powinno się obsłużyć, niektóre powinny pozostać nieobsłużone. I to są sytuacje w których dane pochodzą od programisty/nie od usera.

Czyli co proponujesz? Zamiast jakiejś sytuacji awaryjnej, aplikacja ma się wywalić? Nie rozumiem, o co Ci w tym miejscu chodziło.

Tylko teraz powiedz mi na pytanie :D Mimo pośrednika, co się powinno stać kiedy PdfToHtmlConverter dostanie *.html?

OK, zgadza się. Ale ja to inaczej widzę.
Ty proponujesz (a przynajmniej tak to zrozumiałem) że przekazujemy do convertera jakiś plik, a jeśli jego format nie pasuje, to w normalnym trybie zwracamy wyjątek i działamy dalej, testujemy kolejne ścieżki.
Do mnie to nie trafia.
Ja bym raczej widział to tak, jak napisałem (i jak mi się wydaje - podobnie uważają m.in. @Miang czy @axelbest) - mamy pośrednika, który weryfikuje typ pliku i na tej podstawie przekazuje jego obsługę dalej. Jeśli stwierdzi, że plik jest w formacie PDF, to przekazuje go do handlera odpowiedniego dla tego typu plików.
I teraz mamy dwie sytuacje:

  1. plik można obsłużyć, wtedy wszystko idzie OK
  2. pliku nie można obsłużyć - wtedy wyjątek jest jak najbardziej OK.

Ale widzisz różnicę? Nie steruję przebiegiem programu w "normalnej" sytuacji poprzez wyjątki, tylko mam od tego "zwykłą" logikę. ALE jeśli konwerter dostanie plik niezgodny z tym, czego oczekiwał, to mamy idealny przykład sytuacji wyjątkowej - mimo, że wcześniej było jakieś sprawdzenie, to coś poszło nietak i konwerter dostał dane niezgodne z tym, co powinien otrzymać.
Podsumowując - normalny przebieg pracy robimy w oparciu o "zwykłą" logikę, a wyjątki rzucamy gdy mamy problem.
Zgodnie z moją klauzulą sumienia, jeśli mamy 2 typy plików i dla każdego z nich osobną ścieżkę obsłużenia, to nie ma miejsca na wyjątek, tylko na jakiegoś if. Wyjątek rzucamy, jeśli mimo tej normalnej ściezki, dostaniemy niepoprawne dane wejściowe.

moim zdaniem ten error_get_last() to wgl słaby pomysł z wielu powodów

Też tego nie lubię, ale dałem jako przykład, że nawet sytuacje nieprzewidziane/krytyczne da się ogarnąć bez wyjątków. Przy czym - jak pisałem kilka zdań wcześniej, uważam że wyjątek jest OK - ale w sytuacjach nieplanowanych, a nie jako sterowanie standardowym przebiegiem wykonania aplikacji. Za to raczej powinna odpowiadać zwykła logika, a nie rzucanie wyjątku jako forma udzielenia odpowiedzi.

Ale z perspektywy parsera HTMLa, lub nawet managera plików to jest sytuacja wyjątkowa.

Zgadza się, przy czym o ile Ty mi zarzucasz, że patrzę zbyt globalnie, to mam wrażenie, że Ty przeginasz w drugą stronę :P
Tak, parser jak dostanie nieprawidłowe dane, to powinien rzucić wyjątkiem. Tutaj się zgadzam.
Tylko nie podoba mi się Twój sposób stworzenia logiki - w stylu że daję dane do parsera, a jeśli on mi rzuci wyjątek, to wtedy przetwarzam dane w inny sposób.
Logika powinna być tak zrobiona, że dane trafiają tam, gdzie powinny i w odpowiedniej formie. Jeśli mam kilka ścieżek wykonania, to wybieram odpowiednią i mam być pewny, że jeśli przekazałem do elementu przetwarzającego PDF coś, to to coś jest PDF'em, który się da przetworzyć. A co za tym idzie - jeśli konwerter rzuci wyjątkiem to nie zastanawiam się, gdzie jeszcze mogę przekazać te dane (bo widać nie było to PDF tylko HTML albo DOC), tylko mam informację, że mam gdzieś skopaną logikę i dane są źle przekazywane. W takiej sytuacji nie odpalam kolejnych ścieżek, tylko widzę, że jest problem i szukam rozwiązania, czyli gdzie popełniłem błąd.

Tylko znowu pytanie, z czyjej perspektywy wyjątkowa. Z punktu widzenia aplikacji, raczej nie. Z punktu widzenia jednostki niżej/ warstwy niżej, już tak.

Ponowne - z punktu widzenia convertera to otrzymanie błędnych danych wejściowych jest wyjątkiem. Z tym nie mam problemu. Ale nie podoba mi się logika, w której po otrzymaniu takiego wyjątku Ty dalej szukasz sposobu, w jaki te dane można przemielić. Skoro konwerter PDF dostał coś, co się nie nadaje to nie szukajmy innego sposobu konwersji - bo na to był czas wcześniej, tylko przyjmijmy wyjątek na klatę i stwierdźmy, że się nam coś powaliło.

Widać łatwo, że korzystając z A, jest szansa że trafimy na scenariusz 1 i musimy pamiętaj jakiej wartości się spodziewamy, zrobić ifa na niej. Jeśli tego ni

Ale z drugiej strony są sposoby (aczkolwiek trochę starożytne) żeby tą wartość -1 oznaczającą bład, opakować jakąś stałą.
A potem można zrobić coś w stylu if (result < OK_VALUE) i dalej kilkoma if'ami odczytać (jeśli będzie taka potrzeba) jakiego typu jest ten konkretny błąd.

Skoro może nie wiedzieć że wyjątek może wystąpić; to może też nie wiedzieć że niepoprawna wartość może być zwrócona. W obu przypadkach jest to błąd wynikły z niewiedzy programistów

Tutaj się w pełni zgadzam - jakiekolwiek obsłużenie błędu/nietypowej sytuacji, wymaga działania ze strony programisty. Jedynie można się zastanawiać, co jest gorsze - czy dalej puszczenie w świat wartości błędu jako czegoś poprawnego, czy zostawienie wyjątku bez obsługi. To drugie jest trudniej przeoczyć, bo albo coś wyskoczy na ekranie, albo aplikacja się wychrzani, przez co nie ma ryzyka, że błędne dane pójdą dalej w świat i narobią zamieszania.

Klasa dzieląca liczby zakłada, że w momencie dzielenia dane są poprawne, i dzielnik nie jest zerem. Klasa konwertująca HTML na PDF zakłada, że dostała prawidłowy plik. Jeśli tak się nie dzieje, to taka klasa niby może zwrócić wyjątek, ale może też zwyczajnie zwrócić informację o błędzie

@somekind: a w jaki sposób Ty byś chciał zwrócić tą informację? @TomRiddle chwilę wcześniej napisał (https://4programmers.net/Foru[...]ugi_bledu?p=1749218#id1749218) że takie zwracanie błędu jest niefajne i (szczerze mówiąc) ma w tym dużo racji. I z jednej strony rozumiem i się zgadzam z tym, co On napisał, ale z drugiej to nie czaję/nie przemawia do mnie sterowanie przebiegiem programu w oparciu o wyjątki (poza sytuacjami naprawdę awaryjnymi).
Czyli mamy 3 opcje: jakaś klasę/obiekt, w której będą zaszyte informacje o błędzie, magic value oraz wyjątek. Czy masz pomysł na coś innego? Przypominam, że ten wątek jest raczej taki ogólny, więc nie idźmy w kierunku rozwiązań typowych jedynie dla niektórych języków.

1 użytkowników online, w tym zalogowanych: 0, gości: 1, botów: 0