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ł (Rzucanie wyjątkami a "zwykłe" zaprojektowanie obsługi błędu) ż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.

2

Ja na to patrzę w taki sposób, że są dwie metody obsługi błędów:

  • **jawna **(Either, Result, (res, err)) - optymalizacja bezpieczeństwa -> większa szansa, że błąd zostanie obsłużony -> utrzymanie prostego (lokalnego) data flow i control flow -> łatwiejsza interpretacja kodu w kontekście błędów -> w efekcie podobnie do Task przy asyncu w krótkim czasie większość kodu będzie w monadzie Either
  • **niejawna **(Exception, errno) - optymalizacja złożoności kodu -> mniejszy koszt pisania nowego kodu, bo nie trzeba obsługiwać każdego błędu -> zwiększenie skomplikowania data flow i control flow -> trudniejsza interpretacja kodu w kontekście błędów -> w efekcie prędzej czy później ktoś zapomni obsłużyć jakiś wyjątek i będzie bug

I tutaj takie checked exceptions to jest hybryda, która z jednej strony jest jawna bo błędy są w sygnaturze funkcji, ale komplikuje data/control flow, więc ani się szybciej nie klepie ani kod nie jest łatwiejszy w interpretacji.

Teraz jak wybierzecie jawną metodę, to możecie się zacząć zastanawiać czy to czy jest połączenie z bazą nie powinno jednak rzucać wyjątku, jednak koszt jaki ponieśliście żeby całą resztę obsługiwać jawnie sprawi, że ewentualne jawne obsługiwanie nieobecności zasobów nie będzie jakimś dużym kosztem (bo i tak już wszystko jest opakowane w Either) i zaczyna się naturalnie zbiegać do tego, żeby wszystko było explicit i pure.

Jak wybierzecie niejawną obsługę, to wciąż będą fragmenty kodu, gdzie błąd musi być obsłużony blisko jego powstania, więc będziecie chcieli pokryć te fragmenty jednak jawnie, jeśli jednak kod jest odpowiednio złożony, to pewnie w tych miejscach wołacie jakieś funkcje które mogą rzucać wyjątkami, więc i tak ten jawnie obsługiwany fragment nie będzie pure.

A w rzeczywistości i tak dostaniemy zgniły kompromis, bo obsługa błędów w zależnościach wycieka do naszego kodu a nikomu się nie chce pisać adapterów dla całego API dużych bibliotek.

3

Z checked exceptions jest jeszcze dodatkowo taki problem, że przynajmniej w przypadku implementacji w Javie, one się słabo komponują z lambdami / obiektami anonimowymi. Nie da się np. zrobić metody map, której sygnatura mówi: ta metoda rzuca dokładnie takie same wyjątki jak funkcja, którą przyjmuje za swój argument. I efekty są takie, że w wielu miejscach trzeba opakowywać checked w unchecked i robi się bałagan.

2

Nie podlega dyskusji, że "typowy" wyjątek czyli chociażby dzielenie przez zero jest sytuacją wyjątkową.

W zasadzie to nie ma czegoś takiego jak dzielenie przez 0.
Operacja dzielenia nie jest zdefiniowana dla dzielnika 0.
Co też oznacza, że funkcja postaci:
Int (div) Int -> Int
nie jest na pewno dzieleniem.
Tylko jakimś własnym wytworem wyobraźni informatyków. Luźno opartym o algebrę.

Dzielenie powinno być zdefiniowane jako:
Int (div) Non0Int -> Int
gdzie dzielnik (typu: Non0Int) nie może być zerem, a kompilator nie powinien kompilowac kodu, w którym jest możliwe, że dzielnik będzie innego typu (Int).
Oczywiście, takie rzeczy to tylko w językach akademickich :-) ale tak powinno być. (ewentualnie w językach praktycznych, ale z akademickimi rozszerzeniami).

0

W zasadzie to nie ma czegoś takiego jak dzielenie przez 0. Operacja dzielenia nie jest zdefiniowana dla dzielnika 0.

Teraz to jest czepianie się słówek, doskonale wiesz o co chodzi :P
Poza tym jak wykonujesz jakieś obliczenia to działasz na zmiennych i nie jesteś w stanie na etapie kompilacji przewidzieć tego, co user wpisze w polu "liczba rat" albo "wzrost". Co najwyżej, możesz to później zwalidować na etapie wykonania. Ale zawsze jest ryzyko, że gdzieś będzie jakiś błąd i do dzielenia pójdzie dzielnik o wartości zero.
Zresztą to jest tylko przykład sytuacji, w której uzasadnione jest rzucenie wyjątkiem (tak samo jak np. niemożność podpięcia się do bazy podczas wykonywania zapytania). Nie róbmy offtopu, OK?

2

To nie było czepianie się słówek, ani offtop, tylko podane rozwiązanie problemu - niejako trzecia droga. Lepsza matematycznie.
Poza tym to nieprawda, że kompilator nie jest w stanie tu pomóc.
Nie jest w stanie przewidzieć co napisze user, ale możemy mieć takie funkcje

val input:Int = someIntFromUser
val result = input.onNonZero(x:Non0Int -> (5 / x).toString()).orElse("I hate you.")

W tym przypadku object typu Non0Int możemy stworzyć tylko jeśli zrobiliśmy sprawdzenie czy nie 0 na podanym Int.
Co więcej mamy operacje typu:
Non0Int * Non0Int -> Non0Int, ale
Int * Non0Int -> Int
i kompilator maże całkiem sprawnie sam zgadywać czy w danym miejscu będzie Int czy Non0Int.

1

input.onNonZero(x:Non0Int -> (5 / x).toString()).orElse("I hate you.")

