No exceptions made

 
20 juli 2010

Naar aanleiding van een bevinding tijdens een interne project code review  en een artikel in het laatste Java Magazine hadden we een interessante discussie over de redenen om exceptions toe te passen. Uiteindelijk kon ik zelf achter twee vuistregels staan, één die ik zelf bedacht had, de ander van een collega.

Ik ga bij de onderstaande vuistregels uit van de context van een ontwikkelplatform die de mogelijkheid biedt om ze te gebruiken en ontwikkelaar die als basisregel voor het herkennen van de situatie om een exception in te gebruiken de volgende regel hanteert: gebruik een exception als je niet het normale executiepad meer kunt volgen, met andere woorden, als er iets uitzonderlijks gebeurt of is gebeurd.

Aanvullende vuistregels
Op zich een logische veronderstelling, aangezien de naam Exception in het Nederlands ook letterlijk vertaalt naar Uitzondering. Toch is dit te naïef en horen daar volgens mij de volgende vuistregels bij:

1. Gebruik geen exceptions en als je toch een reden gevonden denkt te hebben om een exception te gebruiken, denk daar dan nog eens goed over na.

2. Gebruik geen exceptions in een (objectgeoriënteerd) domeinmodel.

Onderbouwing
Uiteraard verdienen deze vuistregels wel enige onderbouwing. Ik zie de volgende redenen:

Ten eerste is het gooien van een Exception niet zonder kosten:

  • De runtime engine heeft vaak wat extra execution time nodig voor  het opgooien en afhandelen van een exception
  • Een exception doorbreekt het normale executiepad, zeker als deze niet direct door de aanroepende methode wordt afgevangen. Dit betekent dat invariants niet zonder meer afgedwongen worden. Je zult dus een soort transactioneel mechanisme nodig hebben om de situatie te herstellen , anders moet je ervan uitgaan dat de runtime corrupt is en weggegooid dient te worden.
  • Het betekent een extra return mogelijkheid uit een methode, een extra pad om te volgen. Het verhoogt de complexiteit van een methode
  • Het try-catch mechanisme dat noodzakelijk is om een exception correct af te vangen kost meerdere regels code en leidt af van de werkelijke werking van de code.
  • Het betekent in feite dat een door een gebruiker of andere actor geïnitieerde actie niet voltooid kon worden. In het beste geval kan het nog een keer geprobeerd worden, in het slechtste geval moet de initiator een nieuwe poging doen.

Bovenstaande kosten leiden met name tot vuistregel 1. Als je een exception kunt voorkomen, everything else being equal, dan verdient dat de voorkeur. Er zijn denk ik een aantal situaties waarin het logisch lijkt om een exception te gooien, terwijl er betere alternatieven zijn:

Pre-condities op argumenten kunnen beter declaratief vastgelegd worden
Je zou bijvoorbeeld een exception kunnen gooien als de argumenten waarmee de een call gemaakt wordt, niet voldoen aan de gestelde eisen. Met andere woorden: voor het afdwingen van pre-condities op de argumenten. Dit is echter veel beter te doen door deze eisen expliciet te maken en in een declaratieve manier in je methode signature te verwerken. Door argumenten te verwachten die alleen in een valide toestand kunnen zijn, dwing je de aanroepende methode om zelf te zorgen voor een juiste aanroep. Als deze aanroepende methode dit vervolgens ook doet, dan borrelen deze pre-condities naar boven, zodat ze declaratief en pre-conditieel zijn op de externe interface van de unit of code die je op dat moment bewerkt.

Met andere woorden: ze zijn in de vorm van een (code) contract vastgelegd. Als deze externe interface bijvoorbeeld een web service is en het contract vastligt in xml schema, kan een aanroep voor wat betreft pre-condities al afgekeurd worden door de schema validator, zonder dat de achterliggende runtime aangeroepen wordt en dus zonder dat die in een invalid state terecht kan komen.

Pre-condities op de interne state kunnen beter uitgewerkt worden
Je zou bijvoorbeeld kunnen zeggen dat een bepaalde aanroep alleen bij een bepaalde inner state mag plaatsvinden. Om een voorbeeld te gebruiken wat tijdens de discussie naar boven kwam: een lesmodule bevat slides. De module mag alleen weggegooid worden als alle slides reeds verwijderd zijn. Door de eis zo te stellen,  zorg je ervoor dat de aanroepende methode deze state moet kunnen bepalen (je raakt dus encapsulatie van state kwijt) en bovendien blijft de mogelijkheid van race condities bestaan: er is net een slide toegevoegd voordat je de module wilt gaan verwijderen.

