Creating DSLs in Java, Part 2: Fluency and context

Measure and improve these characteristics in your DSLs

1 2 3 4 Page 3
Page 3 of 4

Context at work

Context was an important element in the method chaining examples I showed you earlier. For instance, you could write the JSONObject example as follows:

Listing 8. JSONObject example 1

JSONObject json = new JSONObject();
json.accumulate("key1", "value1");
json.accumulate("key2", "value2");

But there is so much unnecessary noise in the repeated use of the json object reference. Instead, the following code builds on the context of the object returned by new.

Listing 9. JSONObject example 2

JSONObject json = new JSONObject()
.accumulate("key1", "value1")
.accumulate("key2", "value2");

Context may appear unconventional

When following the traditional Java beans convention, you typically write getters, which are query methods that return some value, and setters, which are mutators or modifiers that perform actions but don't return anything (void methods). In order to realize the context, you break away from the convention of command-query separation. Your mutators, in addition to performing their actions, return this or the self object. It is like bending words and grammar just enough to make a poem rhyme.

Languages like JavaScript and Groovy have facilities to build context so you don't have to break with convention. The with method, used by both Groovy and JavaScript, is one such facility. For example, consider the following example using a Java ArrayList:

Listing 10. ArrayList in Java

java.util.ArrayList<String> cart = new java.util.ArrayList<String>();

cart.add("Milk");
cart.add("Juice");
cart.add("Apple");

System.out.printf("My cart has %d items.", cart.size());

Now I'll re-write the example using the with method:

Listing 11. In Groovy context is implicit

cart = []

cart.with {
  add "Milk"
  add "Juice"
  add "Apple"

  println "My cart has $size items."
}

In the above example, I've used the same good old java.util.ArrayList but in Groovy the context is implicit. Calls to add and size are automatically routed to the cart object -- Groovy's with method sets that context.

Here's the same example written using JavaScript:

Listing 12. ArrayList is JS

cart = new java.util.ArrayList()

with(cart)
{
  add("Milk")
  add("Juice")
  add("Apple")

  println("My cart has " + size() + " items.")
}

Creating a fluent, context-aware DSL

As a person authoring a class, you are often concerned with state and behavior. As a person using an API, you are often concerned with getting your work done. These two concerns tend to conflict with each other, though they don't have to. This is where a fluent, context-aware DSL can help a great deal.

Suppose you want to create a class for making pizza (a PizzaBuilder class -- see Builder Pattern in the Resources section). Following the traditional Java syntax, your API might look like this:

Listing 13. PizzaBuilder API

public class PizzaBuilder
{
  public void setDough() {}
  public void setSauce(int amount) {}
  public void setCheese(int amount, String type) {}
  public void setToppings(String[] toppings) {}
  public void bake() {}
  public Pizza get() { return null; }
}

(I've left the method implementations blank because we are interested in the API or method interfaces, not implementation details.)

Here is an example of using the PizzaBuilder:

Listing 14. PizzaBuilder makes a Pizza

public class UsePizzaBuilder
{
  public static void main(String[] args)
  {
    PizzaBuilder bldr = new PizzaBuilder();
    bldr.setDough();
    bldr.setSauce(2);
    bldr.setCheese(2, "Mozzarella");
    bldr.setToppings(new String[] {"Olives", "Tomatoes", "Bell Peppers"});
    bldr.bake();
    Pizza pizza = bldr.get();
  }
}

The above example has a few problems. First, it has no context, hence the repeated use of the bldr object reference. Second, there is an imposed ordering: the get() method must be the last method to be called. Ordering is a necessary evil in some scenarios, though it would be great if you could avoid it altogether. In cases where you cannot avoid ordering, you can enforce it with tact, by using method chaining to set the return types of methods. Users of your API will then have to follow a certain method order, but doing so will come naturally.

1 2 3 4 Page 3
Page 3 of 4
How to choose a low-code development platform