Nemám rád kontrolované výjimky

Už dlouho jsem nikoho nepoučoval a trochu mi to chybí, takže dneska budu psát o kontrolovaných alias synchronních alias čtverečkovaných alias checked výjimkách. Ano o těch výjimkách, které jste překladačem nuceni odchytit a zpracovat.

Teď pracuji s pěkně starým kódem. Jeho autoři si mysleli, že jsou kontrolované výjimky dobrý nápad, takže je hojně využívali. Já se jim nedivím, na první pohled se líbily i třeba Bruce Eckelovi. A nejen jemu, spousta důležitých knihoven v Javě je také hodně používá. Jenže já je nemám rád. Ukažme si to na příkladu ze života.

Představme si že píšeme finanční aplikaci která má být hyperrobustní. Vytovříme si třídu Money, do které budeme ukládat peníze. A to nejen množství, ale i měnu. Už při vytváření instance může nastat chyba. Filuta uživatel mé skvělé peněžní knihovny se mi může snažit podstrčit neexistující měnu. Jenže já nejsem žádný jakýpak copak, já mu na to přijdu a vyhodím mu CurrencyException. A abych mu ukázal, že se to může stát, tak udělám CurrencyException kontrolovanou (checked). Takže on musí počítat s tím, že mu ji vyhodím a správně ji zpracovat. Skvělé. Dokonce mohu i napsat metodu add, která si bude kontrolovat, jestli sčítám stejné měny a když ne, vyhodí mojí oblíbenou CurrencyException.

	public Money(float value, String currency) throws CurrencyException {
		//value should be BigDecimal, float is used for simplicity
		validateCurrency(currency);
		this.value = value;
		this.currency = currency;
	}
	
	public Money add(Money other) throws CurrencyException
	{
		if (!other.getCurrency().equals(getCurrency()))
		{
			throw new CurrencyException("Currencies do not match.");
		}
		return new Money(getValue() + other.getValue(),getCurrency());
	}

Takže mám super kód, který nutí ostatní programátory zabývat se tím, co se stane, když se pokusí míchat americké dolary a polské zloté. Ať žijí kontrolované výjimky! To si ovšem budeme říkat jen do doby než začneme tuto třídu používat. Nebo než uvidíme kód který ji používá. Setkáme se s podobnými perlami:

		Money money1 = null;
		try {
			money1 = new Money(123, "EUR");
		} catch (CurrencyException e) {
			//can not happen
		}
		Money money2 = null;
		try {
			money2 = new Money(121, "EUR");
		} catch (CurrencyException e) {
			log.log(Level.SEVERE, e.getMessage());
		}
		Money money3;
		try {
			 money3 = money1.add(money2);
		} catch (CurrencyException e) {
			throw new IllegalStateException(e);
		}

Při vytváření instance money1 si programátor zjevně myslel, že výjimka nemůže nastat. Vždyť tam má to euro natvrdo. Tak ji ignoruje. Tento případ je ze všech nejhorší. O trochu lepší případ nastane, když výjimku zapíše do logu jak to udělal v případě money2. Ale i tak, pokud při vytváření money2 dojde k výjimce, program bude pokračovat jen proto, aby o kousek dál skončil s NullPointerException. To je ta lepší varianta. V horším případě k NPE dojde o velký kus dál a my si budeme marně lámat hlavu, jak se nám tam dostal null. Asi nejlepší ze špatných řešení je použito u money3. Ali i to je špatně čitelné a ošklivé. Už chápete proč nemám rád kontrolované výjimky? Pokud by CurrencyException byla potomkem třeba IllegalArgumentException, kód by vypadal takto:

		Money2 money1 = new Money2(123, "EUR");
		Money2 money2 = new Money2(121, "EUR");
		Money2 money3 = money1.add(money2);

Nejen že to je lépe čitelné, ono je to i mnohem bezpečnější. Pokud dojde k té nepravděpodobné události a něco se pokazí, dozvím se o tom ihned. A pokud výjimku neodchytím někde o kus dál, tak se o tom dozví bohužel i uživatel. Ale i to je mnohem lepší, než aby se aplikace tvářila jako že funguje.

