DSL 的全称是领域特定语言(Domain Specific Language),它是编程语言赋予开发者的一种特殊能力,通过它我们可以编写出一些看似脱离其原始语法结果的代码,从而构建出一种专有的语法结构。
不管你有没有察觉到,其实长久以来你一直都在使用DSL 。 比如我们想要在项目中添加一些依赖库,需要在app/build.gradle 文件中编写如下内容:
dependencies {
implementation 'com.squareup.retrofit2:retrofit:2.6.1'
implementation 'com.squareup.retrofit2:converter-gson:2.6.1'
}
Gradle 是一种基于Groovy 语言的构建工具,因此上述的语法结构其实就是Groovy 提供的DSL 功能。借助Kotlin 的DSL ,我们也可以实现类似的语法结构,下面就具体看一下吧。
首先新建一个DSL.kt 文件,然后在里面定义一个Denpendency 类,代码如下所示:
class Dependency{
val libraries = ArrayList<String>()
fun implementation(lib:String){
libraries.add(lib)
}
}
这里我们使用了一个List 集合来保存所有的依赖库,然后又提供了一个implementation() 方法,用于向List 集合中添加依赖库,代码非常简单。
接下来再定义一个dependencies 高阶函数,代码如下所示:
fun dependencies(block: Dependency.() -> Unit): List<String> {
val dependency = Dependency()
dependency.block()
return dependency.libraries
}
可以看到,dependencies 函数接收一个函数类型参数,并且该参数是定义到Dependency 类中的,因此调用它的时候需要先创建一个Dependency 的实例,然后再通过该实例调用函数类型参数,这样传入的Lambda 表达式就能得到执行了。最后,我们将Dependency 类中保存的依赖库集合返回。
没错,经过这样的DSL 设计之后,我们就可以在项目中使用如下的语法结构了:
dependencies {
implementation ('com.squareup.retrofit2:retrofit:2.6.1')
implementation ('com.squareup.retrofit2:converter-gson:2.6.1')
}
简单解释一下。由于dependencies 函数接收一个函数类型参数,因此这里我们可以传入一个Lambda 表达式。而此时的Lambda 表达式中拥有Dependency 类的上下文,因此当然就可以直接调用Dependency 类中的 implementation() 方法来添加依赖库了。
当然,这种语法结构和我们在build.gradle 文件中使用的语法结构并不完全相同,这主要是因为Kotlin 和 Groovy 在语法层面还是有一定差别的。
另外,我们也可以通过dependencies 函数的返回值来获取所有添加的依赖库,代码如下所示:
fun main() {
val libraries = dependencies {
implementation ("com.squareup.retrofit2:retrofit:2.6.1")
implementation ("com.squareup.retrofit2:converter-gson:2.6.1")
}
for (lib in libraries) {
println(lib)
}
}
这里可以用一个libraries 变量接收 dependencies 函数的返回值,然后使用 for-in 循环将集合中的依赖库全部打印出来。现在运行一下main() 函数,结果如图所示:
可以看到,我们已经成功使用DSL 语法结构添加的依赖库全部获取到了。
在实现了一个较为简单的DSL 之后,接下来我们再尝试编写一个复杂一点的DSL 。
如果你了解一些前端开发的话,应该知道网页的展示都是由浏览器解析HTML 代码来实现的。HTML 中定义了很多标签,其中
标签用于创建一个表格,标签用于创建表格的行, 表格用于创建单元格。将这3中标签嵌套使用,就可以定制出包含任意行列的表格了。这里我们来做个试验吧,首先创建一个test.txt 文件,并在其中编写如下HTML 代码:
<table>
<tr>
<td>Apple</td>
<td>Grape</td>
<td>Orange</td>
</tr>
<tr>
<td>Pear</td>
<td>Banana</td>
<td>Watermelon</td>
</tr>
</table>
这段代码会创建出一个两行三列的表格。那么要如何进行验证呢?很简单,修改一下文件的后缀名就可以了,这里将文件改名成 test.html,然后双击文件,使用浏览器打开即可,效果如图所示:
这就是一个两行三列表格的效果,只是默认情况下表格边框的宽度是零,所以我们看不到边框而已。
那么如果现在有一个需求,要求我们在Kotlin 中动态生成表格所对应的HTML 代码,最简单直接的方式就是字符串拼接了,但是这种做法显然十分烦琐,而且字符串拼接的代码也难以阅读。
这个时候DSL 又可以大显身手了,借助DSL ,我们可以以一种不可思议的语法结构来动态生成表格所对应的HTML 代码,下面就来看一下具体应该如何实现吧。
仍然是在DSL.kt 文件中进行编写,首先定义一个Td 类,代码如下所示:
class Td {
var content = ""
fun html() = "\n\t\t<tb>$content</tb>"
}
由于 标签是一个单元格,其中必然是要包含内容的,因此这里我们使用了一个content 字段来存储单元格中显示的内容。另外,还提供了一个html() 方法,当调用这个方法时就返回一段 标签的HTML 代码,并将content 存储的内容拼接进去。注意,为了让最终输出的结果更加直观,我使用了\n和 \t 转义符来进行换行和缩进,当然你可以不加这些转义符,因为浏览器在解析HTML 代码时是忽略换行和缩进的。
完成了Td 类,接下来我们定义一个Tr 类,代码如下所示:
class Tr {
private val children = ArrayList<Td>()
fun td(block:Td.() -> String){
val td = Td()
td.content= td.block()
children.add(td)
}
fun html():String{
val builder = StringBuilder()
builder.append("\n\t<tr>")
for (childTag in children) {
builder.append(childTag.html())
}
builder.append("\n\t</tr>")
return builder.toString()
}
}
Tr 类相比Td 类就要复杂一些了。由于 标签表示表格的行,它是可以包含多个标签的,因此我们首先创建了一个children 集合,用于存储当前Tr 所包含的Td 对象。接下来提供了一个td() 函数,它接收一个定义到Td 类中并且返回值是String 的函数类型参数。当调用td() 函数时,会先创建一个Td 对象,接着调用函数类型参数并获取它的返回值,然后赋值到Td 类的content 字段当中,这样就可以将调用td() 函数时传入的Lambda 表达式的返回值赋值给content 字段了。当然,这里既然创建了一个Td 对象,就一定要记得将它添加到children 集合当中。
另外,Tr 类也定义了一个html() 方法,它的作用和刚才Td 类中的html() 方法一致。只是由于每个Tr 都可能会包含多个Td,因此我们需要使用循环来遍历children 集合,将所有的子Td 都拼接到标签当中,从而返回一段嵌套的HTML 代码。
定义好了Tr 类之后,我们现在就可以使用如下的语法格式来构建表格中的一行数据:
val tr = Tr()
tr.td { "Apple" }
tr.td { "Grape" }
tr.td { "Orange" }
好像已经有那么回事了,但这仍然不是我们追求的最终效果。那么接下来继续对DSL 进行完善,再定义一个Table 类,代码如下所示:
class Table {
private val children = ArrayList<Tr>()
fun tr(block:Tr.() -> Unit){
val tr = Tr()
tr.block()
children.add(tr)
}
fun html():String{
val builder = StringBuilder()
builder.append("<table>")
for (childTag in children) {
builder.append(childTag.html())
}
builder.append("</table>")
return builder.toString()
}
}
这段代码相对就好理解多了,因为和刚才Tr 类中的代码是比较相似的。Table 类中同样创建了一个children 集合,用于存储当前Table 所包含的 Tr 对象。然后又定义了一个tr() 函数,它接收一个定义到Tr 类中的函数类型参数。当调用tr() 函数时,会先创建一个Tr 对象,接着调用函数类型参数,这样Lambda 表达式中的代码就能得到执行。最后,仍然要记得将创建的Tr 对象添加到children 集合当中。
除此之外,html() 方法的代码也都是类似的,这里遍历了children 集合,将所有的子Tr 对象都拼接到了
标签当中。那么现在,我们就可以使用如下语法结构来构建一个表格了:
val table = Table()
table.tr {
td { "Apple" }
td { "Grape" }
td { "Orange" }
}
table.tr {
td { "Pear" }
td { "Banana" }
td { "Watermelon" }
}
这段代码看上去已经相当不错了,不过这仍然不是最终版本,我们还可以再进一步对语法结构进行精简,定义一个table() 函数,代码如下所示:
fun table(block: Table.() -> Unit):String{
val table = Table()
table.block()
return table.html()
}
这里的table() 函数接收一个定义到Table 类中的函数类型参数,当调用table() 函数时,会先创建一个Table 对象,接着调用函数类型参数,这样Lambda 表达式中的代码就能得到执行。最后调用Table 的html() 方法获取生成的HTML 代码,并作为最终的返回值值返回。
编写了这么多代码之后,我们就可以使用如下神奇的语法结构来动态生成一个表格所对应的HTML 代码了:
fun main() {
val html= table {
tr {
td { "Apple" }
td { "Grape" }
td { "Orange" }
}
tr {
td { "Pear" }
td { "Banana" }
td { "Watermelon" }
}
}
println(html)
}
怎么样?这种DSL 结构的语法是不是语义性很强,一看就懂?而且很难想象这种语法结构竟然是用Kotlin 语言编写出来的吧?现在我们可以运行一下main()函数,结果如图所示:
可以看到,这样我们就能够轻松地生成任意表格所对应的HTML 代码了。
另外,在DSL 中也可以使用Kotlin 其他语法特性,比如通过循环来批量生成和标签:
fun main() {
val html= table {
repeat(2){
tr {
val fruits = listOf("Apple","Grape","Orange")
for (fruit in fruits) {
td { fruit }
}
}
}
}
println(html)
}
这里使用了repeat() 函数来作为表格生成两行数据,每行数据中又使用了 for-in 循环来遍历List 集合,为表格填充具体的单元格数据。最终的运行结果如图所示;