Obsługa błędów (a'la custom exceptions)

Temat dotyczy czystego Ruby

Witam. Zastanawiam się nad obsługą błędów w języku Ruby. Załóżmy sobie, że tworzymy mini-aplikację typu “bank”, w której użytkownik poprzez linię poleceń loguje się. Jeśli poda poprawne dane to wszystko ok, wszystko toczy się swoim rytmem - wyświetla komunikat “zalogowano” i znowu zwraca linię poleceń.

Ale co jeśli poda nieprawidłowe? Możemy oczywiście realizować to na if/else. Jednak całkiem sensownym rozwiązaniem wydaje mi się użycie Exceptions (IncorrectInformationError). Z tymże używanie begin-rescue-end jest… trochę nieeleganckie. W Rails istnieje coś takiego jak rescue_from - wtedy wystarczy tylko rzucić wyjątkiem w środku kodu i aplikacja (dzięki rescue_from) wie, jaką metodę ma wywołać, a resztę kodu zatrzymać (zatrzymuje tylko w obrębie metody, w której został wywołany wyjątek, tak?). Czegoś takiego chciałbym użyć w aplikacji w czystym Ruby, jednak język sam w sobie nie oferuje takiej funkcjonalności.

Moje pytanie dotyczyłoby dwóch aspektów: przede wszystkim chciałbym się dowiedzieć, w jaki sposób stworzyć taka funkcjonalność jak rescue_from, a po drugie: w jaki sposób realizować taką walidację jak np. sprawdzanie poprawności danych?

Jeśli jesteś zainteresowany tym w jaki sposób Railsy zapewniają taką obsługę (a myślę, że jesteś skoro chcesz zaimplementować coś podobnego u siebie ;)) to polecam zajrzenie do źródeł.

Tutaj masz impementację rescue_from, a tutaj samą obsługę akcji. Jak widać do tablicy handlerów dodajemy obsługę dla poszczególnego wyjątku, a następnie w samej obsłudzę wyjątku w akcji kontrollera wywołujemy odpowiedni handler dla danego wyjątku.

Zastanowił bym się jednak czy jest to najlepsza metoda dla rozwiązania Twojego problemu.

@soanvig Są generalnie trzy rozwiązania - if/else które jak najbardziej polecam. throw/catch jeżeli masz naprawdę mnóstwo danych do sprawdzenia i chciałbyś móc “wyskoczyć” w dowolnym momencie. I na samym końcu wyjątki. Dlaczego? Wyjątki powinny być stosowane tylko i wyłącznie do sytuacji … zgadliście wyjątkowych. Jeżeli fakt że użytkownik wprowadzi złe dane jest znany i należy do standowego przepływu, wtedy nie należy stosować wyjątków.

Jeżeli kogoś nie przekonują argumenty natury architektonicznej to dodam jeszcze że wyjątki są wilokrotnie wolniejsze niż if/else czy nawet throw / catch

1 Like

Wiem, że wyjątki są najwolniejsze. W takim razie załóżmy throw/catch. Wykonać to mogę podobnie jak railsy wykonują rescue_from, prawda? Wystarczy, że w miejscu błędu wywołam funkcję, która wykona X + throw.
Problem tylko może być ze zdefiniowaniem obszaru, który powinien obejmować catch. Ale to już chyba zależy od struktury aplikacji.
EDIT: throw nie musi być wewnątrz catcha, prawda? Jeśli nazwę jeden i drugi tą samą nazwą? Liczy się tylko to, że catch jest wyżej w hierarchii?

Generalnie ze względu na przejrzystośćthrow/catch powinien być w jednej metodzie … maksymalnie w jednym obiekcie. Powiedzmy:

error = catch(:wrong data) do
  throw(:wrong_data, :login_incorrect) unless login_correct 
  throw(:wrong_data, :password_incorrect) unless password_correct
  bla bla bla bla inne rzeczy
  throw(:wrong_data, :birth_date_incorrect) unless data_urodzenia poprawna
end

if error
  bleh
else
  blah
end

