【疯狂Android之Kotlin】 理解与使用Kotlin中的DSL

1,849 阅读6分钟

DSL简介

DSL是什么?

  • 所谓的DSL(Domain Specified Language)领域专用语言,其基本思想是“求专不求全”,不像通用目的语言那样目标范围涵盖一切软件问题,而是专门针对某一特定问题的计算机语言。以上是我拿官方的回答,是不是比较难懂?
  • 其实DSL并不是单独为Kotlin语言提供的,但是作为Android开发者一定使用并且一直在使用DSL,是不是觉得很惊奇,
  • 简单来说,通过DSL语言我们可以构建属于自己的语法结构,而在Kotlin中并不只有一种方式实现DSL,而主要的实现方式就是高阶函数,之后会有专栏进行介绍高阶函数
  • 下面我们看下build.gradle的代码
  dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
    implementation 'androidx.appcompat:appcompat:1.2.0'
    implementation 'androidx.core:core-ktx:1.3.1'
    implementation 'androidx.constraintlayout:constraintlayout:2.0.1'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'androidx.test.ext:junit:1.1.2'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
}

  • Gradle相信各位同学都知道它是基于Groovy的构建工具,上面的代码其实就是Groovy给我们提供的DSL功能。

常见的DSL

常见的DSL在很多领域都能看到,例如:

  • 软件构建领域 Ant
  • UI 设计师 HTML
  • 硬件设计师 VHDL

DSL 与通用编程语言的区别

  • DSL 供非程序员使用,供领域专家使用;
  • DSL 有更高级的抽象,不涉及类似数据结构的细节;
  • DSL表现力有限,其只能描述该领域的模型,而通用编程语言能够描述任意的模型;

DSL分类

