APIDesignPatterns:Exceptions
From APIDesign
(→Runtime vs. Checked) |
(→Runtime vs. Checked) |
||
(13 intermediate revisions not shown.) | |||
Line 1: | Line 1: | ||
- | + | Casper Bang asked following question after reading the [[TheAPIBook]]: | |
''I was curious as to know how come, in a book strictly about API design in Java, you do not mention exceptions (particular checked exceptions) and the role they play in documenting assertions vs. hampering versionability. Did you simply think this to be too controversial an issue I wonder?'' | ''I was curious as to know how come, in a book strictly about API design in Java, you do not mention exceptions (particular checked exceptions) and the role they play in documenting assertions vs. hampering versionability. Did you simply think this to be too controversial an issue I wonder?'' | ||
Line 7: | Line 7: | ||
== Nothing special == | == Nothing special == | ||
- | One reason why there is no special attention paid to | + | One reason why there is no special attention paid to exceptions is that at the end, exceptions are just classes. As such the same rules that can be applied to any class that shows up in the API can be applied to exceptions in the API as well. When adding exceptions in your API you will not do anything bad if you follow the ''do not expose more than necessary'' credo of [[Do_Not_Expose_More_Than_You_Want|Chapter 5]]. If your exception is supposed to be thrown just by your code, it is quite OK to make its constructor package private. That will guarantee the intended purpose of the exception, which is, to be thrown only by you and caught by clients of your API. It will guarantee that nobody can misuse and misinterpret this intention. From the opposite point of view: if you want your clients to throw an exception and only your code to consume it, you do not need public getters to get values passed into the constructor at the time the exception is thrown. |
On the other hand, [[Do_Not_Expose_More_Than_You_Want|Chapter 5]] also advices to prefer factory methods over exposing constructors. I tried that few times, but I have a feeling that this feels a bit unnatural and as such I cannot recommend code like: | On the other hand, [[Do_Not_Expose_More_Than_You_Want|Chapter 5]] also advices to prefer factory methods over exposing constructors. I tried that few times, but I have a feeling that this feels a bit unnatural and as such I cannot recommend code like: | ||
Line 29: | Line 29: | ||
The common mindshare among Java developers seems to expect that exceptions are raised by writing '''throw new Something''' and it is therefore likely better to expose constructor of your exception class instead of factory method. Still, if you do not expect people to benefit from subclassing your exception, make it '''final''' - your options for [[Ever_Changing_Targets|future evolution]] will remain more open. | The common mindshare among Java developers seems to expect that exceptions are raised by writing '''throw new Something''' and it is therefore likely better to expose constructor of your exception class instead of factory method. Still, if you do not expect people to benefit from subclassing your exception, make it '''final''' - your options for [[Ever_Changing_Targets|future evolution]] will remain more open. | ||
- | In short, exceptions are classes. They shall follow the evolution rules applicable to classes, as discussed in [[Code_Against_Interfaces%2C_Not_Implementations|Chapter 6]]. It is not wise to add abstract methods into exceptions could have been subclassed in prior versions, it is not wise to expose fields, remove elements already available, etc. However Casper is right, this is not all that can be said about exceptions, it seems | + | In short, exceptions are classes. They shall follow the evolution rules applicable to classes, as discussed in [[Code_Against_Interfaces%2C_Not_Implementations|Chapter 6]]. It is not wise to add abstract methods into exceptions that could have been subclassed in prior versions, it is not wise to expose fields, remove elements already available, etc. However Casper is right, this is not all that can be said about exceptions, it seems that something special remains unsaid. |
== Runtime vs. Checked == | == Runtime vs. Checked == | ||
- | [[ | + | [[Java]] is the first industrial language that introduced [[Checked exception]]s. As such, when talking about exceptions in context of [[Java]], one cannot escape from talking about runtime vs. checked benefits and drawbacks. However this is tricky, as far as I know this is a perfect topic to start never-ending flamewar. Even the [[wikipedia::Checked_exception|wikipedia]]'s article related to pros and cons is written very defensively (some say.., others mean..., etc.), so it seems important to approach the topic carefully. |
- | There may differences between checked and unchecked exception in comprehensibility, readability or maintainability of the code written against libraries using the first or the latter. However as I argued in [[Runtime_Aspects_of_APIs|Chapter 11]], Runtime Aspects of APIs, from the point of view of API evolution, there is no difference. | + | There may differences between checked and unchecked exception in comprehensibility, readability or maintainability of the code written against libraries using the first or the latter. However as I argued in [[Runtime_Aspects_of_APIs|Chapter 11]], Runtime Aspects of APIs, from the point of view of API evolution, there is no difference. When a method in a library is written so it can throw some exception in one version and in some newer version decides to throw yet another exception type under some circumstances, then this is an incompatible change. And the change is incompatible in both cases. When using checked exceptions, the change is source incompatible, as one needs to change the signature of the method to define the new exception. As a result code that was compilable, may get broken. In the case of unchecked exceptions, the change is functionally incompatible - as the code which originally caught all exceptions thrown from the method, will no longer work as expected. As such the difference between runtime and checked for API design is not as big as it might have seen. |
== My Single Exception == | == My Single Exception == | ||
- | + | Very common design flaw (maybe flaw is too strong, we can calling it design attempt) is to define your own completely new exception when designing your library. An example of this approach can be found in [[Ant]] or in [[Maven]]. The first defines own '''BuildException''', the later does something similar. These two exceptions are completely unrelated to any other exception defined by the [[Java]] platform. As a result, whenever one writes a build task, it is not enough to just read and write files - e.g. something very natural to almost every build step, but one needs to '''try/catch''' the I/O operations' exception and encapsulate it into | |
+ | |||
+ | <source lang="java"> | ||
+ | try { | ||
+ | // do some operations | ||
+ | } catch (IOException ex) { | ||
+ | throw new BuildException(oldEx); | ||
+ | } | ||
+ | </source> | ||
+ | |||
+ | This is overcomingly verbose, especially if we keep in mind that almost every task needs to deal with I/O. Also the final error reports are not really easy to read - for one I/O failure, there are two exceptions chained to each other. Moreover only the inner one is important, the other is quite useless. Things can get even messier when you imagine that some maven goals are wrappers around Ant tasks. As such one gets chain of at least three exceptions, as maven goal needs to wrap the Ant's task invocation and the tasks wraps the I/O. | ||
+ | |||
+ | It would be much simpler if both [[Ant]] and [[Maven]] designers admitted that it is natural to throw '''IOException''' from inside its tasks/goal implementations. Then there could be: | ||
+ | |||
+ | <source lang="java"> | ||
+ | |||
+ | public abstract class Task { | ||
+ | public abstract void execute() throws IOException; | ||
+ | } | ||
+ | |||
+ | public class BuildException extends IOException { | ||
+ | } | ||
+ | </source> | ||
+ | |||
+ | The code inside custom '''execute''' implementation could be shorter, as there would be no need for the '''try/catch''' of I/O operation exceptions, users would get no wrapped I/O exceptions and the expressiveness would remain the same, as the possibility to raise the '''BuildException''' would be kept. | ||
== Deciding on Importance == | == Deciding on Importance == | ||
- | + | When dealing with flow of exceptions in a complex, [[modular system]], one needs to solve an important problem: ''decide whether an exception is important or not''. This may sound easy, but when you get an exception from a library, how can you tell whether this is something that shall be shown to the user as an information about failed I/O or whether this is an unexpected error state that needs to be logged and workarounded somehow? This is tough. Usual solution builds on the assumption that the top most caller knows the scope of an action (like ''I want to save a file'', ''I want to compile'', etc.) and this top most caller will decide on the importance. This is easy to understand model, and it works well in most situations (especially if there are no exceptional states), however in [[NetBeans]] we needed more granular identification. As such we created a model of annotating an exception with additional attributes: | |
+ | |||
+ | <source lang="java"> | ||
+ | public final class Exceptions { | ||
+ | private Exceptions() { } | ||
+ | |||
+ | public static <T extends Throwable> T attachMessage(T ex, String msg); | ||
+ | public string <T extends Throwable> T attachLocalizedMessage(T ex, String msg); | ||
+ | public string <T extends Throwable> T attachImportance(T ex, java.util.Level level); | ||
+ | |||
+ | public static Level findImportance(Throwable t); | ||
+ | public static String findMessage(Throwable t); | ||
+ | public static String findLocalizedMessage(Throwable t); | ||
+ | } | ||
+ | </source> | ||
+ | |||
+ | More info at [http://bits.netbeans.org/6.1/javadoc/org-openide-util/org/openide/util/doc-files/logging.html logging FAQ]. As a result any code on the exception thrown chain can jump in and mark the exception as important or unimportant and pass it on. The final '''catch''' block then gets the importance information and uses it decide whether show a dialog to the user, or ''swallow'' the exception. | ||
== Extensibility == | == Extensibility == | ||
- | + | Exceptions are [[APIDesignPatterns:ExceptionExtensibility|easily extensible]] via subclassing. | |
+ | |||
+ | |||
+ | [[Category:APIDesignPatterns]] | ||
+ | [[Category:APIDesignPatterns:Exceptions]] |
Current revision
Casper Bang asked following question after reading the TheAPIBook:
I was curious as to know how come, in a book strictly about API design in Java, you do not mention exceptions (particular checked exceptions) and the role they play in documenting assertions vs. hampering versionability. Did you simply think this to be too controversial an issue I wonder?
--Casper Bang 05:17, 5 September 2008 (CEST)
Contents |
Nothing special
One reason why there is no special attention paid to exceptions is that at the end, exceptions are just classes. As such the same rules that can be applied to any class that shows up in the API can be applied to exceptions in the API as well. When adding exceptions in your API you will not do anything bad if you follow the do not expose more than necessary credo of Chapter 5. If your exception is supposed to be thrown just by your code, it is quite OK to make its constructor package private. That will guarantee the intended purpose of the exception, which is, to be thrown only by you and caught by clients of your API. It will guarantee that nobody can misuse and misinterpret this intention. From the opposite point of view: if you want your clients to throw an exception and only your code to consume it, you do not need public getters to get values passed into the constructor at the time the exception is thrown.
On the other hand, Chapter 5 also advices to prefer factory methods over exposing constructors. I tried that few times, but I have a feeling that this feels a bit unnatural and as such I cannot recommend code like:
throw CommandException.exitCode(1); /** Exception to signal result of execution of external process */ public final class CommandException { private int exitCode; private CommandException(int e) { exitCode = e; } public static CommandException exitCode(int exitCode) { return new CommandException(exitcode); } }
The common mindshare among Java developers seems to expect that exceptions are raised by writing throw new Something and it is therefore likely better to expose constructor of your exception class instead of factory method. Still, if you do not expect people to benefit from subclassing your exception, make it final - your options for future evolution will remain more open.
In short, exceptions are classes. They shall follow the evolution rules applicable to classes, as discussed in Chapter 6. It is not wise to add abstract methods into exceptions that could have been subclassed in prior versions, it is not wise to expose fields, remove elements already available, etc. However Casper is right, this is not all that can be said about exceptions, it seems that something special remains unsaid.
Runtime vs. Checked
Java is the first industrial language that introduced Checked exceptions. As such, when talking about exceptions in context of Java, one cannot escape from talking about runtime vs. checked benefits and drawbacks. However this is tricky, as far as I know this is a perfect topic to start never-ending flamewar. Even the wikipedia's article related to pros and cons is written very defensively (some say.., others mean..., etc.), so it seems important to approach the topic carefully.
There may differences between checked and unchecked exception in comprehensibility, readability or maintainability of the code written against libraries using the first or the latter. However as I argued in Chapter 11, Runtime Aspects of APIs, from the point of view of API evolution, there is no difference. When a method in a library is written so it can throw some exception in one version and in some newer version decides to throw yet another exception type under some circumstances, then this is an incompatible change. And the change is incompatible in both cases. When using checked exceptions, the change is source incompatible, as one needs to change the signature of the method to define the new exception. As a result code that was compilable, may get broken. In the case of unchecked exceptions, the change is functionally incompatible - as the code which originally caught all exceptions thrown from the method, will no longer work as expected. As such the difference between runtime and checked for API design is not as big as it might have seen.
My Single Exception
Very common design flaw (maybe flaw is too strong, we can calling it design attempt) is to define your own completely new exception when designing your library. An example of this approach can be found in Ant or in Maven. The first defines own BuildException, the later does something similar. These two exceptions are completely unrelated to any other exception defined by the Java platform. As a result, whenever one writes a build task, it is not enough to just read and write files - e.g. something very natural to almost every build step, but one needs to try/catch the I/O operations' exception and encapsulate it into
try { // do some operations } catch (IOException ex) { throw new BuildException(oldEx); }
This is overcomingly verbose, especially if we keep in mind that almost every task needs to deal with I/O. Also the final error reports are not really easy to read - for one I/O failure, there are two exceptions chained to each other. Moreover only the inner one is important, the other is quite useless. Things can get even messier when you imagine that some maven goals are wrappers around Ant tasks. As such one gets chain of at least three exceptions, as maven goal needs to wrap the Ant's task invocation and the tasks wraps the I/O.
It would be much simpler if both Ant and Maven designers admitted that it is natural to throw IOException from inside its tasks/goal implementations. Then there could be:
public abstract class Task { public abstract void execute() throws IOException; } public class BuildException extends IOException { }
The code inside custom execute implementation could be shorter, as there would be no need for the try/catch of I/O operation exceptions, users would get no wrapped I/O exceptions and the expressiveness would remain the same, as the possibility to raise the BuildException would be kept.
Deciding on Importance
When dealing with flow of exceptions in a complex, modular system, one needs to solve an important problem: decide whether an exception is important or not. This may sound easy, but when you get an exception from a library, how can you tell whether this is something that shall be shown to the user as an information about failed I/O or whether this is an unexpected error state that needs to be logged and workarounded somehow? This is tough. Usual solution builds on the assumption that the top most caller knows the scope of an action (like I want to save a file, I want to compile, etc.) and this top most caller will decide on the importance. This is easy to understand model, and it works well in most situations (especially if there are no exceptional states), however in NetBeans we needed more granular identification. As such we created a model of annotating an exception with additional attributes:
public final class Exceptions { private Exceptions() { } public static <T extends Throwable> T attachMessage(T ex, String msg); public string <T extends Throwable> T attachLocalizedMessage(T ex, String msg); public string <T extends Throwable> T attachImportance(T ex, java.util.Level level); public static Level findImportance(Throwable t); public static String findMessage(Throwable t); public static String findLocalizedMessage(Throwable t); }
More info at logging FAQ. As a result any code on the exception thrown chain can jump in and mark the exception as important or unimportant and pass it on. The final catch block then gets the importance information and uses it decide whether show a dialog to the user, or swallow the exception.
Extensibility
Exceptions are easily extensible via subclassing.