Jak teda s těma výjimkama v busines logice?
Aleš Roubíček |
Na develu se rozvíjí debaty o tom, zda je zpátečnické odrazovat od vyhazování výjimek. Já patřím mezi zpátečníky. :) Tady je moje odpověď, kterou je IMO škoda nemít veřejně dostupnou, takže ji publikuju i tady na blogu.
Na začátek je dobré se opřít o nějakou autoritu, proto tady pro začátek ocituju .net Framework Design Guidelines doporučení k výjimkám:
- Nevracejte chybové kódy. K tomuto účelu jsou určeny výjimky.
- Vyhazujte výjimky pokud dojde k běhové chybě. Pokud člen nemůže úspěšně provést úkol za nějž nese zodpovědnost, mělo by to být považováno za běhovou chybu a měla by být vyhozena výjimka.
- Pokud se dostanete do stavu, kterému nepomůže žádné ošetření výjimky, proveďtě ukončení procesu pomocí
Environment.FailFast()
. - Nepoužívejte výjimky k formálnímu řízení toku programu. S výjimkou systémových selhání by vždy měl existovat způsob jak se vyhnout vyhazování výjimek.
- Zvažte důsledky vyhozené výjimky na výkon.
- Vždy zdokumentujte všechny výjimky vyhazované veřejnými členy z důvodu porušení jejich kontraktu.
- Nemějte veřejné členy, které mají nebo nemají vyhazovat výjimky na základě nějakého parametru.
Type GetType(string name, bool throwOnError)
- Nemějte veřejné členy které vracejí výjimku jako návratovou hodnotu nebo out parametr.
- Nastavte všechny relevatní vlastnoti vyhazované výjimky.
- Preferujte užití systémových výjimek před vlastními.
- Vlastní výjimky vyhazujte v případě, že vyžadují odlišný způsob ošetření než existující typy výjimek.
- Nikdy nevytvářejte vlastní typ výjimky, jen proto, aby váš team měl nějakou vlastní výjimku.
- Vyhazujte co nejkonkrétnější výjimky.
- Nevracejte chybové kódy z obav o výkonostní dopady při vyhazování výjimek.
- Používejte vzory jako TryParse, které snižují negativní dopady na výkon způsobené vyhazováním výjimek.
Z texu je zřejmé, že vyhazování výjimek má své opodstatnění. Důležité je však v jakých scénářích se výjimky vyhazovat mají a v jakých ne.
Často se tu objevují příklady s validací vstupních hodnot nebo třeba autentikace uživatele. Začněme validátorem:
Primární zodpovědností validátoru je validovat hodnoty. Je nevalidní hodnota příčinou nemožnosti korektní práce validátoru (důvodem k vyhození výjimky)? Není (viz bod 2
). Můžeme to řešit vracením primitivy jako je bool
. Já si však myslím, že taková hodnota je nedostatečná. Potřebujeme výsledek validace zpropagovat někam, kde se s ním bude dále pracovat. Vyhodíme tedy výjimku a tu pak odchytíme? Tím se ale dostáváme k bodu 4
. Lepším řešením je definovat komplexní návratový typ:
type ValidationResult =
| Valid
| Invalid of ValidationError list
Pokračujme s autentikací uživatele. Běžnou praxí je vyhazování SecurityException
pokud se nepovede uživatele autentikovat. Výjimka se pak nechá probublat do UI vrstvy, kde se ošetří a uživateli se zobrazí hláška, že zadal špatné jméno nebo heslo. Opět však jde o porušení 4. bodu. Autentikace tam může vracet opět bool
nebo rovnou instanci User
a v případě nevydařené autentikace null
. Tím se zbavíme bezpečnostních výjimek, ale zase si zavedeme potenciální chybu v podobě NRE
… Proto si opět myslím, že by se měl vracet adekvátní typ:
type AuthenticationResult =
| NotAuthenticated
| Authenticated of User
Autentikační modul samozřejmě může vyhazovat výjimky, ale většinou budou spojené s nefungující infrastrukturou (LDAP, SQL nebo jinej auth provider), to je totiž validní důvod, proč nemůže autentikace doběhnout.