根据是否从宿主语言构建而来,DSL 分为:

  • 内部 DSL(从一种宿主语言构建而来
  • 外部 DSL(从零开始构建的语言,需要实现语法分析器等)

DSL基础用法

接下来,我们来看看DSL在kotlin中如何构建自己的语法

  1. 首先我们新建一个Dependency,名字可以随便起,然后我们声明一个list数组,为list提供添加的数据的方法,类代码如下:
class Dependency {
    var libs = mutableListOf<String>()
    fun implementation(lib: String) {
        libs.add(lib)
    }
}
  1. 接着我们定义一个高阶函数,参数是Dependency的扩展函数
fun dependencies(block: Dependency.() -> Unit): List<String> {
    val dependency = Dependency()
    dependency.block()
    return dependency.libs
}

以上代码只要你了解高阶函数,肯定可以看得懂,高阶函数中的参数是Dependency的扩展函数,所以我们要先初始化一个Dependency,通过实例调用参数,就可以执行传入的Lambda表达式了,我们新建一个Test.kt,在main方法中使用如下

dependencies {
        implementation("com.jacky.ll")
        implementation("com.jacky.hh")
    }

因为定义的方法,返回的是List可以将它打印出来,代码如下所示:


var list = dependencies {
        implementation("com.jacky.ll")
        implementation("com.jacky.hh")
    }
    for (text in list) {
       println("$text")
    }
  • 运行程序,结果如下所示
com.jacky.ll
com.jacky.hh
 
Process finished with exit code 0

关于了解Android中的Gradle

Groovy简介

  • 一种运行在JVM虚拟机上的脚本语言,能够与Java语言无缝结合,如果想了解Groovy可以查看IBM-DeveloperWorks-精通Groovy。

打开Android的build.gradle文件,会看到类似下面的一些语法。

buildscript {
    repositories {
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:1.5.0'
 
        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
    }
}
 
allprojects {
    repositories {
        jcenter()
    }
}
 
task clean(type: Delete) {
    delete rootProject.buildDir
}

通过上面的Android的build.gradle配置文件可以发现,buildscript里有配置了repositories和dependencies,而repositories和dependencies里面又可以配置各自的一些属性。可以看出通过这种形式的配置,我们可以层次分明的看出整个项目构建的一些定制,又由于Android也遵循约定大于配置的设计思想,因此我们仅仅只需修改需要自定义的部分即可轻松个性化构建流程。

Groovy脚本-build.gradle

在Groovy下,我们可以像Python这类脚本语言一样写个脚本文件直接执行而无需像Java那样既要写好Class又要定义main()函数,因为Groovy本身就是一门脚本语言,而Gradle是基于Groovy语言的构建工具,自然也可以轻松通过脚本来执行构建整个项目。作为一个基于Gradle的项目工程,项目结构中的settings.gradle和build.gradle这类xxx.gradle可以理解成是Gradle构建该工程的执行脚本,当我们在键盘上敲出gradle clean aDebug这类命令的时候,Gradle就会去寻找这类文件并按照规则先后读取这些gradle文件并使用Groovy去解析执行。

Groovy语法

要理解build.gradle文件中的这些DSL是如何被解析执行的,需要介绍Groovy的一些语法特点以及一些高级特性,下面从几个方面来介绍Groovy的一些特点。

链式命令

  • Groovy的脚本具有链式命令(Command chains)的特性,根据这个特性,当你在Groovy脚本中写出a b c d的时候,Groovy会翻译成a(b).c(d)执行,也就是将b作为a函数的形参调用,然后将d作为形参再次调用返回的实例(Instance)中的c方法。
  • 其中当做形参的b和d可以作为一个闭包(Closure)传递过去。例如:
// equivalent to: turn(left).then(right)
turn left then right
 
// equivalent to: take(2.pills).of(chloroquinine).after(6.hours)
take 2.pills of chloroquinine after 6.hours
 
// equivalent to: paint(wall).with(red, green).and(yellow)
paint wall with red, green and yellow
 
// with named parameters too
// equivalent to: check(that: margarita).tastes(good)
check that: margarita tastes good
 
// with closures as parameters
// equivalent to: given({}).when({}).then({})
given { } when { } then { }
Groovy也支持某个方法传入空参数,但需要为该空参数的方法加上圆括号。例如:

// equivalent to: select(all).unique().from(names)
select all unique() from names
如果链式命令(Command chains)的参数是奇数,则最后一个参数会被当成属性值(Property)访问。例如:

// equivalent to: take(3).cookies
// and also this: take(3).getCookies()
take 3 cookies

操作符重载

  • 有了Groovy的操作符重载(Operator overloading),==会被Groovy转换成equals方法,这样你就可以放心大胆地使用==来比较两个字符串是否相等了,在我们编写gradle脚本的时候也可以尽情使用。
  • 关于Groovy的所有操作符重载(Operator overloading)可以查阅:Operator overloading官方教程

委托

委托(DelegatesTo)可以说是Gradle选择Groovy作为DSL执行平台的一个重要因素了。通过委托(DelegatesTo)可以很简单的定制一个控制结构体(Custom control structures),例如下面的代码。

email {
    from 'dsl-guru@mycompany.com'
    to 'john.doe@waitaminute.com'
    subject 'The pope has resigned!'
    body {
        p 'Really, the pope has resigned!'
    }
}

接下来可以看下解析上述DSL语言生成的代码。

def email(Closure cl) {
    def email = new EmailSpec()
    def code = cl.rehydrate(email, this, this)
    code.resolveStrategy = Closure.DELEGATE_ONLY
    code()
}

上述转换后的DSL语言,先定义了一个email(Closure)的方法,当执行上述步骤1的时候就会进入该方法内执行,EmailSpec是一个继承了参数中cl闭包里所有方法,比如from、to等等的一个类(Class),通过rehydrate方法将cl拷贝成一份新的实例(Instance)并赋值给code,code实例(Instance),通过rehydrate方法中设置delegate、owner和thisObject的三个属性将cl和email两者关联起来被赋予了一种委托关系,这种委托关系可以这样理解:cl闭包中的from、to等方法会调用到email委托类实例(Instance)中的方法,并可以访问到email中的实例变量(Field)。DELEGATE_ONLY表示闭包(Closure)方法调用只会委托给它的委托者(The delegate of closure),最后使用code()开始执行闭包中的方法。

扩展:DSK还能怎么用

DSL还可以将符合标准API规范的代码转化为符合人类理解的自然语言

这是什么意思?what

  1. 首先,我们以创建一个用户对象为例,新建User.kt,为了方便打印 我们重写toString方法,代码如下所示:
data class User(var name: String = "", var age: Int = 0) {
    override fun toString(): String {
        return "My name is $name ,i am $age years old"
    }
}
  1. 仍然在Test.kt中测试代码,下按照API规范我们如何来创建一个User对象
val user = User("jacky", 22)
    println(user)

运行结果如下所示:


My name is jacky ,i am 22 years old
 
Process finished with exit code 0
  1. 如何使用DSL的方式去创建一个User对象呢,首先我们需要提供一个高阶函数
fun create(block: User.() -> Unit): User {
    var user = User()
    block(user)
    return user
}

定义了一个类型为User扩展函数的高阶函数,通过block调用表达式的部分 所以我们可以直接这样来创建一个User对象:

val user1 = create {
    name = "jacky"
    age = 22
}
println(user1)

这种方式会更加合理一些,运行结果和上面一致,同学我就不展示出来了

结语

DSL的使用场景远远不止这些,其实前提就是使用好高阶函数,很多例子都讲到了使用DSL来生成HTML的代码,不过在业务中没get到他的作用,想了解的朋友可以私下和我沟通。其实不管任何一种技术,一个框架,我们不能评判他的好坏,存在即合理,推动项目开展才是王道。 好了 ,DSL的基础了解就到这里了。欢迎继续学习