class Calculator { double hypotenuse(double width, double height) { Math.sqrt(width * width + height * height) } }
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.
Binding
The first way is by passing variables in a groovy.lang.Binding, which the shell implicitly exposes to the script:
def calculator = new Calculator() def binding = new Binding(hypotenuse: calculator.&hypotenuse) def shell = new GroovyShell(binding) assert 5.0 == shell.evaluate("hypotenuse(3.0, 4.0)")
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:
def calculator = new Calculator() def binding = new Binding() calculator.metaClass.methods.each { method -> def name = method.name binding.setVariable(name, InvokerHelper.getMethodPointer(calculator, name)) } def shell = new GroovyShell(binding) assert 5.0 == shell.evaluate("hypotenuse(3.0, 4.0)")
But somehow it feels like there should be a better way.
Script subclass
The 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:abstract class CalculatorScript extends Script { @Delegate final Calculator calculator = new Calculator() }
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:
def config = new CompilerConfiguration() config.scriptBaseClass = CalculatorScript.name def shell = new GroovyShell(config) assert 5.0 == shell.evaluate("hypotenuse(3.0, 4.0)")
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:
def config = new CompilerConfiguration() config.scriptBaseClass = CalculatorScript.name def shell = new GroovyShell(config) def script = shell.parse("hypotenuse(3.0, 4.0)") // script.calculator.context = ... assert 5.0 == script.run()
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.
Closure
What we want is to implement the top level DSL context in the same way as nested contexts: Through a closure delegate, like this:def calculate(Closure script) { Calculator calculator = new Calculator() calculator.with(script) }
The 'with' method is available for each Object and sets it as a delegate of the closure and executes it.
assert 5.0 == calculate { hypotenuse(3.0, 4.0) }
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:
calculate { hypotenuse(3.0, 4.0) }
We need to define the calculate method in the script class or set it as a closure in the binding.
def calculator = new Calculator() def binding = new Binding() binding.setVariable('calculate') { script -> calculator.with(script) } def shell = new GroovyShell(binding) assert 5.0 == shell.evaluate("calculate { hypotenuse(3.0, 4.0) }")
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.
Solution
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:
Closure convertToClosure(String script) { (Closure) new GroovyShell().evaluate("return {$script}") }
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:
def calculator = new Calculator() def script = convertToClosure("hypotenuse(3.0, 4.0)") assert 5.0 == calculator.with(script)
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:
def runInContext(Object context, String script) { Closure cl = (Closure) new GroovyShell().evaluate("{->$script}") cl.delegate = context cl.resolveStrategy = Closure.DELEGATE_FIRST cl() }
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:
def calculate(String script) { def calculator = new Calculator() runInContext(calculator, script) }
Now we can execute our script as a string in a Calculator context, by calling calculate:
assert 5.0 == calculate("hypotenuse(3.0, 4.0)")
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.
No comments:
Post a Comment