Статьи

Groovy DSL — простой пример

Специфичные для домена языки (DSL) стали ценной частью Groovy. DSL используются в сборщиках Groovy, Grails и GORM, а также в средах тестирования. Для разработчика DSL являются потребляемыми и понятными, что делает реализацию более гибкой по сравнению с традиционным программированием. Но как реализован DSL? Как это работает за кулисами? Эта статья продемонстрирует простой DSL, который может дать толчок разработчикам по основным понятиям.

Что такое DSL

DSL предназначены для решения определенного типа проблем. Это короткие выразительные средства программирования, которые хорошо вписываются в узкий контекст. Например, с помощью GORM вы можете отобразить отображение спящего режима с помощью DSL, а не XML.

1
2
3
4
5
6
static mapping = {
  table 'person'
  columns {
    name column:'name'
  }
}

Большая часть теории DSL и преимуществ, которые они предоставляют, хорошо документированы. Обратитесь к этим источникам в качестве отправной точки:

Простой пример DSL в Groovy

В следующем примере предлагается упрощенный вид реализации внутреннего DSL. Фреймворки имеют гораздо более продвинутые методы создания DSL. Однако в этом примере освещаются концепции делегирования замыкания и протокола мета-объекта, которые необходимы для понимания внутренней работы DSL.

Обзор требований

Представьте, что клиенту нужен генератор заметок. Записки должны иметь несколько простых полей, таких как «to», «from» и «body». В заметке также могут быть разделы, такие как «Сводка» или «Важно». Сводные поля являются динамическими и могут быть любыми по требованию. Кроме того, заметка должна быть представлена ​​в трех форматах: xml, html и text.

Мы решили реализовать это как DSL в Groovy. Результат DSL выглядит следующим образом:

1
2
3
4
5
6
7
8
MemoDsl.make {
    to 'Nirav Assar'
    from 'Barack Obama'
    body 'How are things? We are doing well. Take care'
    idea 'The economy is key'
    request 'Please vote for me'
    xml
}

Вывод из кода дает:

1
2
3
4
5
6
7
<memo>
 <to>Nirav Assar</to>
 <from>Barack Obama</from>
 <body>How are things? We are doing well. Take care</body>
 <idea>The economy is key</idea>
 <request>Please vote for me</request>
 </memo>

Последняя строка в DSL также может быть изменена на «html» или «text». Это влияет на формат вывода.

Реализация

Статический метод, который принимает замыкание, — это простой способ реализации DSL. В примере памятки класс MemoDsl имеет метод make. Он создает экземпляр и делегирует все вызовы в замыкании экземпляру. Это механизм, в котором разделы «to» и «from» в конечном итоге выполняют методы внутри класса MemoDsl. После вызова метода to () мы сохраняем текст в экземпляре для последующего форматирования.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class MemoDsl {
 
    String toText
    String fromText
    String body
    def sections = []
 
    /**
     * This method accepts a closure which is essentially the DSL. Delegate the
     * closure methods to
     * the DSL class so the calls can be processed
     */
    def static make(closure) {
        MemoDsl memoDsl = new MemoDsl()
        // any method called in closure will be delegated to the memoDsl class
        closure.delegate = memoDsl
        closure()
    }
 
    /**
     * Store the parameter as a variable and use it later to output a memo
     */
    def to(String toText) {
        this.toText = toText
    }
 
    def from(String fromText) {
        this.fromText = fromText
    }
 
    def body(String bodyText) {
        this.body = bodyText
    }
}

Динамические разделы

Когда замыкание включает метод, которого нет в классе MemoDsl, groovy идентифицирует его как отсутствующий метод. С мета-объектным протоколом Groovy вызывается интерфейс methodMissing для класса. Вот как мы обрабатываем разделы для заметки. В коде клиента выше у нас есть записи для идеи и запроса.

1
2
3
4
5
6
7
8
MemoDsl.make {
    to 'Nirav Assar'
    from 'Barack Obama'
    body 'How are things? We are doing well. Take care'
    idea 'The economy is key'
    request 'Please vote for me'
    xml
}

Разделы обрабатываются с помощью следующего кода в MemoDsl. Он создает класс раздела и добавляет его в список в экземпляре.

1
2
3
4
5
6
7
8
/**
 * When a method is not recognized, assume it is a title for a new section. Create a simple
 * object that contains the method name and the parameter which is the body.
 */
def methodMissing(String methodName, args) {
 def section = new Section(title: methodName, body: args[0])
 sections << section
}

Обработка различных выходов

Наконец, самая интересная часть DSL — это то, как мы обрабатываем различные результаты. Последняя строка в замыкании указывает желаемый результат. Когда замыкание содержит строку типа «xml» без параметров, groovy предполагает, что это метод «getter». Таким образом, нам нужно реализовать getXml (), чтобы отследить выполнение делегирования:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/**
 * 'get' methods get called from the dsl by convention. Due to groovy closure delegation,
 * we had to place MarkUpBuilder and StringWrite code in a static method as the delegate of the closure
 * did not have access to the system.out
 */
def getXml() {
 doXml(this)
}
 
/**
 * Use markupBuilder to create a customer xml output
 */
