The difficulty with "elegant" exception handling is that it depends heavily on the context.
For example, in one application, you need to select the UI control and ask the user to enter a valid value. In another application (if it is a server), you need to throw an exception that must cross the application boundary in one way or another. And in the third application, checking the arguments takes place at a higher level and passing 0 to parameter b
is a bug and should cause the application to crash.
All this leads to the following features:
Add a precondition to the Calculate
method that b != 0
: Contract.Requires(b != 0)
. In this case, we shift the responsibility for code validation to the caller. We say that 0 is an invalid value, and we ask the client not to touch us in this case.
Do nothing and forward a DivideByZeroException
. Another alternative is to document the Calculate
method and "say" that this exception might flow from it.
In this case, we again shift the responsibility to the calling code, which knows better what to do with it. Now the client can issue a message, send the desired response to the client, or convert this exception to some other one.
- Add a new level of indirection. Also, you can add a special type of exception, for example,
CalculationException
or OperationException
. In this case, a new type of exception will describe a family of errors that can occur when working with an abstract operation.
This solution is more extensible, since new heirs may throw new and new types of exceptions, and a specific type of exception may contain more specific information:
public abstract class OperationException { } public class DivideByZeroOperationException : OperationException {} public class Division : BinaryOperation { protected override decimal Calculate(decimal a, decimal b) { if (b == 0) {throw new DivideByZeroOperationException();} return a / b; } }
The client can now decide whether to handle the base exception or more specific one.
- Another alternative is to go back to smart return codes and get away from the exceptions.
Example:
public enum Error { DivideByZero, YetAnotherError } // ΠΡΠΎ ΡΠ°ΠΊΠΎΠΉ ΡΠ΅Π±Π΅ 'Either' ΡΠΈΠΏ, ΠΊΠΎΡΠΎΡΡΠΉ ΠΌΠΎΠΆΠ΅Ρ ΡΠΎΠ΄Π΅ΡΠΆΠ°ΡΡ Π»ΠΈΠ±ΠΎ ΠΎΡΠΈΠ±ΠΊΡ (ΠΊΠΎΠ΄ ΠΈ, // Π²ΠΎΠ·ΠΌΠΎΠΆΠ½ΠΎ, ΡΠΎΠΎΠ±ΡΠ΅Π½ΠΈΠ΅), Π»ΠΈΠ±ΠΎ Π²Π°Π»ΠΈΠ΄Π½ΡΠΉ ΡΠ΅Π·ΡΠ»ΡΡΠ°Ρ public class OperationResult { public decimal? Result {get;} public Error? Error {get;} public static OperationResult Success(decimal result) { } public static OperationResult Failure(Error error) { // ΠΠΎΠΆΠ΅Ρ Π±ΡΡΡ ΡΡΠΎΠΈΡ Π΄ΠΎΠ±Π°Π²ΠΈΡΡ ΡΠΎΠΎΠ±ΡΠ΅Π½ΠΈΠ΅ ΠΈΠ»ΠΈ Π΄ΡΡΠ³ΡΡ ΠΊΠΎΠ½ΡΠ΅ΠΊΡΡΠ½ΡΡ ΠΈΠ½ΡΠΎΠΌΡΠ°ΡΠΈΡ } } public class Division : BinaryOperation { protected override OperationResult Calculate(decimal a, decimal b) { if (b == 0) {return OperationResult.Error(Error.DivideByZero);} return a / b; } }
This is a more "functional" approach, while the previous ones are more canonical in OO languages, such as C #.
The main conclusion: there is no imputed way to fully handle the exception inside the Calculate
method . Error information must be passed to the calling code, since only at a higher level there is enough information (context) to handle this problem in a complete way.