Ale czym się to różni od innych sytuacji - czyli sprawdzenia czy mamy zero i wtedy nie wykonania liczenia, a jedynie zwrócenia "Aj hejt ju", albo w przypadku wartości niezerowej - wykonania obliczenia?
Ewentualnie w czym taki zapis jest lepszy od rzucenia wyjątku?
W przypadku wyjątku masz przynajmniej informację, ze jakaś nieprzewidziana sytuacja wystąpiła, a tak jak napisałeś to i tak dostaje stringa, którego potem sobie muszę sam sprawdzić, inaczej nie dostanę informacji o tym, czy dzielenie się udało. Po prostu - w postaci .orElse dostaję coś w odpowiedzi, ale pchanie tego dalej bez weryfikacji, czy mam stringa z zapisem w postaci tekstowej numerka, czy jakiś komunikat jest bardzo ryzykowne. A jak tego nie sprawdzę tylko pchnę dalej, to w innym miejscu wyskoczy kwiatek w stylu "Średnia wartość zamowienia: I hate you", a to raczej jest bardzo niepożądane.

3

z tym dzieleniem przez 0 i innymi to tracimy ogólność dyskusji, co jeśli w jakimś języku NaN jest wartością która coś mówi i dzielenie przez 0 ma sens ;) a @cerrato chyba miał na mysli że niekoniecznie musimy jawnie to dzielnie zleci, tylko np. dzielimy przez (x-y) a że akurat x i y po wyliczeniach na początku funkcji będą sobie równe to tak samo wyszło

2

@Miang: Tak, a poza tym nie chodzi o to głupie dzielenie. To był jedynie jakiś przykład sytuacji, w której dla mnie zasadne jest rzucenie wyjątku. Ale np. sytuacja, w której da się coś obsłużyć "zwyczajną logiką aplikacji" nie koniecznie się do wyjątku nadaje.

1

@cerrato:
Ech, ewidentnie nie piszemy w tym samym języku.
Nikt normalny nie będzie sprawdzał stringa (z I hate you)- tu było pokazane jak to zakończyć i np. wyrzucić na ekran.
Normalnie siedzisz w tym onNonZero (tam piszesz kod, nawet cały program) tak długo jak trzeba - bez rzucania wyjątkiem.

@Miang

Oczywiście odejmowanie i dodawanie można zdefiniować jako:
Non0Int - Non0Int -> Int

Wtedy po każdej takiej operacji wiemy, że potencjalnie jest problem i wtedy trzeba uciec się do nowego sprawdzenia onNonZero. Po prawdzie doszliśmy do miejsca gdzie trzeba zacząć opowiadać o **monadach **i pokazać, że jeśli to onNonZero wyprodukuje monadę to kod będzie prawie tak samo czytelny jak obliczenia wprost, a jednocześnie będziemy mieli bezpieczeństwo dzielenia zapewnione przez kompilator.

2

Dla mnie sprawa jest prosta:
int/int -> Option[int]

1
cerrato napisał(a):

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".

Chyba zbliżamy się do porozumienia. Nie sądzę że to kwestia interpretacji, tylko bardziej spojrzenia na sytuację z różnych poziomów abstrakcji. Z wysokiego poziomu - nie jest, z niskiego - jest.

Nie podlega dyskusji, że "typowy" wyjątek czyli chociażby dzielenie przez zero jest sytuacją wyjątkową.

To prawda. Taki wyjątek ma sens, nikt go nie łapie; nikt się nie dziwi że wystąpił; wszyscy po prostu do niego nie dopuszczają. Takie sytuacje jak z dzieleniem przez zero można mnożyć. Np przekazanie niepoprawnego regexpa do Pattern.compile() powinno skutkować wyjątkiem, albo próba otwarcia nieistniejącego pliku, lub wyciągnięcie czegoś z pustego arraya/listy.

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.

Odpowiem dwojako.

Myślisz o całej tej sytuacji z globalnego punktu widzenia, z punktu widzenia usera. Tak, plik tak czy tak ma być obsłużony; więc userowi żaden błąd i żaden wyjątek się nie pokaże. Dla użytkownika aplikacji to nie jest sytuacja wyjątkowa. Ale klasa PdfToHtmlConverter spodziewa się PDF'a. Nie powinna dostać htmla, a jeśli go dostanie powinna rzucić wyjątek. Klasy niższe nie powinny polegać na założeniach klas wyżej. Wyobraź sobie pięć aplikacji. Nazwijmy ją Apple, Orange, Pear, Cherry i Banana.

  • Aplikacja Apple działa tak jak opisaliśmy, jeśli dostanie pdf'a to konwertuje go na html'a, a jeśli coś innego, po prostu go zwraca.
  • Aplikacja Orange działa tak, ze jak dostanie pdf'a to go konwertuje na html'a, a jak dostanie coś innego, np html'a właśnie, to go wsadza w iframe'a i dopiero potem wsadza w html'a
  • Aplikacja Pear działa tak, że jak dostanie pdf'a to go konwertuje na html'a, a jak dostanie coś innego, to robi screenshota i tego screenshota wkłada w html'a
  • Aplikacja Cherry to mikroserwis który może dostać tylko pdf'a i nie może dostać nigdy html'a, jeśli dostanie to odpowiada statusem 400 bad request.
  • Aplikacja Banana crawluje internet w poszukiwaniu pdf'ów, znajduje pliki które faktycznie są pdf'em i zamienia je na HTML przy pomocy PdfToHtmlConverter.

Moim zdaniem, klasa PdfToHtmlConverter w każdej z tych aplikacji powinna wyglądać dokładnie tak samo. Tzn, gdyby faktycznie te aplikacje istniały, to klasę PdfToHtmlConverter można by wynieść do biblioteki fruit-commons i jej użyć tak samo w każdej z nich. Pytanie pada, jak jej użyć tak samo, skoro każda aplikacja wymaga innej reakcji na użycie pliku który nie jest pdf'em? Apple go zwraca tak jak wszedł; Orange umiesza go w iframie przed zwróceniem, peer robi screenshota, cherry odpowiada błędem http, a w banana taka sytuacja wgl nie powinna mieć miejsca (i jeśli miała to na 100% mamy buga w szukaniu pdf'ów).