Beter is om in zo’n situatie op zoek te gaan naar nieuwe mogelijkheden. Start daarvoor in dit geval met de vraag: waarom moeten die slides eigenlijk eerst verwijderd worden? Stel dat het antwoord in dit geval is: omdat slides mogelijk ook door andere modules gebruikt kunnen worden en als ze dus zomaar mee verwijderd worden, kan een gebruiker onbewust de inhoud van een andere module verwijderen.

Over het algemeen blijkt dan dat er betere alternatieven zijn. In het bovenstaande geval wordt eigenlijk duidelijk dat slides losstaande aggregates zijn en dat deze dus überhaupt niet meeverwijderd zouden moeten worden met de lesmodule die ze gebruikt, maar dat alleen de link ernaar verwijderd mag worden. Aanvullend zou je een extra mogelijkheid moeten aanbieden om zwevende slides automatisch of handmatig op te zoeken en te verwijderen (garbage collection).

Invariants op de interne state kun je beter afdwingen door locks te gebruiken of geen threading
Op het moment dat de executie reeds gestart is en de interne state door een andere thread tegelijkertijd gewijzigd wordt, is de beste manier om hier bescherming tegen te bieden met het gebruik van locks: dit is namelijk preventief en zorgt er in principe voor dat beide executiepaden vervolgd kunnen worden. Locks hebben uiteraard ook weer nadelen (onduidelijke code, deadlocking) Het gebruik van het actor model zorgt er natuurlijk voor dat je al deze problemen bij de wortel aanpakt.

Overblijvende use case: pre-condities en invariants op de state van de externe omgeving
Uiteraard blijft dan nog wel een bepaalde situatie over voor het gebruik van excepties: de state van de (externe, niet-beïnvloedbare) omgeving voldoet voor of tijdens de executie niet aan alle condities. Het canonieke voorbeeld hier is natuurlijk de database die niet up is voor of down gaat tijdens de uitvoering van een request. Dit kan een goede reden voor het gooien van een exceptie zijn. Uiteraard is door het ruimer nemen van de verantwoordelijkheid hier ook wel iets aan te doen: bijvoorbeeld door als verantwoordelijkheid te nemen dat de huidige runtime state uiteindelijk gesynchroniseerd moet worden met de server  i.p.v. dat deze altijd direct bij aanroep gesynchroniseerd moet worden, is het niet up zijn van de netwerkverbinding niet direct een reden tot het gooien van een Exception meer.

Een (goed) domeinmodel heeft geen afhankelijkheden naar een externe omgeving
Aangezien de externe omgeving eigenlijk alleen een reden zou moeten zijn voor het gooien van een Exception, volgt eigenlijk vanzelf dat een domeinmodel geen enkele reden zou moeten hebben om een Exception te gooien. Deze zou namelijk nooit afhankelijk moeten zijn van een externe omgeving en puur een representatie moeten zijn van de werkelijke omgeving buiten het model, gezien door de lens van het betreffende domein. Die is niet nooit afhankelijk van een database of netwerkverbinding in het systeem waarin het model zich bevindt (zou natuurlijk wel kunnen dat het domein zelf over databases en netwerkverbindingen gaat, maar dan zou het dus ook gaan om domeinobjecten in het model).

Conclusie
Door de bovenstaande vuistregels voor het toepassen van Exceptions te gebruiken, zijn systemen robuuster, eenvoudiger, sneller en bruikbaarder te maken. Uiteraard is het werk dat gepaard gaat met beter modelleren ook een kostenpost en het kan zijn dat dit zich niet altijd uitbetaald, maar binnen het hart van je applicatie, het domeinmodel, zou dit toch wel het geval moeten zijn. Tenzij je applicatie als geheel weinig toegevoegde waarde heeft, maar dan is het misschien sowieso geen goed idee om eraan te beginnen.

An english version of this post is available at http://www.rickvanderarend.nl


Werken met ?
Kijk dan bij onze mogelijkheden voor zowel starters als ervaren engineers.