Pokud vám uvedený příklad připadá umělý, podívejte se na konstruktor java.net.URL nebo na jakýkoliv JDBC kód. Nebo klasický příklad s kódováním.

		Writer writer = null;
		try {
			writer = new OutputStreamWriter(out, "UTF-8");
		} catch (UnsupportedEncodingException e) {
			//can not happen 
		}

Co se stane když vaše JVM nebude znát UTF-8? Správně, dostanete NullPointerException. S trochou štěstí po pár minutách debugování zjistíte co se děje. Když štěstí mít nebudete, tak vám to může zabrat i den.

Takže abych to shrnul. Kontrolované výjimky se mohou používat v jediném případě. Pokud jsem si jist, že programátor který ji odchytí bude vždy vědět co s ní má dělat. Což je asi důvod proč v C# kontrolované výjimky vůbec nejsou. Na celé záležitosti je navíc matoucí to, že ve skutečnosti máme opravdu dva druhy výjimek.

Jeden druh je obvykle způsoben špatnými vstupními daty. V takovém případě má smysl vyhazovat kontrolovanou výjimku a donutit programátora ji nějak zpracovat.

Jenže pak tu máme chyby způsobené chybou v kódu, konfigurací, selháním sítě, disku a podobně. V takovém případě nemá cenu výjimku obvykle chytat, protože beztak nevíme co s ní.

To by napovídalo, že pro chyby prvního typu bychom měli používat kontrolované výjimky, pro chyby druhého typu běhové (runtime) výjimky. EJB specifikace tak dokonce funguje. Podle typu výjimky dělá nebo nedělá rollback transakce (pokud ji neřekneme jinak).

Finta je v tom, že dopředu nikdy nevíme, ke kterému druhu chyby může dojít. Vraťme se k příkladu s penězi. Může se stát, že nám špatná měna přijde z jiného systému v XML. Pak má smysl vrátit specifickou chybovou hlášku a používat kontrolovanou vyjímku. Stejně tak může být měna načtena z konfiguračního souboru. Pak obvykle nemá smysl se výjimku nějak speciálně zabývat, aplikace je prostě nefunkční. Hláška v logu je naprosto dostačující.

Takže opakuji, nikdy dopředu nevíme jestli bude moci programátor používající náš kód umět výjimku zpracovat. Pravděpodobnější je, že to nedovede a provede s ní něco ošklivého. Proto se přikláním k názoru, že by kontrolované výjimky měly být používány zřídka, nejlépe vůbec. Běhové výjimky jsou bezpečnější a kód je s nimi mnohem čitelnější. Nicméně pokud má někdo nějaký příklad, kde si je jist, že by měly být za všech okolností použity kontrolované výjimky, sem s ním, rád se poučím.

Zdroje (aneb důkaz, že nosím dříví do lesa):

Does Java need Checked Exceptions?

Java theory and practice: The exceptions debate