Jedyny sensowny sposób to jest taki, że klasa rzuca wyjątek, który mówi: "Jestem PDF' konwerterem a nie dostałem PDF'a", i dopiero warstwa wyżej która użyła tej klasy wie co wtedy zrobić. Bo to aplikacja ma decydować co zrobić z nie-pdf'em, a nie klasa która ma go skonwertować. Oczywiście mógłbyś zrobić 5 różnych klas, z których każda obsługuje ten sam błąd w różny sposób, ale po pierwsze to by złamało SRP; a po drugie nie czujesz że to w gruncie rzeczy byłby zły design? Nie czujesz że konwertowanie Pdf'a na HTML w każdej z tej aplikacji działa dokładnie tak samo, i powinno być handlowane przez tą samą klasę, i tylko wyjątek od tej sytuacji (czyli przekazany html albo doc) powinien być obsłużony inaczej?

Dodam może, że to że klasa rzuca wyjątek, kiedy dostanie html'a, nie znaczy że kod musi wyglądać tak:

try {
  return pdfConverter.convert(fileFromUser);
} catch (NotPdfException exception) {
  return fileFromUser; // handling
}

Nie musi.

Może wyglądać też tak:

if (fileFromUser.isPdf()) { // nie dopuść do użycia nie-pdf'a
  return pdfConverter.convert(fileFromUser); // rzuć wyjątek jeśli dostaniesz nie pdf'a,
}
return fileFromUser; // handling

Dla mnie dwa kody wyżej są tożsame, w zupełności i mogą być używane zamiennie. Jeśli wolisz wersję z ifem, to spoko. Byleby tylko ten kod nie wyglądał tak:

convert(fileFromUser); // jeśli pdf to convertuj, a jeśli html to zwróć - to jest zło.

Ale pdfConverter.convert() musi rzucić wyjątek jeśli nie dostanie PDF'a.

W przykładzie aplikacji Cherry to mogłoby być (albo if, albo try, wszystko jedno).

if (fileFromUser.isPdf()) { // nie dopuść do użycia nie-pdf'a
  return response(200, pdfConverter.convert(fileFromUser)); // rzuć wyjątek jeśli dostaniesz nie pdf'a,
}
return response(400); // http bad request - handling

A w przykładzie Banana, ten kod może wyglądać tak:

pdfConverter.convert(fileFromUser); // rzuć wyjątek, jeśli nie-pdf

..., pod warunkiem że pdfConverter rzuci wyjątek jeśli dostanie nie-pdf'a, bo to i tak ma się nie zdarzyć nigdy. Banana szuka tylko pdf'ów.

I ta aplikacja Banana to jest to, co próbuję dowieść. W aplikacji banana nie chcę nigdy użyć html'a, nie chcę pisać ifa o pdf'ach i html'ach, bo tam mają być tylko pdf'y, jeśli wsadzę coś innego to aplikacja ma się wywalić, bo na 100% coś zwaliłem. Ale wiem to ja, programista który patrzy z warstwy wyjżej. Klasa PdfConverter tego nie wie. Zadaniem klasy PdfConverter jest tylko dać mi znać, że nie dostała pdf'a, a to ja mam zdecydować co później z nim zrobić. Jeśli nic z tym nie zrobię, bo np nie wiem albo nie zauważę, to język daje mi znać bardziej dobitnie, wyłączając mi aplikacje.

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.

Tak i nie.

