BuilderWithConditionalException
From APIDesign
The builder pattern is gaining more and more popularity in the APIs that I design: In Truffle API we are trying to use it a lot. For example an instance of the PolyglotEngine is constructed via a builder obtained from its newBuilder() method.
Contents |
Builder for Source
These days I am trying to use the builder pattern also for construction of Source. Rather than having various (and overloaded) methods with fromFileName, etc. we'd like to have:
Source source = Source.newFromFile(file). mimetype("text/javascript"). name("FancyName.js"). build();
It works fine and allows the users to specify only those properties that are needed - for example one can omit the name - then it would be derived from the name of the file. The same applies to mimetype.
Throw on I/O Error
The builder pattern delays reading of the actual file up until the build() method is called. As reading of a file means an I/O operation and as each I/O operation in Java may yield checked IOException, it is desirable for the build() method to propagate it to the caller:
public Source build() throws IOException { // construct and return source; }
However there is more. The builder may also construct a {{truffle|com/oracle/truffle/api/source|Source} object from already provided text. The usage looks like:
Source source = Source.newFromText("6 * 7"). mimetype("text/javascript"). name("FancyName.js"). build();
but in this case there is no I/O operation - e.g. throwing IOException from the build() method isn't appropriate. Doing so would actually expose the biggest flaw of checked exceptions in Java - forcing people to catch them when there is no need/no recovery. Can that be fixed?
Throw only for I/O
The option that I come up with is to parametrize the builder with the exception the build() method is going to throw:
public final class Builder<E extends Exception> { public Source build() throws E { // construct and/or read the content and return source; } public static Builder<IOException> newFromFile(File file) { // .... } public static Builder<RuntimeException> newFromText(String text) { // ... } }
The actual code is shown at my github commit. It properly encourages users to handle I/O exceptions when needed, but doesn't annoy them when there is no need.
Changing the State
The nice thing on this pattern in Java is that it is only compile time only - thanks to the erasure of generic types, there is no overhead during runtime. Just the compiler has enough information to advice users whether they should expect an IOException or not.
And it has yet another benefit: the builder can change its state depending on the methods that are being called (see change):
public Builder<R, RuntimeException> content(String code) { this.content = code; return (Builder<R, RuntimeException>) this; }
Now one can start with a builder based on newFromFile, and still switch to mode that doesn't throw an IOException at all:
Source source = Source.newFromFile(file). mimetype("text/javascript"). name("FancyName.js"). content("var x = 42\n"). build(); // no IOException at all
Summary
By giving the builder a generic error type we allow fine grain control over possible errors states that may accumulate when calling various builder configuration methods. The whole solution keeps the runtime behavior of the builder pattern - after applying erasure of generic types, the whole type information (visible to compiler) disappears.