Categorieën: Architectuur, Development

Tags: , , , , , ,


Reacties (10)

  • Matthijs Mast schreef:

    Er schiet mij direct nog een vraag te binnen.

    Is de keuze afhankelijk van externe data en voldoet het model dan nog wel?

    Geplaatst op 05 augustus 2010 om 7:32 Permalink

    • @Matthijs – op zich een terechte vraag. Ik wil wel nog even opmerken dat ik me met de ‘wie’ in mijn eerste vraag zeker niet wilde beperken tot een onderdeel van het systeem wat je aan het ontwerpen bent. Het kan ook een gebruiker, afdelingsmanager, of een extern systeem zijn, bijvoorbeeld.

      Geplaatst op 05 augustus 2010 om 8:19 Permalink

  • Hoi Peter,

    Je ziet dit soort excepties vrij vaak, een switch op een enumeratie wordt natuurlijk niet door de compiler gechecked en dan wil je dus een (backup) controle om wijzigingen in de enumeratie te kunnen detecteren.

    Echter: dit levert wel een runtime error op! In zekere zin ben je niet heel veel beter af dan dat je applicatie op een later moment ergens laat struikelen over dit probleem. (wel iets)

    Er zijn twee richtingen die leiden tot een betere oplossing, denk ik:
    1. Je maakt het bepalen van de size helemaal dynamisch: je kunt verschillende groottes en de bijbehorende maten benoemen in b.v. een configuratiebestand, db, etc. – Excepties zijn dan op hun plaats als het configuratiebestand bij inlezen (niet bij gebruik!) niet voldoet aan je verwachtingen. De Excepties dwingen dan pre-condities af op je externe input (het config bestand). Een xml-schema op je configuratiebestand zou nog mooier zijn, daarmee kan dan ook intellisense in je configbestand ondersteund worden. Maar goed, direct na inlezen is vaak eenvoudiger en meestal wel voldoende.
    2. Je maakt de sizes juist een echt onderdeel van je applicatie en legt hem beter vast in de code, in de vorm van een Type. Je zou dan kunnen starten met verschillende classes die dezelfde interface implementeren, die 1 methode vereist: GetSize(). Deze neemt de plaats in van je huidige enumeratie (PopUpSize). Als je dit doet, zul je waarschijnlijk gaan bedenken dat deze classes meer kunnen doen dan alleen een grotte vastleggen. Waarschijnlijk zijn het representaties van popups die bepaalde functionaliteit bieden: ideaal om bij elkaar in een class vast te leggen..

    Dus: je ziet deze veel en soms is het puur een pre-condition check op een inputparameter. Maar pas op, want enums zijn vaak uitgekleedde classes, waarbij hun functionaliteit over de hele applicatie verspreid ligt. :)

    Is het volgens jou ook 1 van de bovenstaande gevallen of toch nog iets anders?

    Geplaatst op 21 juli 2010 om 20:01 Permalink

    • Peter Moor schreef:

      Om er een aparte interface met implementerende klassen van te maken, zou ik eigenlijk wat overkill vinden, omdat er behalve GetSize() geen methoden/functies in zouden komen.

      PopUpSize wordt wel altijd gebruikt vanuit de klasse PopUp, die een property van type PopUpSize heeft. Gebruik is dus totaal niet verspreid. Een oplossing zou dus ook kunnen zijn om de enumeration in dit geval te verwijderen en de bijbehorende Size (width/height) direct in PopUp te zetten (omdat het een 1-op-1-mapping is).

      Een standaard waarde teruggeven bij een onbekende PopUpSize zou ook kunnen, waarschijnlijk ook in vergelijkbare situaties met willekeurige enumerations. Ik vind het echter wel mooi om een goede “coverage” van mogelijk waarden af te dwingen.

      Bepaalde gegevens verplaatsen naar een configuratiebestand en daardoor afdwingen dat bij het inlezen hiervan (en dus bij het starten van de applicatie) het systeem al crasht, is inderdaad wel netter. Ik vraag me alleen af of het de extra moeite waard zou zijn, aangezien deze enumeration in de praktijk waarschijnlijk nooit wordt uitgebreid. Maar netter zou het zeker zijn.

      Geplaatst op 22 juli 2010 om 7:36 Permalink

      • Heb overigens nog een derde optie bedacht (goed te combineren met de configuratie-aanpak):

        Een service die deze sizes desgevraagd kan ophoesten. Heeft drie methodes: GetSmallPopupUpSize, GetMediumPopupSize, .. – dit in het geval het echt alleen om de sizes gaat. Deze service kan ook Popup-sub-classes of reeds geconfigureerde objecten teruggeven. (als er meer dan alleen de size verschillend is tussen die popups). In eerste instantie kun je de service deze op laten leveren a.d.h.v. configuratie in code, maar hij zou ook een configuratiebestand kunnen inlezen.

        Vraag die bij mij heel duidelijk omhoog komt is: wat zijn die verschillende popups met verschillende groottes – is het echt zo dat deze alleen verschillen op grootte? Verder functioneel volledig hetzelfde? En zo ja: waarom hebben ze dan verschillende groottes?

        Geplaatst op 22 juli 2010 om 8:45 Permalink

        • Matthijs Mast schreef:

          Dat is precies wat mij het eerste tebinnen schoot.
          Is het werkelijk nodig om popups in verschillende sizes te hebben en dan ook nog dynamisch. Je zou aan een oplossing kunnen denken waarbij de popup zelf bepaald welke grootte hij heeft. Waarschijnlijk doe je dat nu extern (buiten de popup class)

          Geplaatst op 03 augustus 2010 om 14:10 Permalink

          • Bij dit soort switches is het denk ik altijd goed je af te vragen:
            1. wie bepaalt de keuze? (en kan die niet meteen het resultaat opleveren)
            2. wanneer moet de keuze bepaald worden (bij installatie, bij opstarten, bij start van een sessie, na keuze ven een gebruiker)? (dat is belangrijk om te bepalen of je de bron of het resultaat moet bewaren en wanneer)

            Heb jij nog andere vragen die je zou stellen?

            Geplaatst op 04 augustus 2010 om 21:47 Permalink

  • Peter Moor schreef:

    Interessante redenering. Ik vind het bewust gebruiken van Exceptions zelf eigenlijk wel heel mooi, maar ik denk dat het inderdaad (grotendeels) mogelijk is om dit te vermijden.

    Hieronder nog wel een subtiel voorbeeld uit eigen code. Hier wordt een extension method gebruikt om aan de hand van een enumeration (PopUpSize) een Size terug te geven. Mocht de enumeration niet worden herkend (omdat in de toekomst iemand een waarde hieraan toevoegt), dan wordt er een Exception gegooid. Op deze manier zal bij het uitbreiden van de enumeration (wat zeer onwaarschijnlijk is) snel duidelijk worden dat de bijbehorende extension method nog niet is uitgebreid. Is dat nu geen pareltje van een Exception!

    public static class PopUpSizeExtender
    {
    public static Size GetMeasurements(this PopUpSize size)
    {
    switch (size)
    {
    case PopUpSize.Small:
    return new Size(636, 256);
    case PopUpSize.Medium:
    return new Size(670, 384);
    case PopUpSize.Large:
    return new Size(676, 512);
    default:
    throw new ArgumentException(“Unknown PopUpSize: ” + size.ToString());
    }
    }
    }

    Geplaatst op 21 juli 2010 om 9:52 Permalink

    • Paul van Dam schreef:

      Hoi Rick,

      Weer een mooi stuk. Doet me wel weer eens goed nadenken over de code die ik tot nu toe heb geschreven. Ondanks je valide argumenten heb ik altijd wel veel gebruik gemaakt van excepties in een domein en is over het algemeen best goed bevallen. Als ik er nu over nadenk had het wel met veel minder excepties gekund.

      @Peter jouw code ziet er uit als een onderdeel van UI code. Dus zou je in dat geval kunnen falen of zou je gewoon een Warning willen loggen en een default size terug geven.

      Geplaatst op 21 juli 2010 om 21:06 Permalink

      • Goed om te horen dat het duidelijk is en aanzet tot nadenken!

        Overigens lukt het mij ook niet om het gebruik van Excepties te allen tijde te voorkomen hoor. :) Maar door de bovenstaande opties langs te lopen weet ik er regelmatig wel een beter alternatief voor te vinden.

        Geplaatst op 21 juli 2010 om 21:51 Permalink