Kiedyś na stronach MS widziałem rekomendację mówiącą “używaj typów wyjątków dostarczanych przez framework“. Jakiś czas temu, na jakimś polskim chyba blogu, przeczytałem tego powtórkę. Wiecie co? U mnie się doskonale sprawdza kompletne przeciwieństwo tej praktyki.
Wyjątek bazowy
Zawsze w swoich aplikacjach staram się mieć jeden bazowy typ wyjątku, abstrakcyjna klasa MyAppException: Exception. Dzięki temu jestem w stanie wyłapać wszystko co rzuca mój kod, a nie jakieś flaki pod spodem. Wyjątki, które ja rzucam, będą charakterystyczne dla mojego systemu, więc będą to, jak je czasami nazywam, “wyjątki biznesowe“.
Co mi to daje? A chociażby to, że łapiąc taki wyjątek mogę zalogować wszystko co mi potrzebne, ale użytkownikowi wyświetlić taki komunikat jaki będzie dla niego zrozumiały.
I…
Dodatkowo czasem doprowadzam swoją wyjątkową praktykę do ekstremum i staram się, aby każdy wyjątek był rzucany tylko raz. Potraficie sobie to wyobrazić? Ile musi być tych klas wyjątków? Ano w tak zwaną pytę. I dobrze, dokładnie o to mi chodzi. Nie muszę wysilać się na jakieś wiele mówiące exception message. CO się stało – powie mi sam typ wyjątku. A towarzyszące temu okoliczności – czyli aktualne dane w tym obszarze systemu – przyjmowane są jako parametry konstruktora.
Na ich podstawie kleję najprostszego możliwego stringa, który wyląduje w logach. Użytkownik tego i tak nie zobaczy, bo… patrzcie wcześniej – typ wyjątku jest zmapowany na komunikat (w resx) wyświetlany użytkownikowi.
Te korzyści same w sobie są jak dla mnie wystarczające, ale to nie wszystko! Ile razy w testach sprawdzaliście, czy treść wyjątku zawiera jakieś odpowiednie informacje żeby upewnić się, że wyjątek leci z oczekiwanej ścieżki kodu? Z takiego InvalidOperationException niewiele poza tym można wyciągnąć. Moje testy znacznie się uprościły od kiedy stosuję opisywane podejście. Sprawdzam po prostu czy w odpowiednich okolicznościach leci odpowiedni typ wyjątku, to tyle.
A przykład z życia? Proszę bardzo!
Życie – dialer
Przed kilka miesięcy pracowałem nad “dialerem” – aplikacją do dzwonienia do ludzi i wciskaniu im produktów (z czegoś podobnego korzysta ten cholerny telemarketer, który w piątek wieczorem dzwoni do was z banku i próbuje przekonać że naprawdę potrzebujecie kolejnej karty kredytowej). A oto przykładowa hierarchia wyjątków (btw, chyba żadna inna część aplikacji nie ma tak rozbudowanego dziedziczenia:) ):
Najpierw mamy “abstract MyDialerException“. Potem wyjątek bazowy dla scenariusza “wykonywania połączenia”: “abstract DialingException: MyDialerException“. A poniżej już konkretne wyjątki, opisujące co poszło nie tak: CallAlreadyInProgressException : DialingException, IncorrectNumberException: DialingException, NumberAlreadyDialedException: DialingException… i jeszcze parę.
Jak ktoś zapyta “co w twoim systemie może pójść nie tak?” to wystarczy, że pokażę klasy dziedziczące z MyDialerException – i wszystko jasne.
Pozamiatane.
Custom exceptions | Maciej Aniserowicz o programowaniu…
Dziękujemy za dodanie artykułu – Trackback z dotnetomaniak.pl…
I super, ale jak masz jakiś argument, który wychodzi poza zakres, to tworzysz własny exception, czy nie? Bo dla biznesu nie widzę sensu robienia tego inaczej niż robisz.
pawelek,
To zależy od sytuacji. W podanym wyżej przykładzie “IncorrectNumberException” jest właśnie takim “out of range”. Każdy “klient” ma 1-3 numery tel i ten wyjątek leci wtedy, gdy ktoś próbuje zadzwonić do pod numer klienta 2 w “kontekście” klienta 1. Ale już “low-levelowe” sytuacje (jak podanie nieprawidłowej wartości enuma) to u mnie zwykłe frameworkowe wyjątki.
A mi to “pachnie” sterowaniem appką za pomocą wyjątków, czy rzeczywiście scenariusz że nr jest zajęty jest aż tak wyjątkowy żeby tworzyć CallAlreadyInProgressException? IMHO to jest złe użycie wyjątków. Bardziej mi to podpada pod eventy domenowe. Używanie wyjątków do sterowania wykonaniem appki (które są kosztowne choćby przez to że trzeba call stack wyciągnąć itp) ti IMHO antypattern.
BTW sam mam w swoich apkach BusinessException – ale używam go w rzeczywiście wyjątkowych sytuacjach i tylko wtedy gdy jestem w stanie “biznesowo” obsłużyć taki wyjątek.
Z nieco innej beczki – zamiast pisać jakie masz klasy lepiej to narysować ;) http://yuml.me – jest to idealne narzędzie do tego
To są przypadki które nie powinny nigdy wystąpić przy poprawnym działaniu aplikacji. Jeśli użytkownik już rozmawia z klientem, to przycisk “dial” powinien być disabled. Jeżeli mimo to user mógł zażądać kolejnego połączenia – to jest to wyjątek jak najbardziej (właśnie CallAlreadyInProgreeException – to nie oznacza że nr jest zajęty, ale nie wchodziłem tutaj w szczegóły). Jeżeli user jakimś cudem wywoła żądanie “zadzwoń do klienta X używając numer klienta Y” to też oznacza że “coś jest nie tak”. Jeżeli dany numer powinien być “disabled” a mimo to user pod niego dzwoni – to też jest to jak najbardziej wyjątek. To nie jest sterowanie, to są przypadki które przy 100% bug-free systemie nigdy by nie wystąpiły.
matma,
Kurde pamiętałem że coś takiego jest ale nie pamiętałem gdzie :) Dzięki, faktycznie może być lepsze na przyszłość.
Tylko jak przekonać zespół, żeby zgodził się na takie podejście jak o twoich mikrokontaktach nie chce słyszeć a na propagowanie pisania tak by się łatwo mockowało jest szczególnie oporny (helpery statyczne UBER ALLES)? :D
siararadek,
Jeżeli lektura posta nie przekonuje to nie potrafię inaczej :). Na szczęście ja nie mam tego problemu, chociaż zdaję sobie sprawę że to może być bolesne…
@Mirek – też nie jestem Exception Driven Development, ale o ile dobrze zrozumiałem to Maćkowi chodzi o to, że część osób przesadza z tym, żeby tego unikać. Dla mnie osobiście rzucanie własnych wyjątków jest ok o ile (tak jak napisał Maciek powyżej) rzucamy je gdy “w teorii” sytuacja nigdy nie powinna wystąpić, a boimy się, że w praktyce (zgodnie z prawami Murphy’ego) może. Nie ma sensu np. walidować coś co nigdy nie powinno się wydarzyć. To już wtedy jest robienie “kuloodpornych aplikacji”, co również nie jest dobrą praktyką.
Oczywiście jest to dosyć śliski temat, ciężko wyważyć proporcje. Ja osobiście tworzę swoje wyjątki, gdy klient zapewnia mnie, że punktu biznesowego taka sytuacja nigdy nie wystąpi i stwierdzam, że lepiej zastosować zasadę “kontrolowanego zaufania”… ;)
@Orkar: po uszczegółowieniu jest to już lepiej ale i tak bym zrobił jednak inaczej (uwaga pseudokod poniżej):
class Dialer {
…
public void Call(…) {
if (currentStatus == CalEstablished) {
DomainEvent.CallAlreadyInProgress.Publish();
return;
}
… tu implementacja Call…
}
Efekt ten sam a nie mamy wyjątków i event możemy dowolnie przetwarzać – np. wyswietlić uzytkownikowi komunikat żeby zdjąć kubek z kawą z przycisku wywołującego fizycznie nawiązanie połączenia, zalogowanie probu połączenia do logu itp.
Podejście z własnymi exception’ami jest jak najbardziej pożądane, ale ograniczył bym się do odpowienich typów wyjątków, a klasę bazową wyjątku rozszerzyłbym o pole (mógłby być enum) określający odpowieni case. W takim przypadku nie było by dodatkowych klas, lecz wystarczyła by klasa DialerException : MyApplicationException, a określenie czym błąd był spowodowany definiowało by pole z klasy bazowej. Oczywiście pozostaje kwestia konstruktorów przy Twoim podejściu gdzie prawdopodobnie przekazujesz odpowienią ilość argumentów w celu konstrukcji odpowiedniej wiadomości. W moim podejściu używam listy parametrów a konstruktor wyjątku pobiera odpowieni message z plików .resx.
Mirek,
To są scenariusze “zabronione” przez reguły biznesowe w domenie. Więc: wyjątkowe. Więc: wyjątek! :)
To nie jest zdarzenie “domenowe” bo domena tego nie przewiduje. To nie jest też sterowanie flowem, tu nigdzie nie ma catch czy jakiegoś ifa. W tym momencie żądanie się kończy a user widzi błąd i tyle.
Sławek,
Prawda, tak też można, ale ja wolę mieć więcej klas niż enum i to nie tylko w tym przypadku, a “w ogóle”. :)
Maciek, “zabronione przez reguły biznesowe w domenie” – czyli jak dla mnie jest to jak najbardziej część domeny. Co do sterowania flowem argument przyjąłem – spoko – po uszczegółowieniu nie mam nic przeciwko – po prostu jak dla mnie domain events dają tu większą elastyczność niż wyjątki – a i obsługa nie zawsze musi polegać na komunikacie dla usera :)
Wydaje mi się, że tak naprawdę wszyscy mówimy o tym samym i się zgadzamy, że sterowanie wyjątkami jest złe, o ile można to trzeba to właśnie zastępować takimi rozwiązaniami jak podał Mirek.
Ale wg mnie fajnie, że Maciek poruszyłeś ten temat, bo sam widzę często, że ludzie boją się używać wyjąśtków. Dużo osób próbuje robić kuloodporne aplikacje, wstawiać ify, dodawać nadmierne walidacje do sytuacji, które tak naprawdę powinny być obsługiwane wyjątkami. Zaśmieca to kod i utrudnia potem refactoring przy zmianie wymagań. Wyjątki od tego są, żeby je rzucać w sytuacji gdy następuje zdarzenie, które nigdy nie powinno wystąpić. Od tego są one… ;)
Tak – IMHO różnimy się tylko definicją sytuacji wyjątkowej :)
Oskar,
Pewnie że fajnie, bo dyskusja pod postem jak zwykle ciekawa ;).
Mirek,
Na to wychodzi. U mnie z reguły jakiekolwiek “definicje” nie są sztywne i nimi sobie manipuluję w zależności od sytuacji. Bo “question authority” ;).
Maciek, sure :) Tutaj to ja “question authority” – Twoje ;) A co do definicji to jak zawsze “it depends” :) Sam w wielu apkach tak wykorzystywałem wyjątki i nie jest to zła praktyka (dopóki nie wpadniesz w control flow) ale teraz Event Sourcing itp i wszędzie eventy widzę :)
Zainspirowany tym postem i komentarzami Mirka popełniłem blog post u siebie (uwaga: Ruby)
http://andrzejonsoftware.blogspot.com/2014/06/custom-exceptions-or-domain-events.html
Jak już jesteśmy w tym temacie, to mam pomysł na nastepnego posta – Best practices w tworzeniu custom exception. Jak Ty to widzisz? :)
Łukasz K,
Ale ja nie mam nic do dodania – widzę to tak jak tutaj napisałem:). Chyba że masz jakieś konkretne pytania?
Nie mam pytań. Dla mnie wszystko jest jasne. Myślałem bardziej o Twoich fanach czytających na codzień Twoje wypociny ;-)
A mnie ciekawi jakim to antypatterenm jest rzucanie wyjatkow w serwisach biznesowych (aplikacyjnych etc) – co takiego zlego jest w tym. W przypadku kodu gdzie dana akacje wykonujemy 2-100000 razy lub wiecej (w odpowiedzi na np jakas akcje uzytkownika) bedzie to zle/zbyt kosztowne, ale mowimy o sytuacji gdzie wyjatek pojdzie jeden raz na akcje uzytkownika – koszt pomijalny (zanim zaczniecie sie tym martwic – w 90% Waszych aplikacji znajdziecie wieksze problemy wydajnosciowe – i nie mowie ze to jest zle – raczej naturalne bo przy obecnych komputerach wydajnosc nie jest tak wazna jak kiedys). Ja osobisicie nie widze jakis szczegolnych wad wyjatkow w takiej sytuacji – moze bym je zauwazyl na rozmowie rekrutacyjnej gdybym wyczul iz druga osoba jest fanatykiem eventow ;) ale w normalnym uzytkownaniu nie widze jakis znacznych plusow ze stosowania rozwiazania zaproponowanego przez Mirka (wad tez nie – wazne aby w aplikacji stosowac jedno podejscie). Jeszcze na koniec odnosnie wyjatkow – nie chodzi mi o wyjatki, ktore nie maja prawa wystapic (blad aplikacji) – zalozmy ze uzytkownik chce przelac pieniadze z konta i podczas proby wykonania okazuje sie iz konto jest zamkniete (sytuacja jak najbardziej mozliwa).