Jul
15
2010

No Exceptions madeNo Exceptions made

In response to a finding during an internal project code review last Friday and an article in the latest Java Magazine we had an interesting discussion on the reasons for using exceptions. Eventually I supported two rules of thumb, one I had thought of myself, the other from a colleague of mine.

The following rules of thumb should be seen as in the context of a development platform that supports their use and a developer who uses as the following, basic rule for recognizing the situation to use an exception: use an exception if you cannot follow the normal execution path any more, in other words, if something extraordinary happens or has happened.Naar aanleiding van een bevinding tijdens een interne project code review  afgelopen vrijdag 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, een die ik zelf bedacht had, de ander van een collega van mij.

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.

Additional rules of thumb
The basic rule above seems like a logical assumption, since the name Exception seems to indicate just that. Yet I think it is too naive and the following rules of thumb should be used as well:

1. Do not use exceptions and if you still think you have found a reason for using an exception, think it over again.
2. Do not use exceptions in an (object oriented) domain model.

Rationale
Obviously these rules of thumb deserve some reasoning to support them.
Firstly, throwing an Exception is not without its cost:

  • The runtime execution engine often uses some extra execution time to throw and handle an exception.
  • An exception interrupts the normal execution path, especially if it is not directly captured by the calling method. This means that it is impossible to guarantee that invariants are enforced. You will need a transactional kind of mechanism to rectify the situation, otherwise you must assume that the runtime is corrupt and that it should be discarded.
  • It means an additional return from a method, an additional path to follow. It increases the complexity of a method
  • The try-catch mechanism that is necessary to correctly capture an exception takes several lines of code and distracts from the actual meaning of the code.
  • It basically means that an action initiated by a user or other actor has been stopped and cannot be completed. At best, it can be attempted again, in the worst case, the initiator needs to repeat the attempt himself.

These costs lead to the first rule of thumb. If you can prevent using an exception, everything else being equal, then that should be preferred. I think there are some situations where at first sight, it seems logical to throw an exception, but there are better alternatives:

Pre-conditions for arguments should be set declaratively
A method can throw an exception if the arguments with which a call is made, do not meet the requirements. In other words, an Exception is used for the enforcement of pre-conditions on the arguments. This is better done by declaring those pre-conditions in an explicit way as part of your method signature. By having arguments only in a valid state, you force the calling method itself to ensure a correct call. When this calling method is implemented in the same manner, these pre-conditions will then bubble up, so they are declarative and pre-conditial on the external interface of the unit or code under design.

In other words, they are in the form of a (code) contract. If the external interface is a web service, for instance, and the contract is coded in XML schema, a call breaking pre conditions is already rejected by the schema validator, without invoking the underlying runtime and therefore without possibly entering an invalid state.

Pre-conditions on the internal state should be handled more elegantly
You might say that a call can only be invoked when a certain internal state is in effect. For example, using what came up during our discussion: a teaching module contains slides. The module may only be discarded if the slides have already been removed from the module. This requirement means that the calling method must be able to determine this state (so encapsulation of inner state is lost) and the possibility of race conditions also remains: a slide was added just before the module was supposed to be removed.

In such a situation it is better to look for other possibilities. In this case, start with the question: why should the slides actually be removed first? Suppose the answer in this case is: because the slides may also be used by other modules and so if they are just removed automatically, a user may unwittingly remove content from another module.
It generally turns out there are better alternatives. In the current case it is actually clear that slides are aggregates themselves and that they should not be removed with the learning module any way, but only the links may be removed. Furthermore, you should offer an additional option for orphaned slides to be found and deleted manually or automatically (garbage collection).

Invariants on the internal state can be better enforced by using locks or no threading
When execution has already started and the internal state is changed during this by another thread, the best way to handle this is the use of locks: this prevents problems from occurring at all and enables both execution paths to be followed (sequentially). Locks have drawbacks too, of course (unclear code, deadlocking). Of course, the use of an actor model for concurrency will tackle these problems at the root.

Remaining: pre-conditions and invariants on the state of the external environment
Obviously a certain situation in the use of exceptions still remains: the state of the (external, non-influenced) environment does not satisfy all conditions before or during execution. The canonical example here is a database that is not up at the start, or goes down during the execution of a request. This remains a good reason for throwing an exception.

Obviously, the use of these exceptions could be countered as well, by taking on a greater responsibility, so it includes the handling of more situations the environment could be in. For example, a unit could take on the responsibility of synchronizing the current client state with a server eventually, instead of needing it to be synchronized instantly. This would make it possible to handle a network that is not available by persisting the client state until the network is available again, without throwing Exceptions.

A (good) domain model has no dependencies to an external environment
As the external environment remains the only reason to throw an Exception, it naturally follows that a domain model has no reason to throw an Exception, as it should not depend on any external environment. A good domain model should be a pure representation of the real world, as seen through the lens of a domain. This real environment is never dependent on a database or network system in the system under design. Of course, the domain itself could be concerned about databases and network connections, but those would then be included as domain objects in the model only.

Conclusion
By applying these rules of thumb for using Exceptions it is possible to make systems more robust, simpler, faster and more useful. Obviously, the work involved in making a better design may not always deliver enough rewards to warrant it. However, within the heart of your application, the domain model, this would surely have to be the case, unless your application as a whole has little added value. But building it might not be a good idea anyway then.

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.

Written byGeschreven door Rick | Tags: , , , , , , |

No Comments - Leave a comment »

RSS feed for comments on this post.Rss feed voor commentaar op deze post.

Leave a comment

Powered by WordPress | Aeros Theme | TheBuckmaker.com WordPress Themes