BuilderWithConditionalException

From APIDesign

Revision as of 07:51, 13 June 2016 by JaroslavTulach (Talk | contribs)
Jump to: navigation, search

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.

Personal tools
buy