27 thoughts on “Nemám rád kontrolované výjimky

  1. v6ak

    Já jsem se s tím taky setkal. Třeba u escapování URL. V těchto příkladech ale se mi nelíbí z objektového hlediska jedna věc: měna nebo kódování je String a to jsme prosím v Javě, ne v PHP. Líbilo by se mi, kdyby existovaly třídy jako Encoding nebo Currency. Pak by stačilo výjimku zachytit při vytváření měny/kódování, ne při používání. Uvažoval jsem i o nějaké implementaci tohoto nápadu (s využitím nejlepšího špatného řešení), zatím jsem to však tolik nepotřeboval.

  2. Ra100

    Ako tak pozeram na tie priklady, tak je jasne, ze spracovanie vynimky je zle ale nie jej vyhodenie ( a deklarovanie ).
    Ked pisem daku kniznicu, nemozem predpokladat, ze jej uzivatel je blbec a preto aj moj kod musi byt blby ( teda niektore firmy to robia, ale s nimi sa nekamaratime ).

    V ziadnom pripade by som nepresiel spracovanie vynimky mlcanim typu //can not happen . Jedna sa predsa o vynimocnu a teda dolezitu a musim na to reagovat. Existuje len par situacii, napriklad zatvaranie connection – co uz ked je zavreta a ja som sa ju pokusil este raz zavriet – ked mozem vynimku ignorovat a v tedy ju zasadne nazyvam “ignored” .

    Takisto obalovanie vynimiek ma svoje pravidla a treba ich dodrziavat.

  3. Slavek Tecl

    Rekl bych, ze na kazdem miste se zminuje jak catch blok pouzivat a co je vzdy zmineno, je nedelat ho prazdny. Navic, samotna vyjimka by mela byt pouzivana rozumne, tim se ovsem dostavame spis do problemu v navrhu aplikace nez k vyjimkam samotnym. A jestli nebude umet programator nas kod zpracovat, tak nema na svem miste co delat ;).

  4. Petr Charvat

    Nejde mi to abych nezareagoval.
    S autorem musim jen souhlasit. Chedked exception jsou jako rakovina, ktera prorusta kodem (vrstvami) a neni jasne, kdy je chytit a co s nimi udelat (krome zalogovani).
    Nehynouci slava springu, ktery vetsinu checked ex prevadi na sve runtime – napr. SQLException.

  5. Dagi

    “A jestli nebude umet programator nas kod zpracovat, tak nema na svem miste co delat”

    O tom to neni, vetsina vyjimek je takoveho charakteru, ze na dane urovni abstrakce uz na ne proste nema sanci programator zareagovat. Takze to neni o tom, jestli se tam udela try/catch blok, ale o tom, jestli ma smysl jej tam delat. Kontrolovane vyjimky maji smysl pouze a jenom pro osetreni nevalidniho vstupu uzivatele.

  6. eestwood

    taky nemam z pouzivani checked exceptions dobry pocit, dobry clanek, poslouzi mi jako argument pri potirani CE

  7. lzap

    Ono přitom stačí trošku pozměnit návrh. V tomto případě bych vyhradil pro měnu samostatnou třídu, která by použila návrhový vzor Originál a vracela by instance měn. Teprve ty by používala třída Money. Pak k podobným “perlám” nemůže dojít a kontrolované výjimky tolik nemůžou vadit.

    Každá třída má mít na starosti jednu věc. V tomto případě Money zapouzdřuje měnu, ale také se stará o kontrolu vstupu od uživatele a zapouzdřuje měnu. Kompozice dvou tříd je v tomto případě více žádoucí.

    Tenhle příklad se mi prostě zdá příliš uměle vyrobený, aby byl proti kontrolovaným výjimkám. Bohužel se s tímto setkáváme i v Java Core API. Ale to už jsme si jaksi zvykli, že ano.

  8. Slavek Tecl

    Souhlasim, na tom prikladu to totiz prilis jasne nevyzniva (hlavne treba co se tyce abstrakci).

    Pokud nema programator sanci tu vyjimku zpracovat, to uz je jina vec. Hlavni proti ve smyslu nepouzivani CE ma vyznam pri pouziti nekterych navrhovych vzoru, kde vlastne neni jasne a ani mozne definovat univerzalniho predka vyjimky. V tom pripade je opodstatnene pouzit zminene tunelovani vyjimek.

  9. Lukáš Křečan Post author

    To v6ak and Izap: ano třída Money může být lépe navržená, je to jen příklad. S příklady je potíž v tom, že buď jsou jednoduché, takže nejsou dobře navržené, nebo jsou dobře navržené, ale pak z nich nikdo nepochopí co chtěl autor ukázat.

    To Slave Tecl: špatně jsem to formuloval, tím že programátor nebude umět výjimku zpracovat jsem myslel takovou situaci, kdy na místě kde ji chytí nebude vědět co s ní. Například proto že je ve vrstvě, kde nemá šanci komunikovat s uživatelem, i kdyby si náhodou myslel, že to uživatele bude zajímat. Jediné co může udělat je ji buď zalogovat, nebo zabalit a vyhodit jako jinou vyjímku.

  10. finc

    Vetsinou pouzivam jednoduche pravidlo:
    Nezadouci stav, ktery dokaze vyresit uzivatel pouziva kontrolovane vyjimky

    Nezadouci stav, ktery dokaze vyresit jen programator ci spravce obsahuje nekontrolovane vyjimky.

    Jinak s autorem clanku souhlasim. Ovsem nekontrolovane vyjimky sebou prinaseji take nezadouci vlivy a tim je samotny fakt, ze signatura metody mi o mozne nekontrolovane vyjimce vubec nerekne. Takze, vzdy, kdyz moje metoda muze skoncit nekontrolovanou vyjimkou, stejne vzdy zapisi throws klausuli.

    Co se tyce toho EJB, tak vychozi stav je takovy, ze nekontrolovana vyjimka vzdy provede rollback, kdezto kontrolovana nikoli (pokud ji sam o to nepozadam ci neanotuji na rollback=true).
    Ten priklad s EJB je zrovna pripad, kdy vyvojar tezko bude z rollback vyjimky neco dolovat ci vubec zachranovat pad casti aplikace. Vetsinou to jde pres tolik vrstev, ze k takovemu stavu by proste dojit nemelo 🙂

  11. v6ak

    Já bych použil jiné pravidlo: pokud je možné výjimce předejít návrhem, má být nekontrolovaná. Pokud jí nejde předejít návrhem, měla by být kontrolovaná.
    Podle tohoto do nekontrolovaných výjimek nepatří síťové problémy.
    Souhlasím taky s tím, že pohlcování výjimek je zpravidla zlo, dá se přirovnat použití zavináče v PHP bez jakékoli další kontroly. Zavináč v PHP má tu nevýhodu, že pohlcuje všechno, takže když jsem upravoval jednu cizí non-E_STRICT-safe třídu a překlepl jsem se v názvu, aplikace skončila bez jediné hlášky, protože volání bylo pečlivě zazavináčované.

  12. Lukáš Křečan Post author

    Ty síťové problémy jsou zajímavý příklad. Co má být v catch bloku někde hluboku v aplikaci, když dojde k síťovému problému. Například selže připojení k databázi v DAO vrstvě?

  13. benzin

    P.S.: Jak cato vidate finnaly bez catch, pred nim? Ja temer nikdy. Vetsinou kontrolovane vyjimky donuti vyvojare zamyslet se nad tim, ze by tam mel byt finally blok. Pokud by vsude byly jenom nekontrolovane vyjimky, dost se obavam, ze by zustalo spousta spojeni nerzusena.

  14. podlesh

    Celý problém je mnohem obecnější a větší: při vývoji by se měl programátor zamyslet nad tím, jaké výjimky generuje a jak se výjimky zpracovávají. Výjimky by měly být dobře dokumentované a měla by existovat pravidla, jak s nimi zacházet.

    Proto se mi checked exceptions velmi líbí a jsem rád že v Javě jsou – nutí člověka přemýšlet. Samozřejmě, spousta lidí si pod “přemýšlením” představí “chytnu, zaloguju, jedu dál, ještě mám spoustu práce před sebou”. Druhá věc pak je, že se špatný návrh často neřeší a celý projekt se lepí a flikuje a bastlí… Jedním z varovných příznaků v Javě je onen zmiňovaný “mor” (lepší termín asi je “rakovina”), kdy se výjimky prorůstají všude. Je to jen příznak jiných, závažnějších problémů.

    Mnou používané a doporučované pravidlo zní: naprostá většina výjimek má být unchecked (Runtime), to je rozumný default. Checked výjimky použít jen tam, kde ji musí volající IHNED zpracovat. Checked výjimka se NESMÍ prostě nechat “probublat” – tím se celý smysl jejich existence ruší (samozřejmě neplatí pro wrappery, pomocné metody a podobně).

  15. v6ak

    No pokud mám nějaké DAO, ve kterém může z nějakého důvodu vzniknout chyba, mělo by to být v dokumentaci, aby to mohl programátor nějak zpracovat. (Může to být i obálka.) Je to jeden z důvodů, proč jsem při experimentech s ORM se rozhodl načíst vždy vše (možná kromě blobů) – aby se programátor nemusel zatěžovat s výjimkami u každého getu. Ve Viewu by to bylo dost otravné, když by musel zajistit integritu HTML výstupu. Mám pro to i další důvody, ale to je teď vedlejší.
    Se zpracováním chyb jsem začal dřív než jsem znal výjimky. Začal jsem v PHP4. (Výjimky má až PHP5.) Funkce vracela buď normální výsledek, nebo instanci jedné třídy, což znamenalo, že nastala chyba. Už tady jsem pracoval s řetězením chyb (výjimek, i když toto nebyly pravé výjimky a ani jsem je tak nenazýval). Potom jsem se setkal s výjimkami v PHP5, což znamenalo zavržení svého nepohodlného systému. Tady jsem několikrát přehodnotil výjimkovou politiku. Chvilku to mělo vypadat i tak, že kdokoli má právo kdykoli na výjimku. Ale webová aplikace běžící na serveru má určitá specifika. Když nastane jakákoli chyba, dozví se to programátor a ten rozhodne, zda se něco pokazilo mimo skript (DB spojení, …) nebo ve skriptu (SQL syntaxe). Uživateli řeknu jen nějaké SRY. Což si třeba u desktopové aplikace nemůžu dovolit – uživatel by měl být o chybě podrobně informován, pokud za ni může. A pokud za ni nemůže, měl by dostat možnost poslat chybu výrobci, nejlépe s vlastním komentářem.

  16. v6ak

    Ještě mě napadla jedna šílenost. Není to sice moc čisté, ale někdy může jít o ono nejlepší špatné řešení. Jde o situaci, kdy mám rozhraní, které nepočítá s problémy, ale já s problémy počítat musím.
    Pak poskytnu nad rámec rozhraní metodu, která umožní nastavit Strategii ( http://objekty.vse.cz/Objekty/Vzory-Strategy ) chování při chybách. Může se to projevit i dotazem v UI. Pokud Stragegie nazná, že se na to mám vykašlat, pak vrátím mulu, prázdnou kolekci nebo něco podobného. Jak říkám, není to čisté, ale někdy se to může hodit.

  17. Lukáš Křečan Post author

    To v6ak:
    Určitě to je použitelné řešení. Má dvě nevýhody.
    a) Když nastane chyba, tak často potřebuju přerušit vykonávání programu a vrátit se do nějakého rozumného stavu. Přesně to mi vyjímka umožní, kdežto strategie (nebo listener) ne.
    b) Lidé nejsou zvyklí to používat. Pamatuji si na jednu IBM knihovnu, která používala podobný mechanizmus. Hrozně dlouho mi trvalo než jsem přišel na to jak zjistit kde se stala chyba.

  18. v6ak

    To Lukáš Křečan: Jasně. Říkám, že to není čisté řešení, ale může být čistější než obalování výjimek. Použitelné je to hlavně tehdy, když mi objektový model celkem mapuje UI.
    Listener může být tesán do kamene a můžu zjistit stacktrace. (Nevím přesně jak, ale přinejhorším pomůže výjimkový hack. Prípadně ožná budu mít k dispozici výjimku, takže v ní budu mít stacktrace.)

  19. Jirka

    No nevim … ten priklad s Money hlavne vubec neukazuje, proc by ‘kontrolovane’ vyjimky meli byt spatne. 1/ Uz samotne pouziti kontrolovanych vyjimek v tomto kontextu je spatne. Pouzil bych, jak sam pises, IllegalArgumentException nebo jeji rozsireni. Zde se proste pouziti jine, nez Runtime vyjimky nehodi. Neznamena to ale, ze by kontrolovatelne vyjimky byli spatne, ale pouze mas spatny priklad. 2/ Vyjimka se nemusi odchytit hned na miste jejiho mozneho vyskytu. Pokud tu tvrdis, ze kod je neprehledny a jsou v nem diry typu moznost vzniku NPE, tak mas samozrejme pravdu, ale neznamena to, ze by kontrolovatelne vyjimky byli spatne, ale ze je v prikladu blbe pouzivas.

  20. v6ak

    To Jirka: On problém, jak autor píše, právě v jejich použití, ne v kontrolovaných výjimkách samotných. Jen autor je trošku radikálnější…

  21. Jira

    Obávam se unchecked exceptions nejsou ideálním řešením, protože přinášejí řadu dalších problémů, např. jaké výjimky můžou kde vzniknout. Nezapomenu na časy Delphi, kde byly unchecked exceptions a já nikdy nevěděl, co to může nebo nemůže vyhodit.

    Unchecked exceptions hrají do noty takovým těm hnusným okýnkům, kde je pro uživatele totálně nesrozumitelná hláška vytažená z výjimky a čeká se jak se rozhodne.

    Checked exceptions nutí programátora k přemýšlení (nebo k odchycení Throwable a pryč od toho), rozumnému přebalování výjimek a pod. Spíš bych řekl, že je problém v catch bloku a zpracování výjimek než v tom, zda jsou checked nebo ne.

    Být líný (tj. unchecked exceptions) není vždy ideální. Nelíbí se mi ani současná podoba checked ani současná podoba unchecked exceptions, ale ty checked mi někdy přijdou menší zlo.

    Nechci mít kód, kde budu ve view zpracovávat SAXException, CastorException, IOException, HibernateException a bůh ještě jaké, protoyže jsou všechny unchecked a programátoři na nižších vrtvách se vyflákli na jejich zpracování …

  22. Franta

    no, hodně provokativní zápisek 🙂

    Utopit výjimku a nepropagovat ji do vyšších vrstev (nebo aspoň do logu) je prostě prasárna – což ale neznamená, že (kontrolované) výjimky přestaneme vyhazovat.

    Osobně mám systém výjimek v Javě dost rád*, protože už jen když jde program zkompilovat, vím, že mám velkou část výjimečných stavů ošetřenou. (i když lenost člověku říká, že bez všech těch try {} cath by to bylo snazší 🙂

    *) naopak způsob, jakým k výjimkám přistupuje C# se mi zdá nešťasný a byl jedním z důvodů, proč jsem na něj rezignoval.

  23. Franta

    ad Jirka:
    +1
    Přijde mi, že používáním nekontrolovaných výjimek se dobrovolně připravujeme o informaci (které výjimečné stavy se mohou objevit). A pak to končí těmi ošklivými okýnky u uživatele.

    Když se někdo nechce ztrácet v try {} catch blocích, nemusí, stačí k metodám přidat “throws” – výjimka vyletí až nahoru jako u nekontrolovaných, ale s tím rozdílem, že víme, které stavy mohou nastat.

  24. Lukáš Křečan Post author

    To Franta: Tak s tím posledním příspěvkem hodně nesouhlasím. To by znamenalo, že bych měl mít v signaturách metod prezentační vrstvy SQLException, XMLException atp. To mi nepřipadá jako dobrý nápad.

  25. v6ak

    “Obávam se unchecked exceptions nejsou ideálním řešením, protože přinášejí řadu dalších problémů, např. jaké výjimky můžou kde vzniknout.”
    Nesouhlasím, na to tu je JavaDoc. Mrkni se někdy do nějakého docu a uvidíš, že tam je throws dvakrát a někdy odlišně – jednou bez nekontrolovyných výjimek.

  26. Adam Hošek

    Problém vidím spíše v tom, že programátoři neumí s výjimkami zacházet. A je jedno, jestli jsou kontrolované nebo ne. Pokud ne, mají tendenci na ně “zapomínat,” pokud ano, naopak se jejich ošetření snaží vyhnout jako čert kříži. Každý dobrý programátor by se podle mě měl naučit nějaké good practices, ale hlavně by měl pochopit, k čemu výjimky jsou, a nebát se jich. Nevím teď, jak přesně se k tomu staví Joshua Bloch v Effective Java, ale kromě obecných rad, jak na to, se mi líbí ještě tato varianta:
    Z vrstvy pode mnou jde jen jeden typ výjimky, respektive několik výjimek dědících od jedné, která je zastřešuje. Pak se nemusim o moc starat. Pokud vím, co takovou výjimku způsobuje (dle dokumentace), třeba jsem schopen s tím něco dělat, nebo nejsem, a pak jí vyhodím výš, ale obalenou vlastní obecnou výjimkou, případně jako obecnou runtime.
    Vždy je dobré snažit se udržet možnou tvorbu výjimek v úzkém prostoru, protože se pak snadno zpracovávají. Vždy ale propaguji výjimku výš, pokud nevím, co s ní.
    V naší aplikaci se všechny dříve nezachycené výjimky chytají v controlleru. Validační a servisní obsahují lokalizované textíky a vše ostatní je prostě neočekávaná chyba (ty se samozřejmě logují).

Comments are closed.