Hopp til hovedinnhold

Teknologi / 5 minutter /

Readable, concise Java code with DSLs

DSLs, i.e. Domain Specific Languages are everywhere. As a system developer or an architect you are most likely using them almost every day, without even thinking about it. Or what do you think, do CSS, SQL, RegExp, Junit, or Hibernate Criteria API sound familiar? Yes, they all are examples of Domain Specific Languages. But how are those languages categorized and constructed, and is this something that I should really care about? In this article I will show some of the most common principles behind DSLs in Java, when their use should be considered, and benefits of internal DSLs in Java.

DSLs can be categorized into external- and internal-DSLs. External DSLs typically have their completely own syntax, and often require some external tools to be worked with. Some examples of external DSLs could be SQL, CSS or Regular Expressions. They are strictly focused on solving one problem domain, and are written with their own syntax. Internal DSLs instead are written with a host language, and can be seen as a special way of using the host language in a specific problem domain. They are often also called Fluent APIs or Embedded DSLs. When talking about Java and internal DSLs, we mostly see two different ways of utilizing DSLs; by using method chaining or nested functions.

Legacy code

The following example from a real-life project will illustrate basic principles behind DSL in Java with method chaining, and hopefully also give some ideas about advantages of internal DSLs in Java.

Let's start, and see what our legacy code looks like. We have a class called VisualField which is basically a POJO with some attributes, and then we have a Plugin-class LegacyPlugin which has a method returning a List of VisualFields.


This is a rather noisy method, which uses lots of temp-variables and has more internal state than is actually necessary.

Building with DSL

We then try to create a small DSL by method-chaining for building of VisualField-objects and apply it to the getVisualFields-method. When thinking().in(terms).of().method(chaining), we basically have 5 building blocks:

  1. Starting word - new Start() or start() (with static import)
  2. Mandatory word - start().mandatory(with optional params)
  3. Optional word - start().mandatory().optional(with optional params)
  4. Repeated word - start().mandatory().repeated().repeated()....
  5. Ending word - start().mandatory().....end()

By combining these blocks, it's possible to build a DSL of almost arbitrary complexity.

We will start our DSL with a class named VisualFieldDSL, which holds an instance of VisualField and has a static factory-method for constructing objects without new-keyword. It allows us to write:

Because VisualField-class has reasonable default values for all other attributes than "labelPosition" and "commentPosition", we have to have a mandatory step in our DSL for setting values to those attributes. This is achieved with a method named "place" which returns a new type called Placed . Now we can write f.ex.

and thus set the label to the right position and comment to the left position.

A common idiom with DSL in Java is to use static imports to improve readability, and for production of decent default values for method parameters. This is demonstrated in the Placed-class with the methods comment(Position) and label(Position). These methods are basically doing nothing, and give you merely an idea of how to use them, but allow us to go further with our DSL:

So far so good. We could stop here and just add a method returning VisualField and would have an object with all attributes initialized to some values. But we will extend our DSL to get a mechanism for adding spanning to the VisualField.

By adding new type Placed to our class hierarchy with the methods spanInput, spanLabel and spanComment which each return Placed type, we add a "repeated"-word to our grammar. It allows us to write:

in arbitrary order.

Or if you want to set all values by once, use helper methods:

These helper methods again return new type Spanning which gives possibility to set maxLength for VisualField, and also demonstrates how validation of DSL can be embedded. In practice, if maxlength attribute exceeds value 100, IllegalArgumentException will be thrown.

The last thing we need for completing our DSL is an ending statement. The class DSLTerminator which was returned from withMaxLength-method has several options for achieving this.

This then fulfills our DSL and returns an instance of the VisualField-class. One important thing to notice, is that spanning and maxLength were made optional with the following class-hierarchy:

This permits us to drop the statements for spanning and maxlength, or one of them, and go directly to the ending statement f.ex. :

Summary

To summarize the patterns we used here to construct our DSL:

  1. Define a new type for the starting word in the grammar.
  2. When in need of a mandatory word, the method returns a new type.
  3. The ending word in the grammar is a type which has a method returning instance of VisualField.
  4. An optional word is achieved by extending the type of ending word.
  5. A repeated word is a type with methods returning type of itself again.

Let's see how our new DSL-boosted version of getVisualFields looks like:

CONCLUSIONS

Internal DSL is best suited for situations with a limited and somewhat isolated problem domain. In Java, the most popular patterns for building DSLs are method-chaining or nested functions. Java is probably not the best language available for constructing internal DSLs, and has clearly it's own limitations with the syntax, but it's rather easy to get started with just some simple principles. When used correctly, internal DSL can communicate well also with people without a special knowledge of programming or problem domain. In our example we constructed a small DSL for building instances of VisualFields and achieved:

  1. Better readability
  2. Something easier to comprehend, also understandable for business people.
  3. Less temp-variables
  4. A more declarative style of code instead of imperative
  5. A higher level of abstraction.

And one more thing you were probably thinking about, fluent-API DSL is not considered as a violation of the law of Demeter :)