Czasem są wyjątki, które nie powinny się zdarzyć. Posłużę się już skrajnym przypadkiem, ale nie mogę znaleźć innych. Przykładem takiego wyjątku jest NullPointerException (czy NullReferenceException w c#). Czy kiedykolwiek ktokolwiek przy zdrowych zmysłach, zrobiłby try {} catch (NullReferenceException ex)? Oczywiście nie. Dlaczego? Dlatego że w tym przypadku (i w przypadku NPE w żadnym przypadku) nie ma sensu obsługa tego wyjątku; bo do niego w ogóle nie powinno dojść. To jest oczywisty sygnał, że gdzieś indziej w aplikacji mamy buga. Najpewniej w warstwie wyżej która chciała coś zrobić na nullu, mimo że nie powinna. Innym przykładem takiego wyjątku jest DivisionByZeroException albo StackOverflowException. Nigdy nie ma sensu wołanie metody na nullu. Ale czy to powstrzymuje userów przed używaniem nulli? Nie, wszędzie zobczysz if (x == null) albo if (x != null). Idąc Twoim tropem, Twoim pomysłem, środowisko nie powinno rzucić NPE jeśli użyjesz metody na nullu, tylko zrobić to co aplikacja ma robić, czyli np null.toString() powinien zwrócić "". No bo skoro user nie wie nic o nullu, to dla niego to nie jest wyjątek prawda? :D :D Oczywiście żartuje, takie przemilczanie nulli prowadziłoby do jeszcze większej ilości błędów, bo miałbyś czasem gdzieś "" i nie wiedziałbyś skąd się tam wziął. Dostając wyjątek który Ci rozwala aplikacje NullPointerException wiesz jakiej natury to był błąd. Dodam może że metoda toString(), zawołana na obiekcie klasy nie wiem Fruit, ta metoda toString() nie wie czy ten owoc jest opcjonalny czy nie. Ty jako programista wiesz czy może czy nie może, ale funkcja nie wie. Czy gdyby wszystkie instancje klasy Fruit były opcjonalne w Twojej aplikacji, czy miałoby sens żeby Fruit fruit = null; fruit.toString() zwróciło ""? Ktoś powiedziałby że tak, skoro wszystkie owoce z punktu widzenia usera są opcjonalne.

Podobnie jak w moim przykładzie z PdfConverter, klasa

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.

Czyli przemilczeć buga? No nie wiem czy to taki dobry pomysł. Języki programowania to narzędzia, mają być reliable. Jeśli ja ich źle używam (bo np przekazuje html'a do pdf konwertera) to chcę żeby ta klasa mi o tym powiedziała. Nie chcę, żeby zakładała że nie-pdf' może się tam znaleźć.

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.

No, mniej więcej to samo co opisywałem z NullPointerException. Jeśli programista zrobi coś głupiego, np podzieli przez 0 albo moim zdaniem również wsadzi html'a do pdf'a, to tak, powinien polecieć wyjątek, i jeśli zostanie nie obsłużony to aplikacja powinna się wywalić. Niemniej, lepiej albo do takiej sytuacji nie dopuścić (nie wkłądać html'a do PdfConverter), a jeśli został włożony to złapać wyjątek i obsłużyć.

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.

No to git :)

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.

No dobra, i co powinien zrobić PdfConverter kiedy dostanie *.docx albo *.html? PdfConverter przetowrzy *.pdf, rzuci wyjątek dla każdego formatu na świecie (*.docx, *.bmp), ale *.html przepuści normalnie?

Brzmi jakby to "obsłużenie ścieżki" to była cicha pułapka którą PdfConverter na mnie zastawił, żebym w nią wpadł, i jak będę miał kiedyś buga, to zamiast mi powiedzieć "Masz buga, wsadziłeś html'a do PdfConvertera" to po prostu to przemilczy i nie będę wiedział że mam buga.

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.

No oczywiście że się da. Jeśli się uprzesz to nawet w brainfucku napiszesz genialną aplikacje. Ale to że się da, nie znaczy że to dobry sposób. Języki ewooluują, i naszczęście teraz mamy elementy języka które to ogarniają.

Justin Bieber i PHP są najlepszymi przykładami na to, że nie musisz być dobry, żeby być popularny.

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

Może i tak. Ale dopóki łapiesz te wyjątki ostatecznie nisko, to z mojej strony wszystko jest okej :D

Tak, parser jak dostanie nieprawidłowe dane, to powinien rzucić wyjątkiem. Tutaj się zgadzam.

Fajnie.

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.

Możesz zrobić też if (file.isPdf()) { converter.convert(file); } else { /* handel */ }. Dla mnie if + convert + else, to jest to samo co try { convert } catch {else}. Z punktu widzenia języka dla mnie one są tożsame ze sobą, przejdą ten sam zestaw unit testów, dla mnie to szczegół implementacyjny. Klasa która używa takiego pośrednika nawet nie będzie wiedziała czy ten wyjątek tam w środku jest czy nie. Dla klasy która umie decydować który typ może co, to możesz wybrać co Ci pasuje, i nie przeszkadzałoby mi gdybyś Ty w tej aplikacji zrobił to ifem. ALE! ALE! Ale co w sytuacji w której *.html nie może wejść. W której tylko *.pdf jest dostępny, i nie ma żaden ifologii która decyduje o extensionach. Wtedy taki wyjątek jest jak tarcza przeciwko bugom.

@cerrato Ale, no właśnie jest "ale". Spytasz pewnie, skoro dla mnie to to samo, to czemu wolę wyjątki, prawda? :D

Odpowiadam.

Żeby zrobić ifa, zanim się da plik do PdfConverter.convert(file), to trzeba zawołać coś takiego if (file.isPdf()) prawda? No albo ewentualnie if (file.getName().endsWith(".pdf")) czy coś podobnego. I teraz, co to znaczy. To znaczy, że klasa wyżej, ponad PdfConverter umie rozpoznać czy coś jest plikiem pdf czy nie. To prawda, możesz to wydelegować do utilsa, if (FileUtils.isPdf(file)), ale to ciągle oznacza, żę klasa która woła PdfConverter musi wiedzieć o tym czy plik jest pdf'em czy nie, albo bezpośrednio, albo pośrednio przez ten util. Więc złamała poziomy abstrakcji. Moim zdaniem, tylko PdfConverter.convert(), albo jego klasy niżej, powinny wiedzieć co jest pdf'em a co nie. Np gdyby doszedł drugi format pliku, np *.pdf oraz *.pdfx. Oczywiście PdfConverter musiałby o tym wiedzieć, ale wszystkie warstwy które go użyły też musiałyby o tym wiedzieć, żeby zrobić odpowiedniego ifa do ohandlowania tego. A nawet jeśli masz ten util, to wszystkie warstwy muszą pamiętać żeby użyć tego utila. Innymi słowy, PdfConverter moim zdaniem lepiej wie co jest poprawnym pdf'em, a co nie. I tej klasy powinniśmy słuchać przy decyzji czy coś jest pdf'em a co nie; nie warstwy która go woła if (file.getName().endsWith(".pdf")), i nie utila, tylko PdfConvertera właśnie, a jedyny sposób w jaki może nam o tym powiedzieć to albo exception, albo ten PageResult który @somekind zaproponował, ale z tym jeszcze nie mam doświadczenia. I nie ma też sensu wystawiać metody publicznej w PdfConverter, tak: if (pdfConverter.isPdf(file)) { pdfConverter.convert(file)); , bo po pierwsze łamie to hermetyzację; po drugie wymusza kontrakt że cokolwiek co isPdf() == true nie rzuca wyjątk, a cokolwiek isPdf() == false rzuca wyjątek, a po trzecie prezentuje interfejs który trochę nie jest SRP.

To jest oczywiście prosty przykład, ale wyobraź sobie system w którym nie jest tak łatwo sprawdzić czy coś pyknie, zanim się nie spróbuje.

Poza tym, słyszałeś może o zasadzie " Tell, don't ask"? Wygooglaj.

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.

Czyli chciałbyś żyć w takim świecie w którym wyjątek == "na 100% ja coś zjeb*** i muszę to szybko naprawić" i w którym wyjątek nie ważne czy na samym dole czy na samej górze, zawsze znaczy to samo.

Nie chcesz żyć w świecie w którym wyjątek == "Jestem klasą/funkcją która dostała parametr i nie wie jak go obsłużyć".

Dobrze rozumiem? :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ć.

Dla mnie to jedno i to samo. Jak chcesz, to zrób ifa.

Dla mnie te dwa kody są identyczne, np w pythonie

try:
  return int(value);
except NumberFormatException:
  return 0;

oraz

if isinstance(value, int):
  return int(value)
return 0

Te same unity mogą otestować ten kod, przejdzie dokładnie ten sam test, działa dokładnie tak samo, z zewnątrz klasy te dwa case'y są nierozróżnialne. Dla mnie to szczegół implementacyjny. Różnica jest tylko wtedy kiedy nie robisz ani try ani if, tylko robisz:

return int(value); # i tu co by było lepiej, skoro ktoś nie obsłużył ani try'em ani if'em złej wartości?

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.

Ale to by znaczyło że jeśli mamy klasę A, która woła PdfConverter, i ten converter rzucił wyjątek, i według ciebie to znaczy że "przyjmijmy wyjątek na klatę i stwierdźmy, że się nam coś powaliło" to to również oznacza że klasa niżej, decyduje jak działa klasa wyżej. A to mi się już nie podoba zupełnie. To trochę tak, jakbyś pisząc klasę PdfConverter zkaładał, że w momencie w którym w tej klasie napiszesz throw new NotPdf(), to znaczy że cokolwiek co zawołało ten converter już nie ma opcji żeby cokolwiek zrobić. Tak jakby nie było możliwości złapania go wyżej, i obsłużenia, tylko że ten wyjątek poleci na samą górę, i pokaże się userowi : "I tried, but not".

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.

No bez jaj, nie używajmy magic values. Pls.

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.

Zawsze możesz łapać szerzej błędy, coś jak catch (Exception) na samej górze. Masz milion możliwości do obsłużenia tego, jaki wyjątek w której warstwie.

3

@TomRiddle: aplikacja powinna sprawdzić czy to jest pdf, a dopiero poterm gow ysłać do funkcji która przetwarza pdf. Jeżeli ta funkcja ma jednocześnie konwertować i sprawdzać (rzucając wyjątkiem jeśli to ni jest) to właśnie wtedy łamie zasadę SRP

1

No dobra, i co powinien zrobić PdfConverter kiedy dostanie *.docx albo *.html? PdfConverter przetowrzy .pdf, rzuci wyjątek dla każdego formatu na świecie (.docx, *.bmp), ale *.html przepuści normalnie?

Either<FileError, byte[]>

2

Nigdy nie ma sensu wołanie metody na nullu. Ale czy to powstrzymuje userów przed używaniem nulli? Nie, wszędzie zobczysz if (x == null) albo if (x != null).

Tutaj mamy w mojej ocenie mieszanie pojęć. Oczywiście - działania na nullach to jakaś bzdura. Ale nie zgadzam się z tym, że if (x == null) jest czymś złym.
To jest bardzo fajny element, który wykrywa fakap zanim się stanie coś groźniejszego.
Masz jakąś wartość, na której chcesz pracować. Możesz ją pchnąć bezmyślnie dalej, ale wtedy może polecą wyjątki albo inne dziwne rzeczy się trafią, ale możesz tego uniknąć - chociażby właśnie we wspomniany sposób.
Oczywiście - nie rozwiąże to problemu, który spowodował że tego nulla mamy, ale przynajmniej nie pogorszy sytuacji. Zamiast jakiegoś błędu czy wywalenia apki na plecy, można pokazać userowi komunikat o błędzie i np. zamknąć daną zakładkę, na której pracował, ale jednocześnie apka nadal pozostanie funkcjonalna. A bez tego, jak null poleci niepilnowany w świat, to mogą się dziać naprawdę dziwne rzeczy ;)

Czyli przemilczeć buga?

Nie tyle przemilczeć, co pójść za tym, o czym sam pisałeś kilka razy - nie patrzeć przez pryzmat usera. Jeśli bug jest taki, że nie uniemożliwia pracy to działać dalej, a jedynie zalogować sobie że coś się popsuło i później niech ktoś to sprawdzi. A jeśli jest to sytuacja krytyczna to wyświetlić stosowny komunikat i pozamiatać to, co jeszcze się da.

No dobra, i co powinien zrobić PdfConverter kiedy dostanie *.docx albo *.html? PdfConverter przetowrzy .pdf, rzuci wyjątek dla każdego formatu na świecie (.docx, *.bmp), ale *.html przepuści normalnie?

Chyba piszemy o różnych rzeczach.
Mi bardziej teraz chodzi o samo podejście do architektury i sterowania przepływem wykonania.
O ile, patrząc jedynie przez pryzmat samego convertera, to logiczne jest rzucenie wyjątku w sytuacji, w której dostał coś innego niż PDF. Ale z punktu widzenia samej logiki to nie podoba mi się sterowanie wykonaniem w oparciu o wyjątki. Zamiast czekać czy konwerter zwróci wynik czy rzuci wyjątek, a jeśli wyjątek będzie to przekażmy może do konwertera, który obsługuje DOC i tak dalej, sama logika powinna być taka, że staram się zminimalizować ryzyko, że do konwertera trafi coś nieodpowiedniego.
Może błędnie zrozumiałem to, co pisałeś wcześniej, ale odniosłem wrażenie, że chcesz stosować wyjątki zamiast ifów do sterowania przebiegiem. Coś na zasadzie:

  • daję coś do konwertera
  • konwerter wywala wyjątek
  • skoro poleciał exception to nie był to PDF
  • więc spróbuję przekazać do konertera DOC
  • konwerter DOC wywala wyjątek
  • w związku z tym próbuję z konwerterem BMP

i tak dalej. Czy o takie coś Ci chodziło, czy się nie zrozumieliśmy? Bo jak pisałeś, że wyjątki są normalnym elementem języka i powinno się ich stosować do przekazywania wartości (plus ogólny odbiór innych Twoich postów) to ja to odebrałem, że to jest scenariusz, który Twoim zdaniem jest OK.

Możesz zrobić też if (file.isPdf()) { converter.convert(file); } else { /* handel */ }. Dla mnie if + convert + else, to jest to samo co try { convert } catch {else}

No a dla mnie to coś zupełnie innego.
Wyjątki są do sytuacji wyjatkowych/awaryjnych. Tak samo - można użyć gaśnicy proszkowej do zgaszenia palnika na kuchence, efekt taki sam - ogień zgaśnie, ale jednak lepiej po prostu przekręcić kurek ;)

