kotlin学习之使用DSL 构建专有的语法结构

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() 函数,结果如图所示:

1

可以看到,我们已经成功使用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,然后双击文件,使用浏览器打开即可,效果如图所示:

img

这就是一个两行三列表格的效果,只是默认情况下表格边框的宽度是零,所以我们看不到边框而已。

那么如果现在有一个需求,要求我们在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()函数,结果如图所示:

2

可以看到,这样我们就能够轻松地生成任意表格所对应的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 集合,为表格填充具体的单元格数据。最终的运行结果如图所示;

3

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值