This thing is long. Here are some navigation links to topic headers:
- Use Exceptions for Exceptional Circumstances?
- Overly Simple, Hacked Example Revisited
- What I Meant to Like About Checked Exceptions
- Non-Existent, Hypothetical Feature Proposal
- What Have We Learned?
- Half-Formed Thoughts
Use Exceptions For Exceptional Circumstances, Right?
Most of what I know (or at least, think I know) was learned in the .net/C# environment. However, I believe it is nearly axiomatic that exceptions are to be used for exceptional circumstances, and (mostly) should not be used as a business logic construct. In the Oracle article, and in my example above, the InvalidAccountException,
StopPaymentException
and the InsufficientFundsException
would appear to flagrantly violate this principle. Within limits, the only valid contingency exception here is AccountNotAvailableException,
which is really just an obfuscation of SQLException.
Can we re-write these classes to do away with some of the possibly extraneous exception handling?
A Overly Simple, Hacked Example – Another Look
First off, three of our four contingencies look a whole lot like validation problems. Depending on our project architecture, we could do away with InvalidAccountException
, StopPaymentException
and InsufficientFundsException
and check for these contingencies from our client code before calling the getCheckingAccount
method defined on the Bank
class, and prior to calling the processCheck
method defined on the CheckingAccount
Class. First, we can add a couple of boolean methods on CheckingAccount to tell us if there is a stop payment order for a check submitted, and whether there are sufficient funds in the account to process the check:
The re-worked CheckingAccount Class:
public class CheckingAccount { private String _accountID; private double _currentBalance; private ArrayList<Integer> _stoppedCheckNumbers; public String getAccountID() { return _accountID; } public double getCurrentBalance() { return _currentBalance; } public void setAccountID(String accountID) { _accountID = accountID; } public void setCurrentBalance(double currentBalance) { _currentBalance = currentBalance; } public ArrayList<Integer> getStoppedCheckNumbers() { if(_stoppedCheckNumbers == null) { _stoppedCheckNumbers = new ArrayList<Integer>(); } return _stoppedCheckNumbers; } public boolean checkAmountApproved(double amount) { double testBalance = _currentBalance - amount; if(testBalance > 0) { return true; } else { return false; } } public boolean checkPaymentStopped(int checkNo) { if(_stoppedCheckNumbers.contains(checkNo)) { return true; } else { return false; } } public double processCheck(Check submitted) throws DatabaseAccessException { double newBalance = _currentBalance - submitted.getAmount(); try { // <Code to Update Database to reflect current transaction> } catch(Exception e) { // <Code to log SQLException details> /* * After logging and/or otherwise handling the SQL failure, throw * an exception more appropriate to the context of the client code, * which does not care about the details of the data access operation, * only that the information could not be retreived. */ throw new DatabaseAccessException("Database Error"); } return newBalance; } }
Note in the above that we have done away with all but our single contingency, DatabaseAccessException
, which in reality, is our API response to a fault. Theoretically, the only exception client code is required to handle now is the DatabaseAccessException
. Now, what happens to our Bank class?
The re-Worked Bank Class:
public class Bank { public static CheckingAccount getCheckingAccount(String AccountID) throws DatabaseAccessException { CheckingAccount account = new CheckingAccount(); try { /* * <Code to retrieve Account data from data store> */ // Use test data to initialize an account instance: account.setAccountID("0001 1234 5678"); account.setCurrentBalance(500.25); account.getStoppedCheckNumbers().add(1000); } catch(Exception e) { // <Code to log SQLException details> /* * After logging and/or otherwise handling the SQL failure, throw * an exception more appropriate to the context of the client code, * which does not care about the details of the data access operation, * only that the information could not be retrieved. */ throw new DatabaseAccessException("Database Error"); } return account; } public static boolean checkAccountExists(String AccountID) { boolean exists = false; try { /* * <Code to check accountID exists in data store> */ if(//a valid row is returned for AccountID) { exists = true; } } catch(SQLException e) { // <Code to log SQLException details> /* * After logging and/or otherwise handling the SQL failure, throw * an exception more appropriate to the context of the client code, * which does not care about the details of the data access operation, * only that the information could not be retrieved. */ throw new DatabaseAccessException("Database Error"); } return exists; } }
Here, we have eliminated the InvalidAccountException
, and added a boolean method to check whether a valid account exists for a given account number. As before, since the original static method getCheckingAccount
is still performing data access, we need to retain our DatabaseAccessException
.
Last, what does our client code look like now? We have done away with using Exceptions in a business logic context, what was the impact?
The re-worked Mock Client Code:
public class MockClientCode { /* * Assume this code is supporting UI operations. */ static String SYSTEM_ERROR_MSG_UI = "" + "The requested account is unavailable due to a system error. " + "Please try again later."; static String INVALID_ACCOUNT_MSG_UI = "" + "The account number provided is invalid. Please try again."; static String INSUFFICIENT_FUNDS_MSG_UI = "" + "There are insufficient funds in the account to process this check."; static String STOP_PAYMENT_MSG_UI = "" + "There is a stop payment order on the check submitted." + " The transaction cannot be processed"; public static void main(String args[]) { // Sample Data: String accountID = "0001 1234 5678"; int checkNo = 1000; double checkAmount = 100.00; // Use test data to initialize a test check instance: Check customerCheck = new Check(accountID, checkNo, checkAmount); CheckingAccount customerAccount = null; double newBalance; if(Bank.checkAccountExists(customerCheck.getAccountID())) { try { customerAccount = Bank.getCheckingAccount(customerCheck.getAccountID()); if(!customerAccount.checkPaymentStopped(customerCheck.getCheckNo())) { if(customerAccount.checkAmountApproved(customerCheck.getAmount())) { newBalance = customerAccount.processCheck(customerCheck); // Output transaction result to UI: System.out.printf("" + "The transaction has been processed. New Balance is: " + DecimalFormat.getCurrencyInstance().format(newBalance)); } else // there were insufficient funds { // Output the message to the user interface: System.out.println(INSUFFICIENT_FUNDS_MSG_UI); } } else // payment was stopped on this check no. { // TODO Auto-generated catch block System.out.println(STOP_PAYMENT_MSG_UI); } } catch (DatabaseAccessException e) { // Output the message to the user interface: System.out.println(SYSTEM_ERROR_MSG_UI); } } else // No valid account { // Output the message to the user interface: System.out.println(INVALID_ACCOUNT_MSG_UI); } }
Wow. Look at that ugly nested conditional. Well, we have absolved our client code of having to handle a bunch of exceptions related to our business logic, and it is possible that improvements to the class structure, and a little refactoring could make significant improvements. In the end, though, it seems like a bit of a trade off.
What I Meant to Like About Java Checked Exceptions
In concept, I liked the idea that a method could declare, as part of its signature, that it throws a specific type of exception. What I do NOT like about it is that, you HAVE to. In many, cases, this mechanism can become quite annoying. A shining example, excerpted from the article link above, as follows:
“To programmers, it seemed like most of the common methods in Java library classes declared checked exceptions for every possible failure. For example, the java.io
package relies heavily on the checked exception IOException
. At least 63 Java library packages issue this exception, either directly or through one of its dozens of subclasses.”
“An I/O failure is a serious but extremely rare event. On top of that, there is usually nothing your code can do to recover from one. Java programmers found themselves forced to provide for IOException
and similar unrecoverable events that could possibly occur in a simple Java library method call. Catching these exceptions added clutter to what should be simple code because there was very little that could be done in a catch block to help the situation. Not catching them was probably worse since the compiler required that you add them to the list of exceptions your method throws. This exposes implementation details that good object-oriented design would naturally want to hide.
Obviously, it seems that it is possible to go a little too far with this scenario.
On the other hand, wouldn’t it be a handy optional language feature to be able to add that throws clause if, in your infinite designer wisdom, it seemed the superior design choice?
Warning: Non-Existent, Hypothetical Feature Ahead
I grew up, so to speak, using C#, which does not have anything like the Java check-or-specify policy. It is up to the developer to properly anticipate, test for, and handle exceptions. Or to throw them programmatically, as the design may require. But in any case, client code accessing an API is blissfully unaware that a method might throw a particular type of exception until either the designed thinks of it, or it occurs in use (er, I mean, “testing”).
I propose that a useful (but at present, non-existent) feature for an existing language would be to put that check-or-specify policy into the hands of the developer, and allow the developer to invoke it by adding the throws clause to a method signature. My hypothetical mechanism would look like this:
- If a method defined on a class throws one or more exceptions, a compiler warning will evidence itself (at least, in the land of IDE’s such as Eclipse or Visual Studio) as opposed to the compiler error we get in Eclipse with Java. However, if there is not a throws clause included in the method signature, the warning is all you would get. You could still consume the method without handling or propagating the exception from below.
- If the developer or designer adds a throws clause to the method signature specifying one or more particular exception types, then client code is required to recognize and address those exceptions, similar to the existing Java Check-or-Specify policy.
The above mechanism would place the control in the hands of the designer, and allow selective enforcement of such a policy, while still providing helpful compiler warnings when, by design, such enforcement is relaxed. After all, if one team invests the time and mental energy to think trough the exception possibilities in their code, why not spare the consumer of that code the headache of doing it all over again? This would leave responsibility upon the developer of the client code to address, (or not) such exceptions as he/she sees fit.
What Have We Learned?
- Exceptions in Java are often misused (as they are in C# and other languages).
- Checked Exceptions, and the Check-or-Specify Policy, can add complexity to a design, and potentially create the need for a large numbers of additional types within a project.
- There is a solid theoretical philosophy behind Checked Exceptions in Java that has not been well-implemented in actual use. Understanding the intent behind this language/environment feature can help in design decisions.
- Thinking of Exceptions in terms of “Faults” and “Contingencies” is a helpful way to guide Exception Handling design in accordance with the previous point.
- Thinking of Exceptions in terms of “Faults” and “Contingencies” encourages the use of Exceptions to enforce business rules (Not sure how I feel about this).
- As with most things in programming, effective use of Exceptions is more often than not a case of making appropriate tradeoffs and design decisions. Careful analysis of the problem domain, exception context, and attention to client code context may demand overriding theory and dogma.
- More study and analysis is needed on my part.
Half-Formed Thoughts
In researching this post, and through the discussion on the r/java sub-reddit, I have developed the following thoughts. In examining an API or class structure, and designing an effective Exception mechanism, ask yourself:
- Who (which component of your code) cares? Where is the specific exception most effectively handled in your design? Can you deal with the specific exception appropriately at the point it is thrown, and propagate (or otherwise notify) a different exception up the call stack which does not contain implementation details of the source class? Can you maintain encapsulation?
- There is no law which states you can’t log the initial exception, and then throw another which is more appropriate to the context of the calling code.
- Is the exception a result of user input? If so, this may be a sign that it is more properly handled with a validation mechanism and/or improved implementation of business rules.
- The previous point is not always true. It could be a tradeoff, where the exception results from user input, but deep within a series of calls. In this case it might be more practically handled with an exception, even though this breaks form, and uses an exception in place of business rules and validation.
- The Java Checked Exception and Check or Specify policy is a real mixed bag. Philosophically, I like it. However, employing it effectively, and the way I was intended, requires careful design – more, I think, than many put into exception handling.
- It is easy to use an exception when you don’t know what else to do (meaning, you need to re-examine your design, or go back to the documentation. I am 100% guilty of this at times).
- Don’t use exceptions to “Pass the buck”” and make what should have been YOUR problem into the nest guy’s problem. As a consumer of your API, he will have even less context do deal with YOUR exception than you do.
This has been a difficult post to write. We tend to think of exceptions and exception handling as something we understand well. I for one tend to think about it when I need it, and not much else. Further, I am learning much of this as I go, meaning I am self-taught, and there is always the danger that I will think I have something figured out, only to learn (often in quite humbling, “what the hell am I doing writing about this” kinds of ways) I had it all wrong, or missed something which should have been obvious.
In trying to formulate a coherent representation of my understanding here, I have developed a greater appreciation for the difficult design choices we are faced with. Trying to wrap my head around the deeper intentions of the Java Exception mechanism, and the semi-polarized disagreement about its usefulness among Java devs, has been an eye-opening experience. The education win for me extends beyond Java, and into my toolbox.
Comments