We want our Calculator to be decoupled from the way it is used, so nothing in this class indicates that it will be used from a script.
We can execute scripts by using the GroovyShell class, which as an evaluate method that parses a script and runs it.
Let's look at how to use Calculator implicitly from a script.
The first way is by passing variables in a groovy.lang.Binding, which the shell implicitly exposes to the script:
The drawback is that we have to set each method explicitly as a closure inside the binding. We can do this automatically for all methods of Calculator:
But somehow it feels like there should be a better way.
Script subclassThe second way is by subclassing groovy.lang.Script, which implements our DSL by defining the DSL methods. As we already have them implemented inside the Calculator class, we can use the groovy.lang.Delegate annotation on a Calculator instance:
This makes all public instance methods (not properties!) of Calculator available within CalculatorScript, and thus within the DSL, because the DSL script will be part of CalculatorScript.
The code becomes:
The drawback is that this script class cannot be instantiated by ourselves, so any context for our Calculator cannot be passed directly to it. Our Calculator is stand-alone, but a real-world DSL probably isn't. A possibility is to parse the script into the script instance, pass our context to it, and then run the script:
Still, this does not feel right, because the Script class is really an implementation detail that we don't want to be concerned about. And it's likely the case that our DSL contains nested contexts, which will be implemented differently, namely through a delegate set on a Closure.
ClosureWhat we want is to implement the top level DSL context in the same way as nested contexts: Through a closure delegate, like this:
The 'with' method is available for each Object and sets it as a delegate of the closure and executes it.
How can we achieve this? We need a closure, but our script is a String. Somehow we have to convert the string to a closure and pass it to calculate().
What we can do is to force the DSL user to wrap his script inside a call to a method that accepts a closure. The script itself will look like this:
We need to define the calculate method in the script class or set it as a closure in the binding.
But the user has to wrap each of his scripts inside a call to calculate. We can do this for him though, as we will see next.
We can wrap the script inside a 'calculate' call ourselves, but why not get rid of the calculate method too? We can achieve our goal of converting the script into a closure by wrapping it as a closure like this:
The result of evaluate is a Closure. The return statement is used to disambiguate between a code block and a closure. Now we can call the script by calling the closure:
The only requirement is that the closure should accept a parameter, as the delegate is also passed as a parameter by the 'with' method. If this is undesirable, we can just run the closure ourselves:
The closure doesn't need to be cloned as we have just instantiated it ourselves. Because we don't pass any parameters to the closure, our wrapper can be defined to have no parameters, so we don't expose the 'it' variable to our script.
The calculate method becomes:
Now we can execute our script as a string in a Calculator context, by calling calculate:
Another advantage to the Binding and Script base class solutions is that properties inside Calculator are also available inside the script. And Calculator can implement dynamic properties and methods with propertyMissing/methodMissing or getProperty/setProperty/invokeMethod, just like normal Groovy builders.