Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

Building DSLs with Groovy

488 views

Published on

Presentation on DSLs with Groovy as delivered to the Chicago Groovy Users Group and UniForum

Published in: Technology
  • Be the first to comment

  • Be the first to like this

Building DSLs with Groovy

  1. 1. Building DSLs with Groovy: A Real World Case Study Sten Anderson
  2. 2. Agenda What is a DSL? Groovy’s DSL-friendly language features Case study: City of Chicago
  3. 3. What is a DSL?
  4. 4. DSL: Domain Specific Language Small, Specific “Mini-language” geared toward a Domain Favor conciseness and readability in a narrow domain over Turing Completeness External • Involve Lexer/Parser • Ant, SQL Internal (Embedded DSLs/Fluent Interfaces) • Defined inside a more general purpose language • Gant/Rake DSL vs. Framework vs. API? • What is OpenGL?
  5. 5. Why use Groovy to Write DSLs? Groovy is a natural extension for Java Developers • IDE support for “free” At a minimum Groovy is “less verbose Java” At a maximum Groovy lets you change the language itself • dynamic methods and properties and operator overloading Groovy has built-in DSLs (Builders)
  6. 6. Groovy’s DSL-Friendly Features Reduced Syntax Closures Metaprogramming • Dynamic methods and properties • ExpandoMetaclass
  7. 7. Language Feature Example: Text Adventure Interpreter
  8. 8. class Game { void go(dir) { println "You go $dir" } void look() { println "You look around and see nothing" } void take(it) { println "You take the $it" } ... }
  9. 9. Let’s Start with Java…
  10. 10. …And Make it Groovy… new GroovyShell().evaluate(“game.${playerInput}”) go(“north”); look(); take(“dagger”);
  11. 11. Reduced Syntax go “north” look() take “dagger”
  12. 12. Metaprogramming: Dynamic Properties class Game { ... def propertyMissing(String name) { name } go north look() take dagger
  13. 13. Metaprogramming: Dynamic Properties def propertyMissing(String name) { if (metaClass.respondsTo(this, name)) { this."$name"() } name } go north look take dagger
  14. 14. Remember This? new GroovyShell().evaluate(“game.${playerInput}”) Not very “Groovy”
  15. 15. Closures and “with” new GroovyShell().evaluate(“game.with{${playerInput}}”) 3.times { go north } look take dagger
  16. 16. One more thing... (the Synonym Problem) look > You see a dagger on the ground get dagger > I don’t understand that grab dagger > I don’t understand that Just let me have the %$!#@ dagger! > I don’t understand that
  17. 17. Metaprogramming: Dynamic Methods def methodMissing(String name, args) { if (["grab", “hold", "yoink"].contains(name)) { this.take(args[0]) } } look > You see a dagger on the ground grab dagger > You take the dagger You’re darn right I do! > I don’t understand that
  18. 18. def methodMissing(String name, args) { def funcName = ThesaurusLookup.contains(name) if (funcName) { Game.metaClass."$name" = { item -> this.”$funcName”(item) } this.”$name”(args[0]) } } Metaprogramming: ExpandoMetaclass
  19. 19. How Far We’ve Come go north look take dagger grab fish game.go(“north”); game.look(); game.take(“dagger”); game.take(“fish”); From this… To this…
  20. 20. Case Study: Budget Books for Fun and (non) Profit
  21. 21. The Challenge Replace Mainframe “Budget Book Generator” application Did not interact with the live budget data Over 3 hours to produce a book Very manual process (print the book, rescan as a PDF of images) Somewhat dated look
  22. 22. The Solution Java SE Swing Application deployed locally via Webstart Time to produce final PDF reduced from three hours to a few minutes Operated Directly with the Budget Data PDF as native output: searchable, indexable
  23. 23. BookSection PageElement Book Object Model
  24. 24. BookSection PageElement TextElement TableHeaderElement TableRowElement …
  25. 25. BookSection BookSection
  26. 26. TextElement TableHeader Separator TableSection TableRow TableTotal
  27. 27. How do we go from Budget Objects to Book Objects? •Departments •Organizations •Appropriations •Positions •BookSections •PageElements Can we write a DSL to create a book?
  28. 28. Building Drafts with ContentBuilder interface ContentBuilder - Draft build(ContentData)
  29. 29. ContentBuilder: First Attempt Java
  30. 30. BookSection bookSection = new BookSection ("Positions"); TableHeaderElement header = new TableHeaderElement(); header.addHeader(new TableHeader(0.05)); header.addHeader(new TableHeader(0.3, "Position", Formatter.LeftAligned)); bookSection.addPageElement(header); for (TableSection section : tableSections) { TableSectionElement sectionElement = new TableSectionElement(); sectionElement.addColumn(new TableColumn(2, section.getName()); bookSection.addPageElement(sectionElement); ...
  31. 31. BookSection bookSection = new BookSection ("Positions"); TableHeaderElement header = new TableHeaderElement(); Remember to fill out your Objects in Triplicate 1 2 3
  32. 32. BookSection bookSection = new BookSection ("Positions"); TableHeaderElement header = new TableHeaderElement(); header.addHeader(new TableHeader(0.05)); header.addHeader(new TableHeader(0.3, "Position", Formatter.LeftAligned)); bookSection.addPageElement(header); Declared here Added here
  33. 33. BookSection bookSection = new BookSection ("Positions"); TableHeaderElement header = new TableHeaderElement(); header.addHeader(new TableHeader(0.05)); header.addHeader( new TableHeader(0.3, "Position", Formatter.LeftAligned)); •What do these arguments mean? •What is “0.05”? •What is “Position”?
  34. 34. ContentBuilder: Second Attempt Java with method chaining
  35. 35. Method Chaining: Return the “Current” Object public void setWidth(double width) { this.width = width;} public TableHeader setWidth(double width) { this.width = width; return this; } •Instead of: •We have:
  36. 36. BookSection bookSection = new BookSection ("Positions"); bookSection.addPageElement(new TableHeader(). addHeader(new TableHeader().setWidth(0.05)). addHeader(new TableHeader(). setWidth(0.3). setName("Position"). setFormatter(Formatter.LeftAligned))); for (TableSection section : tableSections) { bookSection.addPageElement(new TableSectionElement(). addColumn(new TableColumn(). setColumnSpan(2). setName(section.getName())); ... }
  37. 37. Let’s Take a Step Back… Both Java versions “work”, but are verbose and not overly readable Need better support for building complex object graphs at the language level • The structure of the code should mirror the structure of the objects (declarative syntax) • Ceremony vs. Essence We need a “Builder”
  38. 38. Well, Groovy has Builders MarkupBuilder • Tag-based markup (XML, HTML) SwingBuilder • GUIs GraphicsBuilder • Java2D NodeBuilder • General Object graph (Nodes) MetaBuilder • Builder of Builders
  39. 39. MarkupBuilder def htmlBuilder = new MarkupBuilder() htmlBuilder.html { body { h1 "Hi There!" } } <html> <body> <h1>Hi There!</h1> </body> </html>
  40. 40. SwingBuilder def builder = new SwingBuilder() builder.frame (title:"Swing Builder Test", size:[300,100], location:[500,200]) { flowLayout() button(text:"Press Me") }.show()
  41. 41. groovy.util.BuilderSupport void setParent(Object parent, Object child); Object createNode(Object name); Object createNode(Object name, Object value); Object createNode(Object name, Map attributes); Object createNode(Object name, Map attributes, Object value);
  42. 42. createNode(Object name, Map attributes, Object value) h1 "Hi There!" button text:"Press Me“, visible:true
  43. 43. Reduced Syntax foo [a1:“val1”, b2:true] foo a1:“val1”, b2:true foo (a1:“val1”, b2:true, {bar()}) foo (a1:“val1”, b2:true) {bar()}
  44. 44. Instead of this: frame ([title:"Swing Builder Test"], { flowLayout() button(text:"Press Me") }) frame (title:"Swing Builder Test") { flowLayout() button(text:"Press Me") } You have this:
  45. 45. Better Still: FactoryBuilderSupport Used by SwingBuilder Register “Factories” that know how to create your objects Convenience AbstractFactory requires only one method: • Object newInstance ( FactoryBuilderSupport builder, Object name, Object value, Map attributes )
  46. 46. A Look Inside SwingBuilder public SwingBuilder() { ... registerFactory("button", new RichActionWidgetFactory(JButton)); ... }
  47. 47. ContentBuilder: Final Attempt
  48. 48. BookSection BookSection
  49. 49. TextElement TableHeader Separator TableSection TableRow TableTotal
  50. 50. BookSectionBuilder (extends FactoryBuilderSupport) public BookSectionBuilder() { registerFactory("bookSection", new BookSectionFactory()); registerFactory("textElement", new PageElementFactory(TextElement.class)); ... }
  51. 51. BookSectionFactory (extends AbstractFactory) public Object newInstance(FactoryBuilderSupport builder, Object name, Object value, Map attributes) { return new BookSection(attributes.get(“name”).toString()); } public void setParent( FactoryBuilderSupport builder, Object parent, Object child ) { ((BookSection) parent).newBookSection((BookSection) child); }
  52. 52. PageElementFactory (extends AbstractFactory) private Class<? extends PageElement> clazz; public Object newInstance(FactoryBuilderSupport builder, Object name, Object value, Map attributes) { return clazz.newInstance(); } public void setParent( FactoryBuilderSupport builder, Object parent, Object child ) { ((BookSection) parent).newPageElement((PageElement) child); }
  53. 53. Groovy Builder (Final Version) bookSection (name:"Positions") { tableHeaderElement(styleName:”header.large”, preTablePadding:10) { tableHeader width:0.05 tableHeader name:”Position”, width:0.3,formatter:LeftAligned } tableSections.each { section -> tableSectionElement (styleName:"table.subheader.1") { tableColumn columnSpan:2, section.name } ...
  54. 54. ... positions.each { position -> tableRowElement (styleName:"table.row") { tableColumn position.accountId tableColumn position.title } } tableTotalElement(styleName:"table.total") { tableColumn columnSpan:2, "Position Total" } } // table section tableFooterElement() } // book section
  55. 55. for (Position position : positions) { TableRowElement row = new TableRowElement(); row.setStyleName(“table.row”); row.addColumn(new TableColumn(position.getAccountId())); row.addColumn(new TableColumn(position.getTitle())); bookSection.newPageElement(row); } positions.each { position -> tableRowElement (styleName:"table.row") { tableColumn position.accountId tableColumn position.title } } Before: After:
  56. 56. Free Things “Built-In” to the Builder Wiring of map attributes to object properties Preservation of Object hierarchy Automatic management of closure delegates so that the “Builder” is always in scope
  57. 57. Closures: What’s a Delegate? def htmlBuilder = new MarkupBuilder() htmlBuilder.html { body { h1 "Hi There!" } } “html”, “body”, and “h1” don’t exist in scope, but rather are handled by the Builder htmlBuilder needs to be set as the closure’s Delegate
  58. 58. …But the Methods Don’t Exist in the Builder Either def xmlBuilder = new MarkupBuilder() xmoBuilder.invoices { invoice { lineitem ... } } The Builder “pretends” to have these methods invokeMethod is overridden to write out the markup or (in the case of SwingBuilder/BookSectionBuilder) dispatch the method to the correct Factory (Metaprogramming: Dynamic Methods)
  59. 59. Let’s Add Charting Support
  60. 60. First Write a Factory private static class ChartFactory extends AbstractFactory { public Object newInstance(...) { return new Chart(value.toString()); } public void setParent(...) { TableCell cell = (TableCell) parent; cell.addContent(child.toString()); } }
  61. 61. Then Register the Factory… public BookSectionBuilder() { registerFactory("bookSection", new BookSectionFactory()); registerFactory("textElement", new PageElementFactory(TextElement.class)); ... registerFactory("chart", new ChartFactory()); }
  62. 62. …And It’s Ready to use in the Builder tableRowElement { tableColumn (columnSpan:2) { chart dept.fundPercent } tableColumn { chart allFundTotal } }
  63. 63. Is a DSL Right for Me? Are the people who maintain your project different than those who initially developed it? DSLs give the “gift of documentation” Is your project part of a domain? Using Dynamic Languages like Groovy make DSLs more Painless
  64. 64. Resources Writing an Adventure game DSL in LISP: http://www.lisperati.com/casting.html http://groovy.codehaus.org/FactoryBuilderSupport http://docs.codehaus.org/display/GROOVY/MetaBuilder http://docs.codehaus.org/display/GROOVY/Writing+Domain-Specific+Languages http://www.agiledeveloper.com/blog/PermaLink.aspx?guid=ce021e6d-b0c2-4d83-8b69- c163c290983d http://blogs.citytechinc.com/sanderson/ King’s Quest and Zork images taken from Wikipedia.
  65. 65. “Groovy is like Gravy, except that it has the „Groove‟ in it.” -Meredith Anderson (5 Year Old)

×