Creating DSLs in Java, Part 2: Fluency and context

Measure and improve these characteristics in your DSLs

1 2 3 4 Page 2
Page 2 of 4

Groovy speaks

Groovy is a more fluent language than Java: it was designed that way. For instance, Groovy's internal iterators allow you to provide code blocks that execute in the context of each element of a collection. So, in Groovy you could use the each method to iterate over the elements of a collection, like so:

Listing 4. Easy iterating with Groovy's each method

names.each { name ->
 //...
}

Now that's fluent!

Fluent APIs

Once you are looking for it, you'll start noticing the fluency of APIs, as well as languages. One of my favorite fluent APIs is the EasyMock library. When setting up the behavior of a mock object, you can fluently tell it what method to expect and what response to provide, as in this example:

Listing 5. Fluent API: EasyMock

expect(alarm.raise()).andReturn(true);
expect(alarm.raise()).andThrow(new InvalidStateException());

In the above code fragment, you are coaching the alarm mock. You ask it to return a true in response to the first call to a raise() method. Also, you ask it to throw an exception in response to a subsequent call to the same method. The EasyMock code syntax may confuse you if you haven't seen something like it before. Once you get the hang of it, however, you will be tempted to write something like it for your own APIs.

Another example, from the JSONObject library, shows both fluency and context:

Listing 6. Fluency and context in JSONObject

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

The accumulate method acts on a JSONObject, then returns it so that subsequent calls can be chained.

In the Guice dependency injection framework, you can set up the binding of interfaces to classes as follows:

Listing 7. Fluent dependency injection in Guice

bind(Alerter.class)
.to(VisualAlerter.class)
.in(Scopes.SINGLETON);

All of the above examples are from different Java-based APIs, but you can observe two things they have in common. First, they all employ a terse, specialized syntax. Second, they all use method chaining, which means building on what's returned by a method call. Each of these APIs first establishes a context, and then builds on it.

Why context?

Conversation is always based on context. If two people enter a conversation without a shared context, they rarely leave it without having created one. Years of shared context allows many married couples to communicate without words. That sounds romantic, but old friends can do it too. For instance, say you see someone dressed like a character from a movie you watched recently with a friend. All you have to do is smile and a nod in the direction of that person, and your friend will probably know that you think the person is like the character from the movie. When two strangers really hit it off in a conversation, shared context is almost always the glue. By the same token, when a conversation simply cannot get off the ground, it may be because shared context is lacking.

Context also influences interpretation. For example, you might interpret the words "use a fork" differently depending on context. Someone could be telling a child to use a fork at the dinner table. You could be being instructed to use the fork function to create a child process on a Unix-like system. Or perhaps someone is being coached on a chess move. When it comes to interpretation, context is very important.

Like jargon, context reduces the signal-to-noise ratio in communication. In the best cases, it makes communication highly effective. It makes information easier to understand and build on. DSLs similarly benefit a great deal from context -- it is shared context that allows us to make our DSLs terse, highly expressive, easy to understand, and easy to work with.

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