© Adam L. Davis 2019
Adam L. DavisLearning Groovy 3https://doi.org/10.1007/978-1-4842-5058-7_7

7. DSLs

Adam L. Davis1 
(1)
New York, NY, USA
 
Groovy has many features that make it great for writing DSLs (domain-specific languages):
  • Closures with delegates.

  • Parentheses and dots (.) are optional (command chains).

  • Ability to add methods to standard classes using Categories and Extension modules.

  • The ability to override many operators (plus, minus, etc.).

  • The methodMissing and propertyMissing methods.

Domain-specific languages can be useful for many purposes, such as allowing domain experts to read and write code or to clarify the meaning of business logic. They allow business experts to read or write code without having to be programming experts.

Closure with Delegate

Within Groovy you can take a block of code (a closure) as a parameter and then call it using a local variable as a delegate. For example, imagine you have the following code for sending SMS texts:
 1   class SMS {
 2           String from, to, body;
 3           def  from(String fromNumber) {
 4                   from = fromNumber
 5           }
 6           def  to(String toNumber) {
 7                   to = toNumber
 8           }
 9           def  body(String body) {
10                  this.body = body
11           }
12           def  send() {
13                   // send the text.
14           }
15   }
In Java, you’d need to use this the following way:
1   SMS m = new  SMS();
2   m.from("555-432-1234");
3   m.to("555-678-4321");
4   m.body("Hey there!");
5   m.send();
In Groovy you can add the following static method to the SMS class for DSL-like usage (block is expected to be a closure):
1   def static send(@DelegatesTo(SMS) Closure block) {
2           SMS m = new  SMS()
3           block.delegate = m
4           block()
5           m.send()
6   }
This sets the SMS object as a delegate for the block so that methods are forwarded to it. The (optional) @DelegatesTo(SMS) annotation tells the compiler and IDE what class is used as delegate to the closure. Now you can do the following:
1   SMS.send {
2           from '555-432-1234'
3           to '555-678-4321'
4           body 'Hey there!'
5   }

This removes a lot of repetition from the code.

../images/426440_2_En_7_Chapter/426440_2_En_7_Figa_HTML.gif Tip

As demonstrated, you can omit the parentheses when making a simple method call.

Command Chains

As noted earlier, Groovy has support for command chains which allow you to completely omit parenthesis and dots when making method calls with one or more parameters.

For example, let’s take the previous class for sending SMS messages but convert it to support the command chain syntax.
 1   class SMS {
 2           String from, to, body;
 3           SMS  from(String fromNumber) {
 4                   from = fromNumber; return this
 5           }
 6           SMS  to(String toNumber) {
 7                   to = toNumber; return this
 8           }
 9           SMS  body(String body) {
10                   this.body = body; return this
11           }
12           def  send() { /* send the text */ }
13           def static send(@DelegatesTo(SMS) Closure block) {
14           /* same as before */ }
15   }
Now our DSL syntax can be used like the following:
1   SMS.send {
3           from '555-432-1234' to '555-678-4321'
4              body 'Hey there!'
5   }

Overriding Operators

In Groovy you can override operators simply by naming your methods using the English word for the operator. For example, plus for + and minus for -. See the following table for more operators:

Operator

Method Name

+

plus

-

minus

*

multiply

/

div

%

mod

**

power

|

or

&

and

ˆ

xor

<<

leftShift

>>

rightShift

++

next

--

previous

For more complex operators, the variables “a”, “b”, and “c” are used to demonstrate how they are used in the following table (where “a” is an instance of the class defining the method):

Example

Method Declaration

a()

call()

a as b

asType(b)

a[b]

getAt(b)

a[b] = c

putAt(b,c)

+a

positive()

-a

negative()

~a

bitwiseNegate()

b in a

isCase(b)

For example, let’s create a class called Logic with a Boolean value and define the and and or methods.
 1   class  Logic  {
 2       boolean value
 3       Logic(v) {this.value = v}
 4       def and(Logic other) {
 5           this.value && other.value
 6       }
 7       def or(Logic other) {
 8           this.value || other.value
 9       }
10   }
Then, let’s use these methods and see if they work like we would expect:
1   def  pale = new  Logic(true)
2   def old = new Logic(false)
3
4   println "groovy truth: ${pale && old}" //true
5   println "using and: ${pale & old}" // false
6   println "using or: ${pale | old}" // true

Notice that using the built-in && operator uses “Groovy truth” and returns true because both variables are non-null.

Next, let’s try defining the leftShift and minus operators on a class:
 1   class Wizards {
 2       def list = []
 3       def leftShift(person) { list.add person }
 4       def  minus(person) { list.remove person }
 5       String toString() { "Wizards: $list" }
 6   }
 7   def  wiz = new  Wizards()
 8   wiz << 'Gandolf'
 9   println wiz // Wizards: [Gandolf]