private static doXml(MemoDsl memoDsl) {
 def writer = new StringWriter()
 def xml = new MarkupBuilder(writer)
 xml.memo() {
  to(memoDsl.toText)
  from(memoDsl.fromText)
  body(memoDsl.body)
  // cycle through the stored section objects to create an xml tag
  for (s in memoDsl.sections) {
   '$s.title'(s.body)
  }
 }
 println writer
}

Код для HTML и текста очень похож. Единственный вариант — как форматируется вывод.

Весь код

Код в полном объеме отображается далее. Наилучшим подходом, который я нашел, было сначала разработать клиентский код DSL и указанные форматы, а затем заняться реализацией. Я использовал TDD и JUnit для управления моей реализацией. Обратите внимание, что я не потратил лишних усилий, чтобы сделать утверждения на системном выводе в тестах, хотя это можно легко улучшить. Код полностью исполняемый внутри любой IDE. Запустите различные тесты для просмотра вывода DSL.

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
package com.solutionsfit.dsl.memotemplate
 
class MemolDslTest extends GroovyTestCase {
 
    void testDslUsage_outputXml() {
        MemoDsl.make {
            to 'Nirav Assar'
            from 'Barack Obama'
            body 'How are things? We are doing well. Take care'
            idea 'The economy is key'
            request 'Please vote for me'
            xml
        }
    }
 
    void testDslUsage_outputHtml() {
        MemoDsl.make {
            to 'Nirav Assar'
            from 'Barack Obama'
            body 'How are things? We are doing well. Take care'
            idea 'The economy is key'
            request 'Please vote for me'
            html
        }
    }
 
    void testDslUsage_outputText() {
        MemoDsl.make {
            to 'Nirav Assar'
            from 'Barack Obama'
            body 'How are things? We are doing well. Take care'
            idea 'The economy is key'
            request 'Please vote for me'
            text
        }
    }
}
 
package com.solutionsfit.dsl.memotemplate
 
import groovy.xml.MarkupBuilder
 
/**
 * Processes a simple DSL to create various formats of a memo: xml, html, and text
 */
class MemoDsl {
 
    String toText
    String fromText
    String body
    def sections = []
 
    /**
     * This method accepts a closure which is essentially the DSL. Delegate the closure methods to
     * the DSL class so the calls can be processed
     */
    def static make(closure) {
        MemoDsl memoDsl = new MemoDsl()
        // any method called in closure will be delegated to the memoDsl class
        closure.delegate = memoDsl
        closure()
    }
 
    /**
     * Store the parameter as a variable and use it later to output a memo
     */
    def to(String toText) {
        this.toText = toText
    }
 
    def from(String fromText) {
        this.fromText = fromText
    }
 
    def body(String bodyText) {
        this.body = bodyText
    }
 
    /**
     * When a method is not recognized, assume it is a title for a new section. Create a simple
     * object that contains the method name and the parameter which is the body.
     */
    def methodMissing(String methodName, args) {
        def section = new Section(title: methodName, body: args[0])
        sections << section
    }
 
    /**
     * 'get' methods get called from the dsl by convention. Due to groovy closure delegation,
     * we had to place MarkUpBuilder and StringWrite code in a static method as the delegate of the closure
     * did not have access to the system.out
     */
    def getXml() {
        doXml(this)
    }
 
    def getHtml() {
        doHtml(this)
    }
 
    def getText() {
        doText(this)
    }
 
    /**
     * Use markupBuilder to create a customer xml output
     */
    private static doXml(MemoDsl memoDsl) {
        def writer = new StringWriter()
        def xml = new MarkupBuilder(writer)
        xml.memo() {
            to(memoDsl.toText)
            from(memoDsl.fromText)
            body(memoDsl.body)
            // cycle through the stored section objects to create an xml tag
            for (s in memoDsl.sections) {
                '$s.title'(s.body)
            }
        }
        println writer
    }
 
    /**
     * Use markupBuilder to create an html xml output
     */
    private static doHtml(MemoDsl memoDsl) {
        def writer = new StringWriter()
        def xml = new MarkupBuilder(writer)
        xml.html() {
            head {
                title('Memo')
            }
            body {
                h1('Memo')
                h3('To: ${memoDsl.toText}')
                h3('From: ${memoDsl.fromText}')
                p(memoDsl.body)
                 // cycle through the stored section objects and create uppercase/bold section with body
                for (s in memoDsl.sections) {
                    p {
                        b(s.title.toUpperCase())
                    }
                    p(s.body)
                }
            }
        }
        println writer
    }
 
    /**
     * Use markupBuilder to create an html xml output
     */
    private static doText(MemoDsl memoDsl) {
        String template = 'Memo\nTo: ${memoDsl.toText}\nFrom: ${memoDsl.fromText}\n${memoDsl.body}\n'
        def sectionStrings =''
        for (s in memoDsl.sections) {
            sectionStrings += s.title.toUpperCase() + '\n' + s.body + '\n'
        }
        template += sectionStrings
        println template
    }
}
 
package com.solutionsfit.dsl.memotemplate
 
class Section {
    String title
    String body
}

Ссылка: Groovy DSL — простой пример от нашего партнера JCG Нирава Ассара в блоге Assar Java Consulting .