Musings on Language and Design Creating a Domain Specific Language with Groovy by Jeremy Meyer May 8, 2010
Summary
An simple example of how easy it is to make a fairly powerful DSL using the dynamic features and powerful sytax of Groovy
Advertisement
DSLs with Groovy
Creating Domain Specific Languages with Groovy
In my last blog I talked about some of the differences between Groovy
and Java syntax and argued that anyone making a paradigm shift from
Java to dynamic languages and DSLs (domain specific languages) would
actually have a fairly easy time of it with Groovy because of its
seamless integration with Java. The road to discovering all of Groovy's
powerful and "cool" features has the soft grassy verge of Java
compatibility. This means that (whilst not ideal) you can experiment
and take a bit of a risk with the new language without blowing project
deadlines or running into feature deprivation. The worst case scenario
is that you can do things in the Java way, until you can refactor to
Groovy paradigms.
I also said that Groovy syntax, cool features and operators (like the
spread operator) are not necessarily the real reason you should start
using Groovy, but the dynamism and the ability to make DSLs is. I want
to demonstrate what that dynamism means, and how one might create a
tiny DSL (using a Grails-like feature as an example).
To make a DSL useful, we want to end up with a syntax that is functional enough, simple to read and simple to write. Let's look at how
Grails does this with a validation DSL in its domain classes. Don't worry if you don't
use or know anything about Grails, this is an elegantly simple example and speaks for itself.
class Customer {
int age
String name
static constraints = {
age(size:18..65)
name(size:3..20, blank:false)
}
}
Without thinking about what language it is written in, try
and intuit what it might mean. We have a Customer class with two
properties, age and name, and some sort of named block, "constraints"
with matching age and name listed. Each property name is followed by
brackets, inside of which are some descriptions of constraints for
those properties. This is very intuitive. It is pretty obvious that the
age should be between 18 and 65 and the name should be between 3 and 20
characters and shouldn't be blank. Provided someone gave you a list of
allowable constraints (size, blank, zero, date etc.) you would be able
to quickly and easily create your own class with defined validation.
But is this Groovy?
The answer is yes and no. It is actually valid Groovy syntax, but as it
stands, it is somewhat meaningless and somewhat useless. Right now,
syntactically, we have a class definition which has two properties, and
a static closure. The closure contains calls to two fictitious methods,
age() and name() which both take maps as parameters. What do I mean by
fictitious methods? Simply that syntactically we have two method calls,
but to methods which don't exist. If we were to run that closure, we
would just get the following error:
groovy.lang.MissingMethodException: No signature of method: Customer$__clinit__closure1.age() is applicable for argument types: (java.util.LinkedHashMap) values: [[size:18..65]]
By the way, understanding closures is fundamental to using Groovy
effectively. There is a lot of information about them available online.
If you are unsure about them, a few web searches will put you straight.
Otherwise, just think of them as blocks of code which can be passed
around in parameters and run when required. I will talk about them a
bit more below.
So what good is this phoney code? Well, the syntax looks clean and tidy
and has clear affordance, i.e. it shows you intuitively what it should
do. Groovy's friendly map syntax (using comma seperated key/value pairs) gives us neat looking parameter lists. This makes it an effective DSL. It is a language-within-a-language which kind of has its own syntax and structure based in the domain of
constraining values. That makes programming with constraints very easy;
easy to remember, easy to code, easy to read and understand.
But.. it doesn't do anything yet, and since we have a closure
with method calls we can't really use, some clever work needs to be
done to make this DSL work for us. Let's look at some of the main
Groovy mechanisms we need.
Method Interception
One of the dynamic features of Groovy is that it can intercept methods.
The simplest way to do this is to override the invokeMethod method
on your class. In its simplest usage, every time you call a
non-existent method on your object, invokeMethod will
be called first, and passed the method name and the parameter list. The
example below just prints the name of the method being called.
class Worker {
Object invokeMethod(String name, Object args) {
println “${name} method called.”
}
..
}
Using this simple technique, if a method actually does exist, that
method will be called, and the invokeMethod
will not. See the GroovyInterceptable
interface for details of how to implement more complex
behaviour.
Dynamic Invocation
As a corollary to Method Interception, Groovy can call methods and get
and set properties dynamically. You can call a method by calling the invokeMethod method
directly, and you can set and get properties by using setProperty, as in clown.setProperty(“nose”, “red”),
the dynamic equivalent of
clown.setNose(“red”). Doing this in a language like Java
would require much more code.
Closures and Delegates
Since a closure is a defined block of code that is not actually run at
the time it is defined, it needs to be able to keep track of its
definition-time state. The Groovy mechanism for this is the delegate.
When you define a closure, it keeps a reference to this, its “birthday”
context in its delegate property. A method call in a closure, will
resolve to the closure's delegate by default. If you want your closure
to call a method on an object other than the one which defined it, you
have to set the delegate of your closure to point to that object.
Putting them together
Lets look at the code for a simple validator:
class Validator { def subject public void validate(def o) { subject = o o.constraints.setDelegate(this) o.constraints.call() println "Validation complete." }
Object invokeMethod(String name, Object args) { def val = subject.getProperty(name) args[0].each { switch(val?.class) { case null: if (it.key =="blank" && !val) println "failed: property '${name}' is null." break case Integer : if (it.key == "size" && !(it.value.contains(val))) println "failed: Integer property '${name}' has value '${val}' not in range '${it.value.inspect()}'." break case String: if (it.key =="size" && !it.value?.contains(val.length())) println "failed: String property '${name}' has value '${val}' not in length range '${it.value.inspect()}'." break default: break } } } }
The validate method references the object's constraints closure, sets
the delegate of the closure to point to itself and then runs the
closure. This is where the interesting stuff happens. The closure calls
the two methods "age" and "name". These are called on the validator
object (which is the new delegate of the closure). Since the methods do
not exist on the validator object, the invokeMethod handler is called, which is
where the validation actually takes place.
In invokeMethod, we can find out from the parameters the name of the method that was
attempting to be called and get the value of the property with that name, in the line:
def val = subject.getProperty(name)
This is where the "language" comes into play. We are using bogus method names to point us to properties which need validating.
We can use Groovy's default exception behaviour here to handle the case
where the method name in the constraint closure doesn't match a
property name. If the property is not found, Groovy will throw a groovy.lang.MissingPropertyException.
Once we have the property value, we look at the other arguments, the
maps, which were passed to our bogus method call. They contain the
key/value pairs "size:" and "blank:". We
can make good use of Groovy's range syntax here for easy validation:
it.value.contains(val)
In the switch statement we can check the
actual property value for its type and so allow for different
validation behaviour (i.e. size for a String means something different
from size for an Integer). Note that we can use the safe navigation
operator, "?." to ensure that if the value is null, we will catch it in
the switch statement in the null case, rather than throw a null pointer
exception:
switch(val?.class)
That's It!
About 30 lines of code later, we have a (somewhat primitive) validator,
but one which allows us to use a Domain Specific Language approach to
define our constraints. Groovy's closures, powerful operators and
syntax and its easy introspection have made it a fairly simple task.
To use our validator, we create a Customer, create a Validator and call the validate method. Note that you are more likely to want a static validation method on the Validator class if you want to use this for production code, but this demonstrates the point.
c = new Customer(age: 2, name:null)
v = new Validator()
v.validate(c)
If we run this we get:
size validation failed: Integer property 'age' has value '2' not in range '18..65'.
blank validation failed: property 'name' is null.
Validation complete.
Jeremy has been designing and developing software for over 20 years, as well as teaching its mastery. He is fascinated by all aspects of architecture, design and development, the philosophical, the psychological and the aesthetic. He currently heads up the training division at hybris Software, a fast growing and very exciting eCommerce company.