BuilderWithConditionalException
From APIDesign
(→Changing the State) |
(→Builder for Source) |
||
Line 3: | Line 3: | ||
== Builder for Source == | == Builder for Source == | ||
- | These days [[I]] am trying to use the [[builder]] pattern also for construction of {{truffle|com/oracle/truffle/api/source|Source}}. Rather than having various (and overloaded) methods | + | These days [[I]] am trying to use the [[builder]] pattern also for construction of {{truffle|com/oracle/truffle/api/source|Source}}. Rather than having various (and overloaded) methods named '''fromFileName''' with various number and order of parameters, etc. we'd like to have: |
<source lang="java"> | <source lang="java"> |
Revision as of 07:51, 13 June 2016
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 named fromFileName with various number and order of parameters, 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 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 came 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; }
Obviously, when one specifies the content directly, there is no I/O when the build() method is called and there should be no need to require catching an IOException. With the above change 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). 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.