Custom Exception Types
I’m not particularly fond of propagating Java-specific exception types, even if they’re as general as NoSuchElementException. I’d much rather introduce an application-specific exception type. This technique allows me to change my implementation at will; it can also make the code clearer.
Suppose we change code in the deal method to something that no longer throws a NoSuchElementException, but instead throws an ArrayIndexOutOfBoundsException. We could catch ArrayIndexOutOfBoundsException and re-throw NoSuchElementException to avoid any client problems, but the safest bet is to design our exception types better in the first place.
Let’s change the test to expect an application-specific exception (see Listing 4).
Listing 4 Driving a more explicit exception type.
public void testDealTooMany() { assertDealAll(deck1, new ArrayList<Card>()); try { deck1.deal(); fail("should have received exception"); } catch (EmptyDeckException expected) { // success! } }
The compilation error that this change generates drives you to code the following:
package domain; public class EmptyDeckException extends RuntimeException { }
Extending from RuntimeException will allow us to write code all over the place that calls deal, yet doesn’t have to handle the EmptyDeckException. That’s great, because we know that we can control client code so that the exception can never occur.
You’ll note that we don’t have a test class named EmptyDeckExceptionTest. We drove the creation of this class through testDealTooMany. Should we add a test class? I don’t feel compelled to add one yet—there’s no logic whatsoever in EmptyDeckException. This is about the only case where we won’t end up with a test class for a production class. If we add any logic at all, such as a constructor that takes a message argument, we’ll need to document that in a new test class.
Running the tests again, we get a test error showing that the NoSuchElementException was thrown and not caught. To get the test to pass, modify the Deck method deal as shown in Listing 5.
Listing 5 Ensuring that we don’t deal from an empty Deck.
public Card deal() { if (cardsRemaining() == 0) throw new EmptyDeckException(); if (it == null) it = cards.iterator(); Card card = it.next(); it.remove(); return card; }
The nice part about this improved solution is that it enhances the production code to more clearly express what’s going on.