This article describes one way to create an own "Domain Specific Language" (DSL) in Java/Groovy. A DSL is a programming language designed to solve a specific issue very efficiently. For a more elaborated explanation I recommend the article on Wikipedia.
By definition a DSL can be applied to many areas. To make this article easier to follow, I limit its scope a bit. Nonetheless the principles and examples are similar in other areas.
DSL support in Groovy
Writing an own programming language seems like a lot of tedious work. Fortunately this is not the case using Groovy as a base.
A Groovy-based DSL is compiled by the groovy compiler. So there is nothing to do for that other than that the DSL must share syntax with Groovy. The pre-compilation of DSL code and its compilation at runtime are supported as well, though this article covers only the pre-compilation case.
DSL for bootstrapping and configuration
In several projects we use DSLs for very advanced configuration and for the bootstrapping of the application. Usually this kind of configuration is written by the application developers rather than the customer.
An open source Webserver named Ratpack uses the same approach. Check out the Ratpack configuration DSL.
The DSL-configurations are very compact and readable and fit the task 100% (otherwise we would update the DSL). Still adding new keywords to the DSL is rather simple.
Hello World
To execute the examples you can copy-paste them into the GroovyConsole.
class HelloWorldService {
// this function becomes a keyword in our DSL
void printHelloWorld() {
println("Hello World")
}
}
class HelloWorldScript {
static void execute(Closure script) {
// here we wire the colure with the service
script.delegate = new HelloWorldService()
// search the delegate for variables and methods
// if they do not exist search the scope of the closures definition
script.resolveStrategy = Closure.DELEGATE_FIRST
// the closure is executed in the context of its delegate
script()
}
}
HelloWorldScript.execute {
printHelloWorld()
for(def i = 0; i < 10; i++) {
printHelloWorld()
}
}
Output
Hello World
...
Hello World
Hello World with syntax highlighting
IDEAs with good Groovy support provide syntax highlighting and auto-completion for DSLs. To give a hint to the IDEA on which delegate a closure is executed, a special annotation exists: @DelegatesTo. The annotation has no additional effect.
class HelloWorldService {
void printHelloWorld() {
println("Hello World")
}
}
class HelloWorldScript {
// by annotating the parameter we gain syntax highlighting and auto-completion
static void execute(@DelegatesTo(strategy = Closure.DELEGATE_FIRST, value = HelloWorldService) Closure script) {
script.resolveStrategy = Closure.DELEGATE_FIRST
script.delegate = new HelloWorldService()
script()
}
}
HelloWorldScript.execute {
printHelloWorld()
for(def i = 0; i < 10; i++) {
printHelloWorld()
}
}
Bouquets
Usually we want the DSL to have a return value rather than print to standard out, e.g. a model containing the application's configuration.
// our configuration model
class BouquetConfiguration {
private final List<String> flowers = []
// method for use at runtime
int howMany(String flower) {
return flowers.count { it == flower }
}
// DSL keyword for use at DSL execution
void flower(String flower) {
flowers << flower
}
String toString() {
return "Bouquet: " + flowers.toString()
}
}
class Bouquet {
static BouquetConfiguration create(
@DelegatesTo(strategy = Closure.DELEGATE_FIRST, value = BouquetConfiguration) Closure script
) {
def bouquet = new BouquetConfiguration()
script.resolveStrategy = Closure.DELEGATE_FIRST
script.delegate = bouquet
script()
return bouquet
}
}
def bouquet = Bouquet.create {
flower "cornflower"
flower "cornflower"
flower "poppy"
flower "calendula"
}
println bouquet
Output
Bouquet: [cornflower, cornflower, poppy, calendula]
Bouquets even better
The prior Bouquet-DSL has a bad touch: the BouquetConfiguration serves two purposes. On the one hand it serves as data model and on the other it implements keywords for our DSL. In this small example it does not seem like a big deal, but mixing those concerns leads to problems in more complex situations:
- it is harder to decide which method can be used in which context
- it messes up the auto-completion
- it pins the DSL to this model
In theory the DSL can be executed in different ways, e.g. to create mocks during test setups.
Long story short: I recommend to keep the model and the DSL separate.
// our configuration model
class BouquetConfiguration extends ArrayList<String> {
int howMany(String flower) {
return this.count { it == flower }
}
String toString() {
return "Bouquet: " + this.toString()
}
}
// our DSL keyword
class BouquetConfigurationDsl {
private final BouquetConfiguration bouquet
BouquetConfigurationDsl(BouquetConfiguration bouquet) {
this.bouquet = bouquet
}
void flower(String flower) {
bouquet << flower
}
}
class Bouquet {
static BouquetConfiguration create(
@DelegatesTo(strategy = Closure.DELEGATE_FIRST, value = BouquetConfigurationDsl) Closure script
) {
def bouquet = new BouquetConfiguration()
script.resolveStrategy = Closure.DELEGATE_FIRST
script.delegate = new BouquetConfigurationDsl(bouquet)
script()
return bouquet
}
}
def bouquet = Bouquet.create {
flower "cornflower"
flower "cornflower"
flower "poppy"
flower "calendula"
}
println bouquet
Output
Bouquet: [cornflower, cornflower, poppy, calendula]
Florist with nesting
Now let's extend the Bouquet-example to create florists offering different bouquets. We want to nest DSL-blocks inside of other DSL-blocks, in our case Bouquet-configuration inside Florist-configuration.
class BouquetConfiguration extends ArrayList<String> {
int howMany(String flower) {
return this.count { it == flower }
}
String toString() {
return "Bouquet: " + this.toString()
}
}
class FloristConfiguration {
final Map<String, BouquetConfiguration> catalog = [:]
String toString() {
return "Florist: " + catalog.toString()
}
}
class BouquetConfigurationDsl {
private final BouquetConfiguration bouquet
BouquetConfigurationDsl(BouquetConfiguration bouquet) {
this.bouquet = bouquet
}
void flower(String flower) {
bouquet << flower
}
}
class FloristConfigurationDsl {
private final FloristConfiguration florist
FloristConfigurationDsl(FloristConfiguration florist) {
this.florist = florist
}
BouquetConfiguration bouquet(
String name,
@DelegatesTo(strategy = Closure.DELEGATE_FIRST, value = BouquetConfigurationDsl) Closure script
) {
def bouquet = florist.catalog[name]
if (bouquet == null) {
bouquet = florist.catalog[name] = new BouquetConfiguration()
}
script.resolveStrategy = Closure.DELEGATE_FIRST
script.delegate = new BouquetConfigurationDsl(bouquet)
script()
return bouquet
}
}
class Florist {
static FloristConfiguration create(
@DelegatesTo(strategy = Closure.DELEGATE_FIRST, value = FloristConfigurationDsl) Closure script
) {
def florist = new FloristConfiguration()
script.resolveStrategy = Closure.DELEGATE_FIRST
script.delegate = new FloristConfigurationDsl(florist)
script()
return florist
}
}
def florist = Florist.create {
bouquet "meadow", {
flower "cornflower"
flower "cornflower"
flower "poppy"
flower "calendula"
}
bouquet "roses", {
flower "rose"
flower "rose"
flower "rose"
flower "rose"
}
// add another calendula to existing bouquet
bouquet "meadow", {
flower "calendula"
}
}
println florist
Output
Florist: [madow:[cornflower, cornflower, poppy, calendula, calendula], roses:[rose, rose, rose, rose]]
Conclusion
Our past experience with DSLs for bootstrapping and advanced configuration has been very good. Personally I think the benefits outweigh the drawbacks when creating a DSL based on Groovy.
Benefits
- a DSL can make code very compact and readable
- very convenient for complex configuration
- changes to model not necessarily invalidate DSL (stable API)
- easy and quick to implement
Drawbacks
- Groovy syntax is mandatory
- script can contain any Java/Groovy code => not wise to let any user upload a script
Let us know your experiences with DSLs or any comments you have on Twitter!