Żeby zrobić ifa, zanim się da plik do PdfConverter.convert(file), to trzeba zawołać coś takiego if (file.isPdf()) prawda? No albo ewentualnie if (file.getName().endsWith(".pdf")) czy coś podobnego. I teraz, co to znaczy. To znaczy, że klasa wyżej, ponad PdfConverter umie rozpoznać czy coś jest plikiem pdf czy nie

Albo zrobić klasę, którą się obsługuje w taki sposób:

  • klasa.assignFile("cokolwiek.pdf')
  • klasa.isCorrectPdfLoaded()
  • jeśli powyższe TRUE to klasa.convert();

Zresztą sposobów jest kilka, nie wymagają one wyjątkowania ani angażowania jakiejś klasy nadrzędnej. I nie uważam, żeby było to jakieś wielkie złamanie zasad. O ile zgadzam się, że bezpośredni dostęp do treści załadowanego pliku by był lekkim pomieszaniem, ale odpytanie konwertera czy jest gotowy do pracy jest moim zdaniem OK.

Poza tym, słyszałeś może o zasadzie " Tell, don't ask"

Kojarzę hasło, ale jakoś do mnie to nie przemawia. Moim zdaniem ważna jest komunikacja w obie strony, a nie mechanizm czarnej skrzynki - że coś wrzucam do środka, coś się meli i albo dostaję wyjątek, albo wynik. Są jeszcze warianty pośrednie.

Czyli chciałbyś żyć w takim świecie w którym wyjątek == "na 100% ja coś zjeb*** i muszę to szybko naprawić" i w którym wyjątek nie ważne czy na samym dole czy na samej górze, zawsze znaczy to samo.

Odnośnie tego, gdzie ten wyjątek się pojawił to nie ma większego znaczenia - to jest osobna kwestia, jak zostanie on obsłużony.
Ale czy to jest wyjątek na górze czy na dole - nie ma znaczenia, bo zakładając, że exception jest stosowany w sytuacji fakapu, a nie wykorzystywany do zwykłego przekazywania informacji zwrotnych, zawsze powinien on być czymś ważnym.

Dla mnie te dwa kody są identyczne, np w pythonie

A dla mnie nie. Drugi w mojej ocenie jest "poprawny" bo ogarnięciem poprawności danych zajmuje się logika, a wyjątek rzucamy wtedy, gdy dzieje się coś dziwnego. Sam fakt dostarczenia jako argument czegoś nieprawidłowego może spowodować rzucenie wyjątku, ale można tego uniknąć poprzez wcześniejsze sprawdzenie. I nie ma sensu odpalania całej machiny wyjątków zamiast jednego prostego if. Jak kilka osób wyżej pisało (albo w wątku podlinkowanym przez @Miang - teraz nie pamiętam) to o wiele bardziej kosztowne jest odpalenie procedury wyjątkowej, niż "zwykłe" działanie. Oczywiście - przy pojedynczym wywołaniu to nie ma znaczenia, ale jeśli takich akcji są setki, jedna po drugiej, to może być to odczuwalne. I nie uważam tego za mikrooptymalizację.

0

Ale nie zgadzam się z tym, że if (x == null) jest czymś złym.To jest bardzo fajny element, który wykrywa fakap zanim się stanie coś groźniejszego.
Masz jakąś wartość, na której chcesz pracować. Możesz ją pchnąć bezmyślnie dalej, ale wtedy może polecą wyjątki albo inne dziwne rzeczy się trafią, ale możesz tego uniknąć - chociażby właśnie we wspomniany sposób

Wszystko pięknie, tylko jest problem, jak w dużej aplikacji będziesz pchał te if Null - e przez wiele poziomów to nie ma bata gdzieś o nim zapomnisz, a potem ktoś będzie oglądał stacktraces na kilka/naście ekranów.

1

Z nullami i NullPointerException to średni przykład. Bo akurat w nowszych językach jest to obsłużone na poziomie kompilatora.
Kod gdzie wywołujesz metodę na potencjalnym nullu.. się po prostu nie skompiluje. Referencje są nienullowalne, a gdyby jakiś dziadyga jednak nulla potrzebował to musi zadeklarować explicite nullowalności i za każdym razem (przed wywołaniem metody itp.) się tłumaczyć przed kompilatorem, że tam nie ma nulla. Nie da się dostać NullPointerExceptiona (to znaczy.. da się, ale trzeba robić sztuczki i oszukiwać kompilator (co jest proste, ale zawsze to dodatkowy wysiłek) ). Ogólnie to ten sam przypadek co mój przykład z dzieleniem - potencjalnie niepoprawny kod sie nie skompiluje. Potrzeba wyjątku znikła.

0
cerrato napisał(a):

Nigdy nie ma sensu wołanie metody na nullu. Ale czy to powstrzymuje userów przed używaniem nulli? Nie, wszędzie zobczysz if (x == null) albo if (x != null).

Tutaj mamy w mojej ocenie mieszanie pojęć. Oczywiście - działania na nullach to jakaś bzdura. Ale nie zgadzam się z tym, że if (x == null) jest czymś złym.
To jest bardzo fajny element, który wykrywa fakap zanim się stanie coś groźniejszego.

Nie mówiłem że używanie nulli jest złe.

Próbowałem Ci powiedzieć, że mimo że ludzie wszędzie używają ifów do obsługi nulli, to mimo to to jest bardzo dobrze że środowisko rzuca NPM kiedy się go użyje nie właściwie. Więc tu się zgadzamy.

Masz jakąś wartość, na której chcesz pracować. Możesz ją pchnąć bezmyślnie dalej, ale wtedy może polecą wyjątki albo inne dziwne rzeczy się trafią, ale możesz tego uniknąć - chociażby właśnie we wspomniany sposób.

No tak, i własnie dlatego rzucenie wyjątku w takim case'ie ma sens. Podobnie jak w przypadku PdfConvertera. Niepoprawna wartość (null albo *.html) == wyjątek NullPointerException albo NotPdfException.

Oczywiście - nie rozwiąże to problemu, który spowodował że tego nulla mamy, ale przynajmniej nie pogorszy sytuacji. Zamiast jakiegoś błędu czy wywalenia apki na plecy, można pokazać userowi komunikat o błędzie i np. zamknąć daną zakładkę, na której pracował, ale jednocześnie apka nadal pozostanie funkcjonalna. A bez tego, jak null poleci niepilnowany w świat, to mogą się dziać naprawdę dziwne rzeczy ;)

