'. '

ChameleonBuilder

From APIDesign

Revision as of 07:13, 20 June 2016 by JaroslavTulach (Talk | contribs)
(diff) ←Older revision | Current revision (diff) | Newer revision→ (diff)
Jump to: navigation, search

Classical Builder pattern allows the users to set its attributes one by one and doesn't force them to do so in a predefined order. Non-essential attributes can even be skipped in the favor of using defaults. However what can one do if certain attribute is essential - e.g. needs to be specified?

Contents

Runtime Exception

One can throw an exception in the build() method. But that leaves the notification of obvious coding error up to the execution. That doesn't support the cluelessness people using type safety of Java or other typed languages are used to. We can certainly do better.

Factory Method

Of course, one can use a factory method - e.g. let the builder be created only with necessary attributes. However this has the typical drawbacks to evolution of factory methods - the order of parameters is important, needs to be remembered and potentially number of overloaded methods grows. We used to have fromText method in Source class. One would use it as:

Source src = Source.fromText("my.js", "my.js");

Now, can you guess what of the parameters is name for the Source and which of them is the content? Hardly, I don't remember the order either. Now imagine we add also a MIME type:

Source src = Source.fromText("my.js", "my.js", "text/javascript");

Three subsequent String arguments are really hard to use properly. Let's try to use the builder pattern, but make it unfinished first.

Unspecified Return Type

Let's signal to the user of your builder that it is not yet properly configured by returning wrong type from the build() method. The class could look like:

public final class Builder<R> {
  public R build() {
    // construct Source, but is type to return R
    // throw error if essential attribute is missing
    return theSource;
  }
 
  public static Builder<Void> newFromText(String text) {
    // new instance of unfinished (because build method returns Void) builder
  }
 
  public Builder<R> name(String name) {
    // assign the name
    return this;
  }
 
  public Builder<Source> mimeType(String mime) {
    // assign the name
    return (Builder<Source>)this;
  }
}

We parametrize the Builder with R - the return type of the build() method. Initially, when new instance of the builder is created the parameter is set to be Void - e.g. non-existing. One can call various non-essential configuration methods that don't change the state (like the name one) - those still return the same builder with the same return type. If you try to use it as in following example, you'll get a compile time error:

Source src = Builder.newFromText("function hello() { print 'Hello'; }").
  name("hello.js").
  build(); // returns Void not Source!

Once the essential attribute mimeType is set, the same this is returned, but the system re-casts it to Builder<Source> - e.g. it indicates that the builder is now ready and can be used to create real Source object:

Source src = Builder.newFromText("function hello() { print 'Hello'; }").
  name("hello.js").
  mimeType("text/javascript").
  build();

The type information is really compile time only information - e.g. it gets erased during execution. Thus the performance of this API Design Pattern is the same of plain builder - but rather on relying on runtime exceptions saying something is missing - it co-operates with Javac to give the users early edit time/compile time error indications.

Initially Initialized

Of course, there can be other factory methods that can create the builder in a state that is already initialized. For example, if we create a Source from a real file on disk, we can ask the OS to guess the MIME type for us:

// define as
public static Builder<Source> fromFile({{JDK|java/io|File}} file) { /* ... */ }
 
// use it as
Source src = Builder.fromFile(new File("c:\\x.js")).build();

There can be a mixture of such finished or unfinished builder factory methods in the same class.

Drawbacks

There can be only a single return type in Java. As such this API pattern only works for a single essential builder property. One starts in the Void-state and once any important attribute is set one changes the return type to the real one (Source in the previous case). This trick cannot be repeated more than once.


Sometimes it makes sense to mix the ChameleonBuilder pattern with BuilderWithConditionalException. Then you end up with a builder with two generic types. That doesn't support cluelessness either - in general clueless programmers are scared by generics. On the other hand, in the typical usage, the generics aren't visible:

Source src = Builder.newFromText("function hello() { print 'Hello'; }").
  name("hello.js").
  mimeType("text/javascript").
  build();

The other problem is error reporting. The classical error is:

 Result of build() method cannot be assigned to src. Expecting Source got Void

This is a perfect indication that something is wrong. However the fix (e.g. one has to call mimetype method first) isn't completely obvious from the error message. Good Javadoc on the builder factory method or the build() method itself can fix that - however this is not as clueless solution is we might want.

Check ResistingBuilder to see a possible way to improve the ChameleonBuilder pattern.

Personal tools