Przy czym jeszcze trzeba doprecyzować, co masz na myśli pisząc o "punkcie widzenia usera" - czy chodzi o programistę, czy użytkownika aplikacji?
Od razu mówię: w całej mojej wypowiedzi, kiedy pisałem "user" miałem na myśli użytkownika aplikacji.
Przykładem może być program: "Spróbuj sparsować plik jak HTML, a jeśli się nie da, to zwróć pierwszą linijkę". Z punkty widzenia usera nie ma tu miejsca na sytuacje wyjątkową, a więc wyjątek
Ja bym to widział inaczej:
- jeśli się da to przetwórz plik
- jeśli się nie da to zwróć pierwszą linię
- jeśli pliku nie ma albo nie da się do niego dostać, to wtedy właśnie mamy tą "sytuację wyjątkową" i możemy rzucić wyjątek, żeby się nam apka nie wysypała.
Noi to jest typowy punkt widzenia usera - punkt widzenia holistyczny, z całej aplikacji. Tzn "nie mogę otworzyć pliku HTML" to nie jest błąd dla usera, ponieważ jest pokazany inny scenariusz. Dla usera to nie jest błąd. Błedem jest za to brak pliku. To jest totalnie, 100% poprawny widok usera na plikacje; widok całościowy; i moim zdaniem to jest to co programiści przez pomyłkę starają się odwzorować.
Ale, jest też inny sposób żeby na to popatrzeć; nie całościowy, ale lokalny. 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. Dla usera nie jest wyjątkowa, bo będzie obsłużona tak czy tak; ale dla programisty jest wyjątkowa, bo nie da się wczytać HTMLa. I tą sytuację wyjątkową można obsłużyć wyjątkiem; a ten wyjątek. dopiero w warstwie wyżej obsłużyć drugą ścieżką scenariusza. Pewnie zapytasz "co za róznica?" - no różnica jest taka, że tutaj używasz klasy HtmlReader
w takim celu, ale nie koniecznie. Klasa HtmlReader
nie musi wiedzieć że plik "może"/"ma pozwolenie" na bycie niepoprawnym. Klasa powinna być głupia. Powinna nie wiedzieć, jakie są podjęte decyzje biznesowe nt scenariuszy które dzieją się poza jej kontrolą. Gdyby jej pozwolić na zdecydowanie o tym czy przepuścić ten plik czy nie, ta klasa załamałaby SRP i ciężej byłoby jej użyć w innym miejscu.
Przy czym jeszcze trzeba doprecyzować, co masz na myśli pisząc o "punkcie widzenia usera" - czy chodzi o programistę, czy użytkownika aplikacji? Bo user jako użytkownik w ogóle nie powinien wiedzieć, w jaki sposób są różne rzeczy zaimplementowane. On widzi jedynie 3 stany:
- dostaję widok HTML
- dostaję jakąś linię dziwnych treści, których się nie dało sparsować
- dostaję komunikat, że jest jakiś błąd - np. brak pliku do przetworzenia
Again, typowy widok na aplikacje przez oczy usera, który widzi tylko warstwę interfejsu użytkownika; który nie widzi żadnych warstw niżej.
To by miało sens, gdyby wyjątki były globalne. Tzn raz rzucone, mogą być tylko obsłużone w najwyższej warstwie. Wtedy taki punkt widzenia miałby sens. Ale kiedy masz np 10 wartsw, i rzucisz wyjątek w 9 warstwie, który będzie obsłużony przez 7dmą wartswę, to user nie musi o tym nic wiedzieć; a z punktu widzenia designu to ciągle będzie dobra decyzja.
Można zrobić tak że parser html'a rzuca wyjątek, i jest on używany żeby przejść na drugą ścieżkę pobierania pierwszej linijki.
Ale można to zrobić bez wyjątków - na zasadzie if (!udaloSieSparsowac) uruchomDruguSposob()
. I głównie o to mi chodziło w moim pytaniu - czemu jedna albo druga droga by miała być lepsza, czemu w razie jakiejś sytuacji problematycznej (ale jednak typowej i przewidywalnej, a nie jakieś przekroczenie zakresu, brak pamięci itp) lepiej/gorzej jest rzucić wyjątek, a nie po prostu odpalić procedurę awaryjną
Można. Ale wtedy klasa zakłada że błędny html pochodzi od usera. Ta klasa da sobie rękę uciąć, że błędny HTML nie przyszedł od programisty, w wyniku np błędnie działającego upload'u. Prawdziwie dobra klasa byłaby, gdyby była sceptyczna. Gdyby klasa pomyślała: "hmm, czy ten argument przyszedł od usera i trzeba mu powiedzieć w jego języku (pretty-print)? Czy dane przyszły od programisty, i trzeba mu powiedzieć w jego języku (wyjątek)?". I moim zdaniem, jeśli klasa nie jest pewna skąd te dane są, powinna zawsze się komunikować w języku programistów a.k.a. elementy języka programowania, a.k.a w tym wypadku wyjątki.
Kiedy obliczam BMI, i wpiszę 70kg, 172cm wzrostu, a kalkulator powie mi że mam 180 BMI - to jest błąd, innymi słowy "bug".
A ja to widzę zupełnie inaczej - dla mnie błędem by było, jakby się takie obliczanie BMI wywaliło z czymś w stylu "proces wykonał niedozwoloną operację i zostanie zamknięty", albo dało odpowiedź w stylu "Twój BMi to zielony". Jeśli podaje błędną wartość to może i jest bug, ale po stronie logiki. Aplikacja wykonuje się poprawnie, ale po prostu jej wynik jest niezgodny z oczekiwaniami. Ale nie jest to dla mnie typowy błąd, który się ogarnia wyjątkiem, tylko trzeba sprawdzić dlaczego otrzymane wartości są z tyłka.
Poza tym zauważ, że wyjątek jest w sytuacji, która pojawia się nagle i wiąże się z czymś niezależnym - brak pliku, dzielenie przez zero itp. Pomijając ręczne ich rzucanie, w wielu miejscach mogą one być rzucane z automatu (jakbyś podzielił to swoje BMI przez zero). A to, że wynik obliczeń będzie nieprawidłowy, nie spowoduje tego, że samo się coś rzuci. Co najwyżej Ty możesz porównać otrzymaną odpowiedź z tym, co Twoim zdaniem jest dopuszczalne i samemu rzucić wyjątek. Ale równie dobrze możesz zamiast wyjątku wywołać funkcję badBMIResult()
, która zrobi to samo, co Twój wyjątek.
No właśnie, bo myslisz o wyjątkach jak o błędach :D Ale pomińmy może nazewnictwo. Kłótnie o nazewnictwo nie prowadzą do niczego. Może unikajmy słowa "błąd" i posługujmy się słowami "bug", "wyjątek" i "scenariusz terminujący" (np scenariusz, że jak user wpisze że ma 1200 lat, to jego ścieżka się kończy i widzi komunikat "błędny wiek"). + Jeszcze dodatkowymi, jeśli masz jakiś pomysł.
poziom ich obsługi (nisko, od razu user-friendly-message; albo wysoko wyjątek+try/catch) nie ma znaczenia.
No ja właśnie nie jestem taki pewien. Podejrzewam, że są jakieś "zasady sztuki", wytyczne, zalecenia itp, a ja czuję, że mimo poszukiwań nie znalazłem odpowiedzi, gdzie przebiega granica i kiedy lepiej stosować dane podejście. I nie ukrywam, że mnie to męczy. Bo z jednej strony często staram się unikać wyjątków i zrobić to "zwyczajnie" - czyli if (niepoprawnaWartosc) wyswietlKomunikat()
, ale z drugiej strony obawiam się, że jest to trochę amarotskie. Ale nie chcę też pójść w drugą stronę i przekombinować poprzez owyjątkowanie wszystkiego co się da, stworzyć tzw. exceptions driven development
;)
Źle zrozumiałeś moją wypowiedź, pozwól że napiszę ją jeszcze raz.
Faktycznie napisałem: "Łatwo stąd dojść do wniosku, że skoro tylko dane od usera mogą być błędne; to tylko taka obsługa błędów ma sens; a zatem poziom ich obsługi (nisko, od razu user-friendly-message; albo wysoko wyjątek+try/catch) nie ma znaczenia. I moim zdaniem to jest punkt wyjścia Twoich rozważań (mogę się mylić)."
Powinienem może napisać: "Łatwo stąd można się pomylić, że skoro tylko dane od usera mogą być błędne; to tylko taka obsługa błędów ma sens; a zatem w dalszych rozważaniach można dojść do błędnego wniosku że poziom ich obsługi (nisko, od razu user-friendly-message; albo wysoko wyjątek+try/catch) nie ma znaczenia. I moim zdaniem to jest punkt wyjścia Twoich rozważań (mogę się mylić)."
Czasem pochodzą od samego programu.. I właśnie takich sytuacji nie należy handlować nisko. Należy rzucić wyjątek, bo to się nie powinno zdarzyć.
Ale co za różnica czy:
- dane pochodzą od usera albo z innego źródła? Poza tym, że usera można poprosić o ponowne wprowadzenie, a inne źródło co najwyżej można odpytać ponownie
Bo o błędnych danych z innego źródła niż od usera, nie powinieneś informować usera. Usera trzeba informować "podałeś zły X", tylko wtedy jeśli to on wpisał X (albo wpisał inne dane które spowodowały X). Np wpisał "Firma: Aple", a firm "Aple" nie ma. Wtedy możemy mu pokazać "There is no company: Aple". Natomiast, jeśli błąd w aplikacji wynika np z tego że ktoś (my, programiści) zwaliliśmy kodowanie, i user wpisze "Firma: Kruger&Matz", a my się jebniemy z kodowaniem, i chcemy wysłać Firma: Kruger&Matz
, to nie możemy userowi pokazać: Firma: "Kruger&Matz" nie istnieje
. Absolutnie nie można takiego błędu mu pokazać, bo to nie on te dane wprowadził. Te dane są wynikiem jebnięcia się programisty. I jedyne co możemy zrobić to rzucić wyjątek. Jedyne co możemy zrobić, to złapać go wyżej i powiedzieć: "Był problem z przetwarzaniem Twojego rządania" albo wysadzić apkę całkiem.
- [...] co za różnica, czy takie coś obsłużę w ramach wyjątku, czy procedury awaryjnej odpalonej w sytuacji, w której stwierdzę, że pobrane dane są niezgodne z oczekiwaniami?
Bo 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.
Ciężko stwierdzić czy to user popełnił "błąd", czy programista. Dlatego z reguły core'y języków programowania rzucają wyjątek (bo zakładają że to programista się pomylił), ale aplikacje bądź frameworki zwracają default'y (bo zakładają że user się pomylił). Ale to jest tylko ever only an assumption.
Ale co to zmienia? Tak czy siak - jest jakiś problem. Dla mnie ważniejsze jest to, żeby problem został obsłużony - a czy w postaci wyjątku, czy przez ręczne odpalenie jakiejś funkcji, to raczej sprawa drugorzędna. Dlatego powtarzam pytanie - czym się różni reakcja w takiej sytuacji za pomocą wyjątku od reakcji "standadrowej" (np. przerwanie obliczeń, zapisanie błędu w logu, wyświetlenie komunikatu o błędzie itp.)?
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.
Co powinna zrobić klasa PdfToHtmlConverter kiedy dostanie plik *.html? Patrząc tylko na tę klasę, oczywistym jest że powinna rzucić wyjątek, bo jej odpowiedzialność to zamiana *.pdf na *.html, jeśli dostanie cokolwiek innego niż *.pdf, np *.docx albo właśnie również *.html, powinna rzucić wyjątek (pamiętajmy że to jednak nie jest błąd aplikacji - to po prostu sposób w jaki klasa odpowiedziała na parameter, bo wyjątek to element języka)
A można jeszcze inaczej - dodać jakiegoś pośrednika, który sprawdzi co to za plik. Jeśli HTML to go zwróci, jeśli PDF to przekaże do konwersji. W ten sposób wiemy, że obiekt konwertujący HTML na PDF zawsze dostanie to, czego mu do szczęścia potrzeba, więc w ogóle można w takim przypadku, za pomocą drobnej zmiany w logice, wyciąć potrzebę rzucania wyjątków albo obsługi sytuacji nietypowych (przynajmniej w zakresie radzenia sobie z typem pliku).
No okej.
UserController
> Pośrednik
> PdfToHtmlConverter
.
Bardzo mi się podoba ten pomysł, żeby to pośrednik sprawdzał, a nie PdfToHtmlConverter
na potrzeby usera.
Tylko teraz powiedz mi na pytanie :D Mimo pośrednika, co się powinno stać kiedy PdfToHtmlConverter
dostanie *.html
? :D Czyżby nie wyjątek? Który nigdy nie powinien polecieć, skoro ta sytuacja ma się nie zadżyć?
Ale w sumie można (chociaż to jest trochę prowizorka) zwrócić informację jako wynik funkcji - albo realny wynik, albo kod błędu - czyl analogia do rzucenia wyjątku. Coś w stylu error_get_last();
- https://www.php.net/manual/en/function.error-get-last.php
No, moim zdaniem ten error_get_last()
to wgl słaby pomysł z wielu powodów. Nie uznaję takich rozwiązań.
U mnie, wtedy kiedy jest 1000000% pewności, że błędne dane pochodzą od użytkownika albo od zewnętrznego źródła (jak plik albo network) oraz wtedy kiedy jest biznesowe wymaganie obsłużenia tego. Kiedy jest chociaż cień szansy, że bug jest moim błędem (programisty), wtedy rzucam wyjątek, i handluję go wyżej.
To jest chyba najbliżej odpowiedzi, jakiej oczekiwałem :D
Cieszę się :)
Tylko co w sytuacji, kiedy nie masz aż tak rozbudowanych warstw, kilka wywołań funkcji i nic poza tym? Jakaś prosta apka. Czy jest sens pchać tam wyjątki? Nie jest to overengineering
?
Nie sądzę. Wyjątek to element języka, czemu go nie użyć do celu do którego został stworzony. Ja bym nigdy nie stronił od wyjątków, bo uważam że to bardzo pomocny i użyteczny element języka. To że jest powszechnie (i moim zdaniem błędnie) utożsamiany z niepoprawnym korzystaniem aplikacji na najwyższej warstwie - cóż.