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?