Z tym że zazwyczaj o wiele łatwiej zrobić coś takiego jako serię if/else.
Co więcej z punktu widzenia UX to najlepiej było by przeprowadzić wszystkie testy tak czy tak i zwrócić wszystkie błędy (nic tak bardzo nie wk*** jak wysłanie forumlarza po poprawieniu jednego błędu tylko po to żeby zobaczyć kolejny błąd).
A z punktu widzenia bezpieczeństwa powinieneś przeprowadzić wszystkie testy za każdym razem oraz upewnić się że każdy z nich za każdym razem wykonuje się tak samo długo (np porównania łańcuchów zawsze wykonują maksymalną dozwoloną ilość porównań) żeby zapobiec tzw. timing attacks

Catch musi obejmować throw w stosie (throw musi być objęty catch).

Sposób w jaki railsy robią rescue_from jest powiedzmy kontrowersyjny. Tego typu obsługa wyjątków jest generalnie uważana za antywzorzec.

Podsumowując: O ile nie ma naprawdę dobrego powodu żeby nie używać if/elsif/else, albo dokonywać wszystkich testów tak czy tak, to absolute nie należy używać throw/catch ani wyjątków.

PS. Żeby dać jakieś wyobrażenie gdzie throw/catch jest użyteczne:

  # Pobieraj dane aż skończy się strumień albo znajdziemy złoto.
  catch(:znalezlismy_zloto) do
    while (data = fetch_data)
      for x in data
        if x == y
          do something with data
          throw(:znalezlismy_zloto) if znaleziono_zloto?
       end
      end
     end
   end
  end

Chodzi generalnie o łatwość “wyskoczenia” z bardzo głębokiego zagnieżdżenia (np. 3-4 pętle), bez try/catch musielibyśmy na każdej pętli dodawać ekstra warunek sprawdzający czy mamy przerwać przetwarzanie danych, przy pomocy catch/throw możemy przerwać przetwarzanie w dowolnym punkcie bez zaciemniania pętli/warunków.

Mnie zawsze uczono, że to czy rzucać wyjątki czy sprawdzać zmienne zależy tak naprawdę tylko od tego ile taki wyjątek kosztuje. W Javie nie rzuca się wyjątkami na prawo i lewo, bo sprawdzanie zmiennej jest chyba zawsze, niezależnie od wyniku tańsze od wyjątków. Ale np. w Pythonie używanie try except jest tylko nieznacznie wolniejsze od if’a w przypadku błędu, a w każdym innym przypadku szybsze i dlatego też EAFP Easier to ask for forgiveness than permission jest polecaną strategią.

Ale jeżeli w Rubym wyjątki są wielokrotnie wolniejsze od if’ów to flow control należy robić za pomocą if’ów.

W takim razie źle cię uczono :smiley: Każda struktura/wzorzec w programowaniu ma swoje przeznaczenie, używanie wyjątków do sterowania przepływem programu to duże snafu w każdym języku programowania.

Nie możesz pisać że w każdym, bo tak jak pisałem bo w Pythonie EAFP jest standardem i jak to Python glossary określa:

This clean and fast style is characterized by the presence of many try and except statements.

To tak jakbyś chciał stwierdzić że goto jest niewybaczalne w żadnym języku, ale zapomniał o assemblerze.

Przeczytałem to glosary. Wyrwałeś cytat z kontekstu. EAFP odnosi się do konkretnego przypadku - dostępu do danych lub wołania metod które mogą nie być zdefiniowane

This common Python coding style assumes the existence of valid keys or attributes and catches exceptions if the assumption proves false.

czyli zamiast

if access[:complicated] && access[:complicated][:data] && access[:complicated[:data][:structure]
  foo = access[complicated][data][structure]
end

robisz po prostu

begin
  foo = access[complicated][data][structure]
rescue NoMethodError
  # Przy czym niestety no method error jest strasznie szerokim wyjątkiem więc wypadało by sprawdzić czy pasuje do wzorca <NoMethodError: undefined method `[]' for nil:NilClass>
end

Co jest akceptowalnym (a nawet powidziałbym że lepszym/bardziej eleganckim) podejściem w tych konkretnych przypadkach (z dodatkowym bonusem dla którego rekomendowane to jest w pythonie czyli wątki).

Python NIE rekomenduje używania wyjątków do kontroli przepływu programów. Do tego są ify

Masz rację, niepotrzebnie zupełnie pisałem o flow control, bo zupełnie też nie przemyślałem co to jest flow control.

W takim układzie użyję standardowego if/else, ale napiszę klasę do obsługi błędów.