No, wbrew pozorom pogorszy, bo go ukryje, schowa, zaciemni miejsce skąd ten null przyszedł, ciężej będzie zlokalizować dokładne miejsce skąd się wziął i dokąd poszedł.

Czyli przemilczeć buga?

Nie tyle przemilczeć, co pójść za tym, o czym sam pisałeś kilka razy - nie patrzeć przez pryzmat usera. Jeśli bug jest taki, że nie uniemożliwia pracy to działać dalej, a jedynie zalogować sobie że coś się popsuło i później niech ktoś to sprawdzi. A jeśli jest to sytuacja krytyczna to wyświetlić stosowny komunikat i pozamiatać to, co jeszcze się da.

No, czyli przemilczeć. Jedyne co to zalogujesz to, ale poza tym aplikacja działa tak jakby nie było bug'a, jeśli jakiś bug jest, to user go zobaczy. + masz logi.

No dobra, i co powinien zrobić PdfConverter kiedy dostanie *.docx albo *.html? PdfConverter przetowrzy .pdf, rzuci wyjątek dla każdego formatu na świecie (.docx, *.bmp), ale *.html przepuści normalnie?

Chyba piszemy o różnych rzeczach.
Mi bardziej teraz chodzi o samo podejście do architektury i sterowania przepływem wykonania.
O ile, patrząc jedynie przez pryzmat samego convertera, to logiczne jest rzucenie wyjątku w sytuacji, w której dostał coś innego niż PDF. Ale z punktu widzenia samej logiki to nie podoba mi się sterowanie wykonaniem w oparciu o wyjątki. Zamiast czekać czy konwerter zwróci wynik czy rzuci wyjątek, a jeśli wyjątek będzie to przekażmy może do konwertera, który obsługuje DOC i tak dalej, sama logika powinna być taka, że staram się zminimalizować ryzyko, że do konwertera trafi coś nieodpowiedniego.
Może błędnie zrozumiałem to, co pisałeś wcześniej, ale odniosłem wrażenie, że chcesz stosować wyjątki zamiast ifów do sterowania przebiegiem. Coś na zasadzie:

  • daję coś do konwertera
  • konwerter wywala wyjątek
  • skoro poleciał exception to nie był to PDF
  • więc spróbuję przekazać do konertera DOC
  • konwerter DOC wywala wyjątek
  • w związku z tym próbuję z konwerterem BMP

