SETT
SETT

The Power of Groovy DSLs

by James Kleeh, Software Engineer

November 2016

In this article, I will show you what a DSL is, how it works, and how you can create your own DSL to improve your productivity.

DSL stands for Domain Specific Language. It is not a new concept; people have been creating DSLs for a long time now. The reason I am writing this article is to hopefully show you a couple tricks that you may not be aware of.

What is a DSL?

There is a larger definition of a domain specific language, however in the context of Groovy code, a DSL is a way of creating APIs that leverages Groovy's closures to create an easy way to build complex data. To understand how a DSL works, you must understand how closures work.

Closures

If you are familiar with Groovy at all, then you are also likely familiar with closures. Closures are the gateway to functional programming in Groovy. They represent executable pieces of code that may be passed as arguments to other methods.

  1. void someMethod(Closure c) {
  2. println "Inside someMethod"
  3. c.call()
  4. }
  5. Closure closure = {
  6. println "I'm inside a closure"
  7. }
  8. someMethod(closure)
  9.  
  10. //The following will be output to the console
  11. Inside someMethod
  12. I'm inside a closure

Closures also have a couple other properties that are essential to building DSLs. The first is the delegate. You can assign an instance of a class to be the "delegate" of the closure. What that means is that, by default, if you attempt to execute a method inside the closure, and it is not found, then it will look for that method in the delegate. In this next example, the append method will be called on the instance of StringBuilder that we have assigned to be the closure's delegate, because we don't have an append method defined in the scope of the closure.

  1. Closure closure = {
  2. append("I'm inside a closure")
  3. }
  4. StringBuilder sb = new StringBuilder()
  5. closure.delegate = sb
  6. closure.call()
  7.  
  8. assert sb.toString == "I'm inside a closure"

If a method called append does exist, that method will instead be called.

  1. void append(String text) {
  2. println text
  3. }
  4. Closure closure = {
  5. append("I'm inside a closure")
  6. }
  7. StringBuilder sb = new StringBuilder()
  8. closure.delegate = sb
  9. closure.call()
  10.  
  11. assert sb.toString == ""
  12. //The following will be output to the console
  13. I'm inside a closure

Fortunately, we have control over that behavior. We can tell the closure what order it should look for methods to execute. We do that with the resolveStrategy property of the closure.

  1. void append(String text) {
  2. println text
  3. }
  4. Closure closure = {
  5. append("I'm inside a closure")
  6. }
  7. StringBuilder sb = new StringBuilder()
  8. closure.delegate = sb
  9. closure.resolveStrategy = Closure.DELEGATE_FIRST
  10. closure.call()
  11.  
  12. assert sb.toString == "I'm inside a closure"

In the above example, we set the resolve strategy of our closure to tell Groovy we would like to search for methods on our delegate, before the "owner" of our closure.

Possible resolve strategies include:

A closure combined with a delegate and resolve strategy is the cornerstone of the type of DSL this article discusses.

Creating a DSL

For many of the examples here, I refer to creating an Excel document. I have recently created a great DSL to build Excel documents with support for many things including merged cells, formulas, and cell styling. It is a perfect example because it took a complicated API, the Apache POI library, and converted it into something that may be learned in a few minutes for most use cases. Take a look at my Excel Builder library to see if it is something that may be useful to you!

I would also like to highlight another fantastic library created by Craig Burke that allows you to build PDFs and word documents using a DSL: Document Builder

To understand why creating a DSL may be beneficial, take a look at the simplest example possible to create a workbook, sheet, row, and cell in Apache POI.

  1. import org.apache.poi.xssf.usermodel.XSSFWorkbook
  2. import org.apache.poi.xssf.usermodel.XSSFSheet
  3. import org.apache.poi.xssf.usermodel.XSSFRow
  4. import org.apache.poi.xssf.usermodel.XSSFCell
  5.  
  6. XSSFWorkbook wb = new XSSFWorkbook()
  7. XSSFSheet sheet = wb.createSheet()
  8. XSSFRow row = sheet.createRow(0)
  9. XSSFCell cell = row.createCell(0)
  10. cell.setCellValue("A1")

The power of a Groovy DSL transforms that into this:

  1. import com.jameskleeh.excel.ExcelBuilder
  2.  
  3. ExcelBuilder.build {
  4. sheet {
  5. row {
  6. cell("First Cell")
  7. }
  8. }
  9. }

