Creating DSLs in Java, Part 2: Fluency and context

Measure and improve these characteristics in your DSLs

1 2 3 4 Page 4
Page 4 of 4

A better PizzaBuilder

Let's look at some ways to improve the fluency and context of this API. We'll start with the Java language, then switch hats and see how to improve the API further using some Groovy constructs.

First, in order to improve the API's fluency, we'll rename its methods. Instead of traditional setter methods, we can name the methods based on intent. Second, in order to provide context and method chaining, we can rewrite the methods to automatically return the PizzaBuilder a user is working with. Here is the modified PizzaBuilder class (again the implementation of the methods is not shown):

Listing 15. Modified PizzaBuilder API

public class PizzaBuilder
{
  public PizzaBuilder prepareDough() { return this; }
  public PizzaBuilder addSauce(int amount) { return this; }
  public PizzaBuilder addCheese(int amount, String type) { return this; }
  public PizzaBuilder addToppings(String[] toppings) { return this; }
  public PizzaBuilder bake() { return this; }
  public Pizza get() { return null; }
}

Here's the new builder in action:

Listing 16. A better PizzaBuilder makes a better Pizza

public class UsePizzaBuilder
{
  public static void main(String[] args)
  {
    Pizza pizza = new PizzaBuilder()
      .prepareDough()
      .addSauce(2)
      .addCheese(2, "Mozzarella")
      .addToppings(new String[] {"Olives", "Tomatoes", "Bell Peppers"})
      .bake()
      .get();
  }
}

The above code is more fluent than the original. We've also cut out some noise by getting rid of the repeated instances of the bldr object reference. A user of this API starts with an instance of PizzaBuilder and ends up with an instance of Pizza, which is what he or she was really after.

A Groovy PizzaBuilder

Let's see how we can stretch the PizzaBuilder API's fluency and context even further in Groovy. First, take a look at this modified PizzaBuilder class written in Groovy:

Listing 17. PizzaBuilder written in Groovy

public class PizzaBuilder
{
  void prepareDough() { }
  void addSauce(int amount) {  }
  void addCheese(int amount, String type) { }
  void addToppings(String[] toppings) { }
  void bake() { }
  Pizza get() { return null; }

  static Pizza make(closure)
  {
    PizzaBuilder bldr = new PizzaBuilder();
    closure(bldr);
    return bldr.get();
  }
}

All instance methods except the get method are void methods (meaning they don't return this). We've also added a static method (make()) that takes care of creating an instance of the builder and sending it to the closure. A user of the API can focus on building a pizza instead of creating an instance of PizzaBuilder. Also, the API's user wouldn't have to call the get() method -- he could even remove it and move its function into the make method. Listing 18 shows the Groovy PizzaBuilder at work.

Listing 18. Groovy bakes a pizza

Pizza pizza = PizzaBuilder.make { bldr ->
        bldr.prepareDough()
        bldr.addSauce(2)
        bldr.addCheese(2, "Mozzarella")
        bldr.addToppings("Olives", "Tomatoes", "Bell Peppers")
        bldr.bake()
}

Put it in context

So far we've eliminated the need to call get (and hence the sequence or order of calling), but the API still lacks context. Here's where Groovy's with method comes in handy. Listing 19 rewrites make using the with method.

Listing 19. Groovy's with method creates context

static Pizza make(closure)
{
  PizzaBuilder bldr = new PizzaBuilder();
  bldr.with closure
  return bldr.get();
}
Closures and Java
The elegance of the Groovy examples comes partly from the use of closures. A process is underway to include closures in the Java language. In the future you will likely be able to use closures as part of your Java language syntax.

Do you understand what is going on here? Let's stop to understand what Groovy's with() method does. In essence, it makes a clone of the closure given to it, sets the delegate property to the target object (bldr, in this case) and then invokes the cloned closure. So now you might wonder what is the significance of the delegate property of a closure. If a closure can't find anyone to handle a method, it then, as a last attempt, asks the delegate (if set) if it can handle the method. So, if you simply call a method without any object reference, the delegate gets an opportunity to handle the call. (See Programming Groovy for more about delegates.)

Now, the code to make Pizza will look like this:

Listing 20. The fastest way to pizza yet!

Pizza pizza = PizzaBuilder.make {
        prepareDough()
        addSauce(2)
        addCheese(2, "Mozzarella")
        addToppings("Olives", "Tomatoes", "Bell Peppers")
        bake()
}

That is less noisy -- an implicit PizzaBuilder object is still being used, but the API user is not burdened by it. We can increase the API's fluency by removing the parentheses in the methods that take parameters (for now we're stuck with parentheses for methods that don't take parameters).

Listing 21. Remove parentheses in methods with parameters

Pizza pizza = PizzaBuilder.make {
        prepareDough()
        addSauce 2
        addCheese 2, "Mozzarella"
        addToppings "Olives", "Tomatoes", "Bell Peppers"
        bake()
}

As a last effort toward streamlining the API, it would be great to get rid of the double quotes. If the Cheese type and the toppings names are well known, we can do it easily. In the PizzaBuilder class, we define Mozzarella, Olives, Tomatoes, and Bell_Pepper (and so on) as properties, as shown in Listing 22.

Listing 22. One more step toward the fluent API

public class PizzaBuilder
{
  def Mozzarella = "Mozzarella"
  def Olives = "Olives"
  def Tomatoes = "Tomatoes"
  def Bell_Peppers = "Bell Peppers"
  //...

Now you can use the PizzaBuilder like so:

Listing 23. Spin the pizza!

Pizza pizza = PizzaBuilder.make {
        prepareDough()
        addSauce 2
        addCheese 2, Mozzarella
        addToppings Olives, Tomatoes, Bell_Peppers
        bake()
}

In conclusion

In this article you've learned about the concepts of fluency and context, and how they apply to domain-specific languages. We started with the simple exercise of seeing how to loop through a collection with less ceremony. You then saw how fluency and context work in different programming languages and APIs, and how they affect usability. Finally, we did an exercise in iteratively improving the user experience of an API. We gradually refined the internal DSL of the PizzaBuilder API, first in Java and then in Groovy, working back and forth to improve both context awareness and fluency. The end result is a Groovy-based PizzaBuilder API that lets the user make a Pizza with great cognitive ease and almost no redundant typing. We'll return to this example in a future article in this series, seeing how we can further improve the API). But for now ... aren't you feeling hungry for pizza?

Dr. Venkat Subramaniam has trained and mentored thousands of software developers in the U.S., Canada, Europe, and Asia. He helps his clients succeed with agile development. He's author of the book .NET Gotchas (O'Reilly), and coauthor of the 2007 Jolt productivity award-winning book Practices of an Agile Developer (Pragmatic Bookshelf). Venkat is a frequently invited speaker at international conferences. His latest book is Programming Groovy: Dynamic Productivity for the Java Developer (Pragmatic Bookshelf).

Learn more about this topic

This story, "Creating DSLs in Java, Part 2: Fluency and context" was originally published by JavaWorld.

Copyright © 2008 IDG Communications, Inc.

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