一、DSL了解
1、DSL介绍
DSL(Domain Specific Language)是针对某一领域,具有受限表达性的一种计算机程序设计语言。 常用于聚焦指定的领域或问题,这就要求 DSL 具备强大的表现力,同时在使用起来要简单。说到DSL,大家也会自然的想到通用语言(如Java、C等)。
为什么没有一种语言同时 兼具『简洁』和『业务表达』能力呢?
从信息论本质上来讨论这个问题,每个语言的程序都可以抽象为一个字符串,每个字符串由有限数量的合法字符组成,它在运行时会实现某个功能,因而可以看作是一种需求的信源编码。每种需求可以映射到一个或多个正确的程序,但一个程序肯定只对应到一种需求,因而程序包含的信息熵不低于需求的信息熵。而程序中不仅仅需要描述需求的信息,还需要包含 可读性、辩识度,如果是静态语言还需要 静态检查等额外信息。 这里也可以看出来,为什么DSL是特定领域的语言了。
2、DSL分类
最常见的分类方法是按照DSL的实现途径来分类。马丁·福勒曾将DSL分为内部和外部两大类,他的分类法得到了绝大多数业界人士的认可和沿袭。内部与外部之分取决于DSL是否将一种现存语言作为宿主语言,在其上构建自身的实现。
2.1、内部DSL
也称内嵌式DSL。因为它们的实现嵌入到宿主语言中,与之合为一体。内部DSL将一种现有编程语言作为宿主语言,基于其设施建立专门面向特定领域的各种语义。例如:Kotlin DSL、Groovy DSL等;
2.2、外部DSL
也称独立DSL。因为它们是从零开始建立起来的独立语言,而不基于任何现有宿主语言的设施建立。外部DSL是从零开发的DSL,在词法分析、解析技术、解释、编译、代码生成等方面拥有独立的设施。开发外部DSL近似于从零开始实现一种拥有独特语法和语义的全新语言。构建工具make 、语法分析器生成工具YACC、词法分析工具LEX等都是常见的外部DSL。例如:正则表达式、XML、SQL、JSON、 Markdown等;
3、DSL示例
3.1、内部DSL
HTML: 通过自然语言编写
在Groovy中,通过DSL可以用易读的写法生成XML
import groovy.xml.MarkupBuilder
def s = new StringWriter()
def xml = new MarkupBuilder(s)
xml.html{
head{
title("Hello")
script(ahref:'https://xxxx.com/vue.js')
}
body{
p("Excited")
}
}
println s.toString()
最后将生成
Excited
这里相对于Java这样的动态语言,最为不同的就是xml.html这个并不存在的方法居然可以通过编译并运行,它内部重写了invokeMethod方法,并进行闭包遍历,少写了许多POJO对象,效率更高。
3.2、外部DSL
以plantUML为例,外部DSL不受限于宿主语言的语法,对用户很友好,尤其是对于不懂宿主语言语法的用户。但外部DSL的自定义语法需要有配套的语法分析器。常见的语法分析器有:YACC、ANTLR等。
4、DSL & DDD(领域驱动)
DDD和DSL的融合有三点:
-
面向领域;
-
模型的组装方式;
-
分层架构演进;
DSL 可以看作是在领域模型之上的一层外壳,可以显著增强领域模型的能力。
它的价值主要有两个,一是提升了开发人员的生产力,二是增进了开发人员与领域专家的沟通。外部 DSL 就是对领域模型的一种组装方式。
5、DSL不是银弹
前开篇也提到了,在信息量不变的情况下,代码行数越短,它的“潜规则”信息量就越多,那么如何排查?如何定位?如何扩展?成为一个好的DSL需要考量的点。好的DSL难点在于:
-
DSL只是一种声明式的编程语言,无法承载大量业务。
-
DSL语句与编译生成的“字节码”的过程是黑盒的,不但对内部工作不明朗,如果报错的话,不但堆栈行数无法与源码对应上,而且无法“断点”或者“日志”。
-
DSL对设计者要求高,需要会一个领域有通透的理解,设计时要克制『增加各种特性』,DSL还要文档齐全,支撑充分,甚至要开源以帮助使用者定位。
二、有哪些工具
上节中提到,DSL分为内部和外部。由于外部DSL需要自己编写分析器,所以笔者使用 内部DSL实现。从之前收集的大量资料中,调研到 有两种比如轻量实现DSL的方式。
-
第一种:使用Groovy语言的元编程特性,天然支持DSL的下定义,而且兼容Java调用,生成的class更容易被JVM优化,执行性能上不会有太多损失。
-
第二种:使用Jetbrains MPS,开发基于java base的内部DSL。支持快速修复、智能提示、语法检查等。
https://www.jetbrains.com/zh-cn/mps/
下面我们将以:第一种 Groovy为基础语言,开发 内部DSL。
三、Groovy实战DSL
1、 实现原理
(1)闭包
官方定义是“Groovy中的闭包是一个开放,匿名的代码块,可以接受参数,返回值并分配给变量”
简而言之,他说一个匿名的代码块,可以接受参数,有返回值。在DSL中,一个DSL脚本就是一个闭包。
比如:
//执行一句话
{ printf 'Hello World' }
//闭包有默认参数it,且不用申明
{ println it }
//闭包有默认参数it,申明了也无所谓
{ it -> println it }
// name是自定义的参数名
{ name -> println name }
//多个参数的闭包
{ String x, int y ->
println "hey ${x} the value is ${y}"
}
每定义的闭包是一个Closure对象,我们可以把一个闭包赋值给一个变量,然后调用变量执行
//闭包赋值
def closure = {
printf("hello")
}
//调用
closure()
(2)括号语法
当调用的方法需要参数时,Groovy 不要求使用括号,若有多个参数,那么参数之间依然使用逗号分隔;如果不需要参数,那么方法的调用必须显示的使用括号。
def add(number) { 1 + number }
//DSL调用
def res = add 1
println res
也支持级联调用方式,举例来说,a b c d 实际上就等同于 a(b).c(d)
//定义
total = 0
def a(number) {
total += number
return this
}
def b(number) {
total *= number
return this
}
//dsl
a 2 b 3
println total
(3)无参方法调用
我们结合 Groovy 中对属性的访问就是对 getXXX 的访问,将无参数的方法名改成 getXXX 的形式,即可实现“调用无参数的方法不需要括号”的语法!比如:
def getTotal() { println "Total" }
//DSL调用
total
(4)MOP
MOP:元对象协议。由 Groovy 语言中的一种协议。该协议的出现为元编程提供了优雅的解决方案。而 MOP 机制的核心就是 MetaClass。
有点类似于 Java 中的反射,但是在使用上却比 Java 中的反射简单的多。
常用的方法有:
-
invokeMethod()
-
setProperty()
-
hasProperty()
-
methodMissing()
以下是一个methodMissing的例子:
detailInfo = [:]
def methodMissing(String name, args) {
detailInfo[name] = args
}
def introduce(closure) {
closure.delegate = this
closure()
detailInfo.each {
key, value ->
println "My $key is $value"
}
}
introduce {
name "zx"
age 18
}
(5)定义和脚本分离
@BaseScript 需要在注释在自定义的脚本类型变量上,来指定当前脚本属于哪个Delegate,从而执行相应的脚本命令,也使IDE有自动提示的功能:
脚本定义
abstract class DslDelegate extends Script {
def setName(String name){
println name
}
}
脚本:
import dsl.groovy.SetNameDelegate
import groovy.transform.BaseScript
@BaseScript DslDelegate _
setName("name")
(6)闭包委托
使用以上介绍的方法,只能在脚本里执行单个命令,如果想在脚本里执行复杂的嵌套关系,比如Gradle中的dependencies,就需要@DelegatesTo支持了,@DelegatesTo执行了脚本里定义的闭包用那个类来解析。
上面提到一个DSL脚本就是一个闭包,这里的DelegatesTo其实定义的是闭包里面的二级闭包的格式,当然如果你乐意,可以无限嵌套定义。
//定义二级闭包格式
class Conf{
String name
int age
Conf name(String name) {
this.name = name
return this
}
Conf age(int age) {
this.age = age
return this
}
}
//定义一级闭包格式,即脚本的格式
String user(@DelegatesTo(Conf.class) Closure closure) {
Conf conf = new Conf()
DefaultGroovyMethods.with(conf, closure)
println "my name is ${conf.name} my age is ${conf.age}"
}
//dsl脚本
user{
name "tom"
age 12
}
(7)Java加载并执行脚本
脚本可以在IDE里直接执行,大多数情况下DSL脚本都是以文本的形式存在数据库或配置中,这时候就需要先加载脚本再执行,加载脚本可以通过以下方式:
CompilerConfiguration compilerConfiguration = new CompilerConfiguration();
compilerConfiguration.setScriptBaseClass(DslDelegate.class.getName());
GroovyShell shell = new GroovyShell(GroovyScriptRunner.class.getClassLoader());
Script script = shell.parse(file);
给脚本传参数,并得到返回结果:
Binding binding = new Binding();
binding.setProperty("key", anyValue);
Object res = InvokerHelper.createScript(script.getClass(), binding).run()
2、Groovy DSL示例
(1)需求
假设我们要做一个备忘录生成器。备忘录有to、from、body 三个字段,它也可以包含如 Summary、Important等动态字段,最后它可以以 xml、html、text三种格式输出。
Groovy中DLS实现后的效果,如下:
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
}
输出结果如下(DSL的最后一行 决定输出格式,可以xml、html、text):
Nirav Assar
Barack Obama
How are things? We are doing well. Take care
The economy is key Please vote for me
(2)实现
定义接收类
MemoDsl类中make静态方法,会创建一个MemoDsl实例,并委托给闭包。后续to、from方法,将调用到MemoDsl实例上,在调用to()方法后,文本将保存在实例中,以便稍后使用。
class MemoDsl {
String toText
String fromText
String body
def sections = []
// mark方法需要接受一个闭包,并委托closure方法到memoDsl,所以DSL方法才能生效
def static make(closure) {
MemoDsl memoDsl = new MemoDsl()
// 任务调用到闭包的方法,都将委托给memoDsl实例
closure.delegate = memoDsl
closure()
}
// 将参数保存到变量中,以便稍后使用
def to(String toText) {
this.toText = toText
}
def from(String fromText) {
this.fromText = fromText
}
def body(String bodyText) {
this.body = bodyText
}
}
处理动态属性
当闭包包含了MemoDsl类不存在的方法时,groovy会将方法标识为缺失方法。它会通过groovy的元对象协议,调用到MemoDsl的methodMissing接口上。这也是我们能正确处理idea、request字段的原因。
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
}
处理缺失属性的方法如下:
// 当遇到缺失属性时,groovy通过元对象协议,调用methodMissing方法
def methodMissing(String methodName, args) {
def section = new Section(title: methodName, body: args[0])
sections << section
}
处理输出格式
最后,DSL输出各种格式呢?闭包中的最后一行指定了所需的输出。当闭包包含一个没有参数的字符串(如“xml”)时,groovy会假定这是一个“getter”方法。因此,我们需要实现“getXml()”来捕获委托执行:
// 指定xml、html、text时,默认会调用get...方法
def getXml() {
doXml(this)
}
// 使用MarkupBuilder输出xml
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)
// 循环创建 动态xml节点
for (s in memoDsl.sections) {
"$s.title"(s.body)
}
}
println writer
}
text和html的输出也类似。
完整代码
pom.xml添加依赖:
org.codehaus.groovy
groovy-all
3.0.10
pom
junit
junit
4.13.2
test
SimpleDslTest.groovy文件:
import groovy.test.GroovyTestCase
import org.junit.Test
class SimpleDslTest extends GroovyTestCase {
@Test
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
}
}
@Test
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
}
}
@Test
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
}
}
}
import groovy.xml.MarkupBuilder
// 简单DSL示例
class MemoDsl {
String toText
String fromText
String body
def sections = []
// mark方法需要接受一个闭包,并委托closure方法到memoDsl,所以DSL方法才能生效
def static make(@DelegatesTo(MemoDsl.class) Closure closure) {
MemoDsl memoDsl = new MemoDsl()
// 任务调用到闭包的方法,都将委托给memoDsl实例
closure.delegate = memoDsl
closure()
}
// 将参数保存到变量中,以便稍后使用
def to(String toText) {
this.toText = toText
}
def from(String fromText) {
this.fromText = fromText
}
def body(String bodyText) {
this.body = bodyText
}
// 当遇到缺失属性时,groovy通过元对象协议,调用methodMissing方法
def methodMissing(String methodName, args) {
def section = new Section(title: methodName, body: args[0])
sections << section
}
// 指定xml、html、text时,默认会调用get...方法
def getXml() {
doXml(this)
}
def getHtml() {
doHtml(this)
}
def getText() {
doText(this)
}
// 使用MarkupBuilder输出xml
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)
// 循环创建 动态xml节点
for (s in memoDsl.sections) {
"$s.title"(s.body)
}
}
println writer
}
// 使用MarkupBuilder输出html
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)
// 循环创建节点,并将 内容转换为大写 + 加粗
for (s in memoDsl.sections) {
p {
b(s.title.toUpperCase())
}
p(s.body)
}
}
}
println writer
}
// 使用字符串模板输出text格式
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
}
}
class Section {
String title
String body
}
-------------点个赞吧!!当你读到这里,代表这篇文章对你是有价值的,欢迎拍砖。-------------
参考资料
-
领域驱动设计 (DDD) 总结:https://cloud.tencent.com/developer/article/1662032
-
使用Groovy构建DSL:https://www.cnblogs.com/lesofn/p/14480455.html
-
DSL编程技术的介绍: https://juejin.cn/post/6844903506659262478
-
松本行弘推荐的代码生成书籍-《Code Generation in Action》
-
松本行弘对编程的杂谈-《松本行弘的程序世界》
-
知名度很高的自我修炼实践:《程序员修炼之道:从小工到专家》
-
Intellij开源的元编程工具: https://www.jetbrains.com/mps/
-
王垠对编辑器与IDE的一些思考 : http://www.yinwang.org/blog-cn/2013/04/20/editor-ide
-
为什么没有一种令绝大部分程序员满意的编程语言?https://www.zhihu.com/question/305131584/answer/566237106
-
DDD:DSL(领域专用语言):http://apframework.com/2019/12/21/ddd-dsl/
-
领域专用语言实战 (图灵程序设计丛书):https://book.douban.com/subject/25741352/
-
当DDD遇上DSL:https://www.itdks.com/Act/apply?id=3188
-
DDD:架构思想的旧瓶新酒:https://www.infoq.cn/article/K6AfHfMlx6IZqKwmpXcu
-
Groovy DSL A Simple Example:https://www.javacodegeeks.com/2012/08/groovy-dsl-simple-example.html