i tak dalej. Czy o takie coś Ci chodziło, czy się nie zrozumieliśmy? Bo jak pisałeś, że wyjątki są normalnym elementem języka i powinno się ich stosować do przekazywania wartości (plus ogólny odbiór innych Twoich postów) to ja to odebrałem, że to jest scenariusz, który Twoim zdaniem jest OK.

No, to zależy. W naszym przypadku? Że weź pdfa to zamień go na html, a jak to html to go zwróć? To nie. Nie poszedłbym w to. Ale, jeśli polecenie biznesowe byłoby napisać aplikacje która: Bierze plik, jeśli to pdf to konwertuje na html', jeśli to doc to rzuca wyjątek, jeśli to bmp to konweruje na coś inneg, etc.", to nie widzę problemu żeby to zrobić albo ifami albo wyjątkami. To zależy od skomplikowania aplikacji które konwertery będziesz używał. Jeśli aplikacja jest skomplikowana - tzn wymagania są skomplikowane - to i oczywiste że potencjalnych wyjątków będzie więcej.

Możesz zrobić też if (file.isPdf()) { converter.convert(file); } else { /* handel */ }. Dla mnie if + convert + else, to jest to samo co try { convert } catch {else}

No a dla mnie to coś zupełnie innego.

Czemu niby? Gdybyś napisał unit test do UserController, to nie miałbyś w żaden sposób kontroli nad tym co on robi. Z warstwy wyżej to jest po prostu nierozróżnialne, a więc szczegół implementacyjny.

Wyjątki są do sytuacji wyjatkowych/awaryjnych. Tak samo - można użyć gaśnicy proszkowej do zgaszenia palnika na kuchence, efekt taki sam - ogień zgaśnie, ale jednak lepiej po prostu przekręcić kurek ;)

Niemerytoryczny argument, bo użycie gaśnicy ma dużo nieprzyjemnych konsekwencji. Rzucenie i złapanie wyjątku nie, jeśli jest obsłużone.