With this new code, it is no longer necessary to understand the Apache POI API or keep track of our row and cell indexes. In addition, I believe the DSL made our code much easier to understand, especially to someone who is not familiar with Apache POI. Let's take a look at how this was done.

  1. class ExcelBuilder {
  2. static XSSFWorkbook build(Closure callable) {
  3. XSSFWorkbook wb = new XSSFWorkbook()
  4. callable.resolveStrategy = Closure.DELEGATE_FIRST
  5. callable.delegate = new Workbook(wb)
  6. callable.call()
  7. wb
  8. }
  9. }
  10.  
  11. class Workbook {
  12. private final XSSFWorkbook wb
  13.  
  14. Workbook(XSSFWorkbook wb) {
  15. this.wb = wb
  16. }
  17.  
  18. XSSFSheet sheet(Closure callable) {
  19. XSSFSheet sheet = wb.createSheet()
  20. callable.resolveStrategy = Closure.DELEGATE_FIRST
  21. callable.delegate = new Sheet(sheet)
  22. callable.call()
  23. sheet
  24. }
  25. }
  26.  
  27. class Sheet {
  28. private final XSSFSheet sheet
  29. private int rowIdx
  30.  
  31. Sheet(XSSFSheet sheet) {
  32. this.sheet = sheet
  33. this.rowIdx = 0
  34. }
  35.  
  36. XSSFRow row(Closure callable) {
  37. XSSFRow row = sheet.createRow(rowIdx)
  38. callable.resolveStrategy = Closure.DELEGATE_FIRST
  39. callable.delegate = new Row(row)
  40. callable.call()
  41. rowIdx++
  42. row
  43. }
  44. }
  45.  
  46. class Row {
  47. private final XSSFRow row
  48. private int cellIdx
  49.  
  50. Row(XSSFRow row) {
  51. this.row = row
  52. this.cellIdx = 0
  53. }
  54.  
  55. XSSFCell cell(Object value) {
  56. XSSFCell cell = row.createCell(cellIdx)
  57. cellIdx++
  58. cell.setCellValue(value.toString())
  59. cell
  60. }
  61. }

The above classes are an extremely simplified version of the actual implementation; however, it is enough to demonstrate the basic usage. The above classes abstract away Apache POI and manage for us the row and cell indexes. With some reference documentation, any developer should be able to create extremely simple excel documents within minutes.

Problems

Using DSLs comes with a couple of drawbacks of which you should be aware.

One of the major problems with DSLs created in the past (and some created today) is that it is often difficult or impossible for the IDE to know about what methods are available to be executed. The IDE has no idea what class will be assigned to the closures delegate. Because of that, you are often left with reading the documentation to discover the different method signatures that a given method has.

In addition to IDE support, performance may also be a concern if you are doing processor intensive tasks. Groovy code is typically slower than Java and that may be a factor in deciding how to write your code in some cases. A common solution is to annotate your class with @CompileStatic. However, if you are using a DSL like the above, it will not compile, because the compiler doesn't know which delegate will be assigned in the future. 

The following class will fail compilation in Groovy 2.4.7:

  1. @CompileStatic
  2. class Bar {
  3.  
  4. void foo(Closure x) {
  5. x.delegate = new StringBuilder()
  6. x.call()
  7. }
  8.  
  9. void doIt() {
  10. foo({
  11. append('u')
  12. })
  13. }
  14.  
  15. }
  16.  
  17. new Bar().doIt()

The Solution

Groovy 2.1 included an annotation to assist with all of the problems described above. The DelegatesTo annotation may be added to the Closure parameter to denote what class will be assigned to the delegate. Adding that annotation will allow your code to statically compile, as well as allow your IDE (if it has good Groovy support) to auto complete methods. Here is what that looks like:

  1. @CompileStatic
  2. class Bar {
  3.  
  4. void foo(@DelegatesTo(StringBuilder) Closure x) {
  5. x.delegate = new StringBuilder()
  6. x.call()
  7. }
  8.  
  9. void doIt() {
  10. foo({
  11. append('u')
  12. })
  13. }
  14.  
  15. }
  16.  
  17. new Bar().doIt()

Now our example code will compile just fine and will perform essentially the same as regular Java code.

I hope this article has introduced you to some pretty cool features of Groovy, and that you will use this new knowledge to create tools to improve the productivity of your team!

 

The Software Engineering Tech Trends is a monthly newsletter featuring emerging trends in software engineering.

Subscribe

© Copyright Object Computing, Inc. 1993, 2016. All rights reserved

WebSanity Top Secret