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.
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
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.
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.
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!
Dein Besuch auf unserer Website produziert laut der Messung auf websitecarbon.com nur 0,28 g CO₂.