ConfigurationObject

From APIDesign

Jump to: navigation, search

ConfigurationObject pattern is often used by JavaScript libraries to deal with evolution in a manageable way. While TheAPIBook advocates getting ready for the fact that first version of any API is never perfect, people keep repeating the same design mistake again and again: optimistically ignore the need for evolution! Usual history of an API starts with introducing function with one argument:

function upper(text) {
  return text.toUpperCase();
}
upper("Hello World!") === "HELLO WORLD!" || error();

then one finds an additional argument is needed:

function upper(text, firstLetterOnly) {
  if (firstLetterOnly) {
    return text.substring(0, 1).toUpperCase() + text.substring(1);
  }
  return text.toUpperCase();
}
upper("hello world!") === "HELLO WORLD!" || error();
upper("hello world!", true) === "Hello world!" || error();

and later another one, and another and so on, until one realizes the whole API is total mess and it is time to switch to ConfigurationObject design pattern:

function upper(data, firstLetterOnly) {
  if (typeof data === "string") {
      data = {
          "text" : data,
          "firstLetterOnly" : firstLetterOnly
      };
  }
  if (data.firstLetterOnly) {
    return data.text.substring(0, 1).toUpperCase() + data.text.substring(1);
  }
  return data.text.toUpperCase();
}
upper({ 
  "text" : "hello world!"
}) === "HELLO WORLD!" || error();
upper({ 
  "text" : "hello world!",
  "firstLetterOnly" : false
}) === "HELLO WORLD!" || error();
upper({ 
  "text" : "hello world!",
  "firstLetterOnly" : true
}) === "Hello world!" || error();

Adding named properties to ConfigurationObject is more easily evolvable. Moreover it is certainly easier to use ten named properties than a function with ten arguments that have to be specified in fixed order. No surprise the ConfigurationObject becomes more and more popular in many JavaScript libraries.

Now let's take a Java view. DukeScript (a way to use HTML from Java) is all about Java and JavaScript co-operation. To prevent reinventing the wheel the core of DukeScript ecosystem is built around wrapping JavaScript libraries with type-safe Java APIs. As ConfigurationObject is becoming more frequent, it is more and more important to find proper realization of such API in Java. Let's discuss the options.

Contents

JavaBeans like Style

JavaBean specification is popular in Java and using some familiar patterns (in this case GettersAndSetters) when designing own API will shorten the Time To Market and increase acceptable cluelessness of users of your API. The JavaBean style for the above example would look like:

public final class UpperConfig {
  private String text;
  private boolean firstLetterOnly;
  public void setText(String text) {
    this.text = text;
  }
  public String getText() {
    return text;
  }
  public void setFirstLetterOnly(boolean f) {
    this.firstLetterOnly = f;
  }
  public boolean isFirstLetterOnly() {
    return firstLetterOnly;
  }
}
public final class Upper {
  private Upper() {}
  public static String upper(UpperConfig c) {
    if (c.isFirstLetterOnly()) {
      return c.getText().substring(0, 1).toUpperCase() + c.getText().substring(1);
    }
    return c.getText().toUpperCase();
  }

The benefit is clear: GettersAndSetters are easily recognizable by Java developers. However the problem is the usage - using such API is way more verbose than the JavaScript version:

UpperConfig config = new UpperConfig();
config.setText("hello world!");
config.setFirstLetterOnly(true);
assert Upper.upper(config).equals("Hello World!");

What is the problem? One needs to create a local variable to hold the JavaBean instance and repeat çonfig.set for every property. While our fellow Java developers are probably OK with that, this kind of verbosity is found ridiculous by non-Java developers when they look at the Java code. Can we do better?

DukeScript Intermezzo

Before we leave the JavaBean style completely, let's explore easier way to write the same API. It is provided by the Html4Java API which is in core of DukeScript:

import net.java.html.json.*;
@Model(className="UpperConfig", properties={
  @Property(name="text", type=String.class),
  @Property(name="firstLetterOnly", type=boolean.class)
})
public final class Upper {
  private Upper() {}
  public static String upper(UpperConfig c) {
    if (c.isFirstLetterOnly()) {
      return c.getText().substring(0, 1).toUpperCase() + c.getText().substring(1);
    }
    return c.getText().toUpperCase();
  }
}

DukeScript optimizes the way to write JSON-like objects and expose them as JavaBeans. By harnessing the power of AnnotationProcessors, we save typing of tons of boilerplate code. Rather than writing the ConfigurationObject class manually we let the DukeScript processor to generate it when processing the three lines that define the Model annotation.

The client code however remains the same - e.g. while DukeScript helps us to write our API more easily, it does not (in this case) improve experience of users of our API. As there is many more users of the API than designers (usually just you), you should rather strive for optimizing user experience than making your life easier.

Builder Approach

There are two ways to avoid the repetition of config.set in Java. Either use CumulativeFactory or Builder pattern:

public final class Upper {
  private String text;
  private String firstLetterOnly;
 
  public Upper text(String text) {
    this.text = text;
    return this;
  }
 
  public Upper firstLetterOnly(boolean b) {
    this.firstLetterOnly = b;
    return this;
  }
 
  public String upper() {
    if (firstLatterOnly) {
      return text.substring(0, 1).toUpperCase() + text.substring(1);
    }
    return text.toUpperCase();
  }
}

All property modifying methods are type-safe and moreover by returning this from each of them we allow to chain the calls. The usage of this API then becomes comparable to the original usage of ConfigurationObject in JavaScript while having the type-safe benefits of Java:

String result = new Upper()
  .text("hello world!")
  .firstLetterOnly(true)
  .upper();
assert "Hello world!".equals(result);
 
 
result = new Upper()
  .text("hello world!")
  .upper();
assert "HELLO WORLD!".equals(result);

When to use Builder and when CumulativeFactory? In case the result is only needed before the build method is called and no longer after that, I would suggest to go with CumulativeFactory - the immutability of its object instances brings logical benefits to the system - one can reason about immutable system more easily than about mutable one.

If the values of the builder object are needed even after the build call - e.g. the system observes them and update some state based on their changes, it is of course necessary to choose mutable object. One preconfigures it, calls the build method and keeps reference to the object - to later modify it and change its behavior.

Summary

When converting ConfigurationObject pattern to Java choose CumulativeFactory in case build operation on the object is the final one or Builder-like pattern in case one wants to modify the object even after calling build operation. Consider using Html4Java @Model annotation once it is enhanced to support builder style as well:

import net.java.html.json.Model;
import net.java.html.json.Property;
 
@Model(className = "UpperConfig", builder = "", properties = {
    @Property(name = "text", type = String.class),
    @Property(name = "firstLetterOnly", type = boolean.class)
})
public final class Upper {
    public static String upper(UpperConfig c) {
        if (c.isFirstLetterOnly()) {
            return c.getText().substring(0, 1).toUpperCase() + c.getText().substring(1);
        }
        return c.getText().toUpperCase();
    }
 
    public static void main(String... args) {
        String result = upper(new UpperConfig()
            .text("hello world!")
            .firstLetterOnly(true)
        );
        assert "Hello world!".equals(result);
 
        result = upper(new UpperConfig()
            .text("hello world!")
        );
        assert "HELLO WORLD!".equals(result);
    }
 
}

An example of using CumulativeFactory to implement ConfigurationObject can be found in the Bck2Brwsr ahead of time compiler javadoc.

Personal tools
buy