There are basically three ways a method can signal an error—or any status—to its caller: by return value, by internal state change, or by method call. Returning a value is the most natural way, so I will focus on that. It comes in flavors. The most obvious is by means of the return statement in Java. Out-put parameters would be another way, but Java does not allow parameters to be passed out. Languages like C++ or PASCAL allow that through call-by-reference. In Java one would have to change the state of an input-parameter to archieve this effect. But that places the burden of managing this out-parameter on the caller. Hence, it is not really an option.
Yet another way for returning a value is—by throwing an exception. Exceptions try to solve two problems. The first is cramping computation result and computation status into a single return value. If a method is called merely for its side-effects, normal return values work fine. Instead of void, the methods is changed to yield a status value. The method «addRoll(int)» of the bowling example would change from:
public void addRoll(int roll) throws BowlingException { if (isComplete()) throw new BowlingException("No more rolls allowed."); if (roll < 0 || roll > 10) throw new BowlingException("Roll out of bounds."); if (mExtraRolls > 0) { … } }
to something like this:
public enum Status { GAME_COMPLETE, ROLL_OUT_OF_BOUNDS, OK } public Status addRoll(int roll) { if (isComplete()) return Status.GAME_COMPLETE; if (roll < 0 || roll > 10) return Status.ROLL_OUT_OF_BOUNDS; if (mExtraRolls > 0) { … } return Status.OK; }
While the exception is now gone, the code on the caller site is not going to look better. The try-catch-construct that mars the «main()»-method of the bowling example would have to be replaced by switch-case or if-then-else.
In cases where the method does not use all values of its co-domain–well, it is not swell but still viable to encode the return state into the unused values. In the bowling example, the method «getScore()»—if it would have to signal errors—could use any number of type int except for the range between 0 and 300. A negative number could signal an error, for instance. Constants have to be defined (and used!) for those error states or things might get out of hand. Readability of the code on the caller site suffers, however, by the additional state parsing.
With a bit-mask, both a value and a status can be encoded into a single integer. It is a quite common practice, especially in low level functions or on devices where memory is rare. Android does it in various places, for instance with «MotionEvents». As not only checking the state but also decoding the return value is required by the caller, readability suffers even more. Also debugging gets no easier. Defining special composite return types or using general collections less obfuscates the structure of the return value, but decoding and checking on the caller site will not go away.
As said before, exceptions address two problems. The first was coercing computation status and result into a single return value. This no longer happens, and decoding becomes unnecessary. The second is checking the state. It now happens in a code path altogether different from normal execution. Still readability is not great, because—as aspect-oriented programming would say–two concerns are intermingled that cannot clearly be expressed in the linear structure of source code.
Readability-wise it does not really matter whether to signal errors by return value or by exception. The caller has to treat the error-states separately from the normal return values and to act upon it. This might shroud the business logic of the caller. The best solution to this, it seems, is to avoid errors altogether! I will focus on this—among other things—in my next post.
No comments:
Post a Comment