Żeby zrobić ifa, zanim się da plik do PdfConverter.convert(file), to trzeba zawołać coś takiego if (file.isPdf()) prawda? No albo ewentualnie if (file.getName().endsWith(".pdf")) czy coś podobnego. I teraz, co to znaczy. To znaczy, że klasa wyżej, ponad PdfConverter umie rozpoznać czy coś jest plikiem pdf czy nie

Albo zrobić klasę, którą się obsługuje w taki sposób:

  • klasa.assignFile("cokolwiek.pdf')
  • klasa.isCorrectPdfLoaded()
  • jeśli powyższe TRUE to klasa.convert();

Zresztą sposobów jest kilka, nie wymagają one wyjątkowania ani angażowania jakiejś klasy nadrzędnej. I nie uważam, żeby było to jakieś wielkie złamanie zasad. O ile zgadzam się, że bezpośredni dostęp do treści załadowanego pliku by był lekkim pomieszaniem, ale odpytanie konwertera czy jest gotowy do pracy jest moim zdaniem OK.

Ale jest. Łamie warstwy abstrakcji, łamie hermetyzację, sprawia że więcej niż jeden obiekt wie o PDF'ach, i łamie SRP.

  • Jeśli otwieram plik *.zip, to nie chcę najpierw sprawdzać czy mam dostęp. Wolę otworzyć, i złapać permission exception, i obsłużyć to inaczej.
  • Jeśli wysyłam maila, to nie chcę najpierw sprawdzać czy mam neta, a dopiero potem wysłać. Wolę wysłać, i jeśli się nie udało to zrobić coś innego, np zapisać kopie zapasową.
  • Jeśli chcę coś wkleić, to nie sprawdzam czy mam coś w schowku zanim wkleję, tylko wklejam, i jeśli schowek jest pusty to ogarniam exception, i robię coś innego, np nie edytuję dokumentu w ogóle
  • Jeśli chcę zespawnić proces, to nie sprawdzam najpierw czy mam admina, i sprawdzam wszystkie permissiony których potrzebuje posługując się rozmaitymi utilami, tylko spawnuje proces, i obsługuję wyjątek.

Przykłady można mnożyć.

Poza tym, słyszałeś może o zasadzie " Tell, don't ask"

Kojarzę hasło, ale jakoś do mnie to nie przemawia. Moim zdaniem ważna jest komunikacja w obie strony, a nie mechanizm czarnej skrzynki - że coś wrzucam do środka, coś się meli i albo dostaję wyjątek, albo wynik. Są jeszcze warianty pośrednie.

No i nie rozumiem czemu wrzucenie *.htmla do PdfConvertera miałoby nie skończyć się wyjątkiem?

Dla mnie te dwa kody są identyczne, np w pythonie

A dla mnie nie. Drugi w mojej ocenie jest "poprawny" bo ogarnięciem poprawności danych zajmuje się logika, a wyjątek rzucamy wtedy, gdy dzieje się coś dziwnego. Sam fakt dostarczenia jako argument czegoś nieprawidłowego może spowodować rzucenie wyjątku, ale można tego uniknąć poprzez wcześniejsze sprawdzenie. I nie ma sensu odpalania całej machiny wyjątków zamiast jednego prostego if. Jak kilka osób wyżej pisało (albo w wątku podlinkowanym przez @Miang - teraz nie pamiętam) to o wiele bardziej kosztowne jest odpalenie procedury wyjątkowej, niż "zwykłe" działanie. Oczywiście - przy pojedynczym wywołaniu to nie ma znaczenia, ale jeśli takich akcji są setki, jedna po drugiej, to może być to odczuwalne. I nie uważam tego za mikrooptymalizację.

Znowu argumenty nie merytoryczne. Posługujesz się określeniami "machina wyjątków" oraz "jeden prosty if", tak żeby nadać nacechowanie tym elementom. Ale oba są używane we wszystkich językach programowania, i obie są proste dla każdego kto programuje na nie amatorskim poziomie.

Co do performance'u, to odsyłam do trzech zasad optymalizacji. Są to:

  1. Don't.
  2. If you think you should, don't
  3. If you must, use profiler.

Jestem niemal przekonany, że w wielu aplikacjach znajdzie się 100 bardziej ważnych punktów do optymalizacji niż używanie wyjątków. A nawet jak się tak stanie, to wystarczy że dorobisz swojego ifa na początku, a wyjątek nadal będzie bezpieczną tarczą przeciwko bugom. Bo nie będzie sprawiał performance'owego bólu, jeśli nie zostanie wyrzucony.


@cerrato Szanuję Cię jako forumowicza, ale nie widzę specjalnej wartości w Twoich dwóch ostatnich postach. Włożyłeś dużo wysiłku w to, żeby odpisać na moje posty zgodnie z formatem, ale nie widzę w kolejnych postach dodatkowych argumentów z Twojej strony. Jedynie powtarzanie Twoich przekonań bez uargumentowania ich. Wpadniemy w nieskończone echo. Proponuję streścić niejasności, może napisz statementy/questiony/rzeczy z którymi się nie zgadzasz które masz w prostej liście i zaczniemy od nowa?

2

Nie mogę się zgodzić, że próba otwarcia nieistniejącego pliku powinna rzucać wyjątkiem. Ta sytuacja jest poza kontrolą aplikacji, nie da się przed nią zabezpieczyć jak przed dzieleniem przez zero, więc to powinna być standardowa ścieżka do obsłużenia, tak jak każde inne nieprawidłowe dane od użytkownika.

1

@Krolik: Możesz przecież sprawdzić czy plik istnieje zanim go otworzysz.

0

@PerlMonk:

i modlić się że pomiędzy sprawdzeniem i użyciem nikt nic z nim nie zrobi :P

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