10   wiz << 'Harry'
11   println wiz // Wizards: [Gandolf, Harry]
12   wiz - 'Harry'
13   println wiz // Wizards: [Gandolf]
You can also implement the putAt and getAt methods that allow you to use the bracket syntax. For example:
1   def value = wiz[1] // uses getAt(1)
2   wiz[1] = value // uses putAt(1, value)

This can be useful when writing for a domain that uses the bracket notation.

Missing Methods and Properties

As noted previously, Groovy provides a way to implement functionality at runtime via the methodMissing method :
1   def methodMissing(String name, args)

However, Groovy also provides a way to intercept missing properties that are accessed using Groovy’s property syntax. Property access is implemented using propertyMissing(String name) (which returns a value) and property modification via propertyMissing(String name, Object value) (which sets the value for a property).

For example, here’s an excerpt from a DSL for chemical compounds:
 1   class  Chemistry  {
 2     public static void exec(Closure block) {
 3       block.delegate = new  Chemistry()
 4       block()
 5     }
 6     def propertyMissing(String name) {
 7       def  comp = new  Compound(name)
 8       (comp.elements.size() == 1 && comp.elements.values()[0]==1) ?
 9         comp.elements.keySet()[0] : comp
10     }
11   }
Given the following code for Compound and Element:
// Represents a chemical Element
class Element  {
      String symbol
      Element(s) { symbol = s }
      double getWeight() {symbol=='H' ? 1.00794 : 15.9994}
      String toString() { symbol }
}
// Represents a chemical Compound
class Compound {
      final Map elements = [:]
      Compound(String str) {
                   def matcher = str =~ /([A-Z][a-z]∗)([0-9]+)?/
              while (matcher.find()) add(
                          new Element(matcher.group(1)),
                          (matcher.group(2) ?: 1) as Integer)
      }
      void add(Element e, int num) {
            if (elements[e]) elements[e] += num
            else elements[e] = num
      }
      double getWeight() {
            elements.keySet().inject(0d) { sum, key ->
                          sum + (key.weight * elements[key])
            }
      }
      String toString() { "$elements" }
}
In this example, propertyMissing (on lines 6-9) creates a new Compound object and returns either the Compound or an Element object if there is only one element in the Compound. This enables the creation of Compounds based on the name of a missing property . For example:
1   def  c = new  Chemistry()
2   def water = c.H2O
3   println water // [H: 2, O: 1]
4   println water.weight // 18.01528

This is interpreted as trying to access a property named H2O, which triggers the propertyMissing method.

../images/426440_2_En_7_Chapter/426440_2_En_7_Figb_HTML.gif Info

H2O refers to the chemical composition of water, which is two hydrogen atoms and one oxygen atom.

By using the static exec method, this DSL reaches its full potential by exposing an instance of Chemistry as a delegate to the closure, which allows for the following example:
1   Chemistry.exec {
2           def water = H2O
3           println water
4           println water.weight
5   }

This has the same effect of creating the H2O compound by calling the propertyMissing method of Chemistry.

The full code for Groovy Chemisty1 is provided on GitHub. It provides the ability to compute the atomic weights of chemical compounds and percentages by atomic weight. It includes all known elements, their names, and atomic weights.

This DSL would be very difficult to implement without the help of Groovy. In Java, for example, you would need to use strings to represent compounds, polluting the syntax with tons of quotes and parentheses.

Extension Modules

Extension modules in Groovy allow you to add functionality to existing classes to a project by including a library. It works much like Categories but works everywhere without requiring a use clause.

To create an Extension module, create a file named org.codehaus.groovy.runtime.ExtensionModule under the META-INF/services/ directory. Within that file, list all of your extensionClasses and staticExtensionClasses. For example, create such a file with the following contents:
moduleName=adamldavis-groovy-dsl-example
moduleVersion=0.1-beta1
extensionClasses=com.adamldavis.gdsl.MyDSLExtension
staticExtensionClasses=com.adamldavis.gdsl.MyStaticDSLExtension

When included in a project, Groovy will look at all of the static methods of the MyDSLExtension class (in the com.adamldavis.gdsl package) and use them to add functionality to instances of the first parameter of each method’s class. The moduleName and moduleVersion properties are only there to make sure multiple conflicting versions of the library are not loaded.

For example, given the following code, the String class would essentially have the upper() method added to it.
class MyDSLExtension {
      static String upper(String it) { it.toUpperCase() }
}
Likewise, Groovy will look at all of the static methods of the MyStaticDSLExtension class and use them to add functionality to the class of the first parameter of each method. For example, the following code would add a method, boom, to the String class allowing String.boom() to return “!”:
class MyStaticDSLExtension {
      static String boom(String it) { "!" }
}

Note that for staticExtensionClasses, the given parameter (it in the preceding code) cannot be used within the method; it is only used for the type.

../images/426440_2_En_7_Chapter/426440_2_En_7_Figc_HTML.gif Exercise

Create a DSL in Groovy for something that interests you, be it sports, math, movies, or astrophysics.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset