TryCatchRedo
From APIDesign
Often when reasoning about exceptions, they are thought to be somewhat special, not really full featured classes. At least many coding practices advice to not use them in situations where regular objects would be more than appropriate. As a result developers often do not think about using inheritance or other object oriented aspects when dealing with exceptions. Sometimes this is justified, sometimes it can be too restricting. In Java, exceptions are just like any regular class and this can be sometimes used to achieve surprising results.
All developers think about exceptions as something that can be thrown and caught. Some know that it is useful to call exceptions' getters to get more info about the failure represented by the exception. However only few explore design style where exceptions also contain setters or other mutable methods. Not that it would make sense to modify state of the exception itself, as that is short living object, however the exception can serve as a bridge to some internal longer living object and can be its interface allowing changes to its state.
This imperative style can be used to implement Try/Catch/Redo pattern. Imagine that some code deep inside of a set of I/O operations cannot proceed without asking a question. For example the NetBeans version control modules may need to ask the user whether file shall be locked before allowing its content to be changed. Showing dialog by itself is big no-no for low level I/O libraries as it often leads to deadlocks when fighting for the shared screen. Instead imagine that there is a special extended IOException:
Code from UserQuestionException.java:
See the whole file./** Specialized I/O exception to request some kind of user confirmation. * A code that needs to ask user shall not attempt to open a dialog itself, * rather it shall emit this exception and let its callers show the dialog * at appropriate time. * * @author Jaroslav Tulach * @since 2.0 */ public abstract class UserQuestionException extends IOException { /** Description of the dialog to show to the user. Whoever catches * this exception shall use * {@link #getQuestionPane()}. * {@link JOptionPane#createDialog(java.lang.String)} * to construct and display the dialog. * * @return the pane to display to user */ public abstract JOptionPane getQuestionPane(); /** When the user confirms (or rejects) message presented by the * {@link #getQuestionPane()} dialog, the exception shall be notified * by calling this method with {@link JOptionPane#getValue()} option. * * @param option the option selected by the user */ public abstract void confirm(Object option); }
This is just another IOException, so for clueless API users nothing changes. One can continue to write classical saving code:
Code from SaveAction.java:
See the whole file.try { OutputStream os = where.openConnection().getOutputStream(); os.write(what.toString().getBytes()); os.close(); } catch (IOException ex) { JOptionPane.showMessageDialog(null, ex); }
However in case one needs to provide support for safe queries, one can extend the code to recognize the special exception and communicate with it:
Code from SaveActionWithQuery.java:
See the whole file.for (;;) { try { OutputStream os = where.openConnection().getOutputStream(); os.write(what.toString().getBytes()); os.close(); } catch (UserQuestionException ex) { JOptionPane p = ex.getQuestionPane(); JDialog d = p.createDialog(ex.getLocalizedMessage()); setVisible(d, p); ex.confirm(p.getValue()); if ( !p.getValue().equals(JOptionPane.CANCEL_OPTION) && !p.getValue().equals(JOptionPane.CLOSED_OPTION) ) { continue; } } catch (IOException ex) { JOptionPane.showMessageDialog(null, ex); } break; }
The confirm method is the callback to the internals of the URLConnection provider, like the NetBeans version control modules. The confirm method can change the internal state, just like in this simple stream example:
Code from QueryStream.java:
See the whole file.public final class QueryStream extends OutputStream { private ByteArrayOutputStream arr = new ByteArrayOutputStream(); /** this field can be manipulated by the QueryException */ Boolean reverse; @Override public synchronized void write(byte[] b, int off, int len) throws IOException { if (reverse == null) { throw new QueryException(); } arr.write(b, off, len); } @Override public synchronized void write(int b) throws IOException { if (reverse == null) { throw new QueryException(); } arr.write(b); } @Override public String toString() { if (reverse == null) { return "Reverse question was not answered yet!"; } if (reverse) { StringBuilder sb = new StringBuilder(); sb.append(arr.toString()); sb.reverse(); return sb.toString(); } return arr.toString(); } private class QueryException extends UserQuestionException { @Override public JOptionPane getQuestionPane() { JOptionPane p = new JOptionPane("Store in reverse way?"); p.setOptionType(JOptionPane.YES_NO_CANCEL_OPTION); return p; } @Override public void confirm(Object option) { if (option.equals(JOptionPane.YES_OPTION)) { reverse = Boolean.TRUE; return; } if (option.equals(JOptionPane.NO_OPTION)) { reverse = Boolean.FALSE; return; } } } }
After confirming the desire to really lock the file, one can redo the whole operation. The stream's internal state is now changed and the whole operation successfully proceeds.
Balance Simplicity and Flexibility
Some may say that exceptions are not suitable for regular code flow control and as such using the TryCatchRedo is more a misuse than a regular design pattern, however this all depends on the frequency.
If the UserQuestionException is used in 99% of cases, by majority of users, then this pattern is definitely not suitable. Calling once, at the try round, catching the exception and calling again is ridiculously complex. On the other hand if 99% of usages are OK with catching plain IOException then this pattern is a nice example of making simple things easy and complex possible. To implement this kind of communication without use of exception would create quite rich API - overly complex for those 99% of its users.
Actually this is nice example of a Teleinterface, a bottleneck API, based on simplistic assumptions, which allows rich communication between the API user and provider that both decide to understand the enhanced UserQuestionException. The basic URL API does not even know that the enhanced communication is going on. Neither 99% of its users. Still, such exception can be teleported from the API provider to the API client if necessary.
<comments/>